test.rs (8988B)
1 use std::{ 2 str::FromStr, 3 sync::{Arc, LazyLock}, 4 }; 5 6 use axum::{ 7 Router, 8 http::{StatusCode, header}, 9 }; 10 use serde_json::json; 11 use sqlx::{PgPool, Row as _, postgres::PgRow}; 12 use taler_common::{ 13 api::{ 14 HashCode, ShortHashCode, 15 prepared::PreparedTransferConfig, 16 revenue::RevenueConfig, 17 wire::{TransferRequest, TransferResponse, TransferState, WireConfig}, 18 }, 19 db::IncomingType, 20 error_code::ErrorCode, 21 types::{ 22 amount::{Amount, Currency, amount}, 23 base32::Base32, 24 payto::{FullIbanPayto, PaytoURI, payto}, 25 url, 26 }, 27 }; 28 use taler_test_utils::{ 29 db::db_test_setup_manual, 30 routine::{ 31 Status, admin_add_incoming_routine, in_history_routine, out_history_routine, 32 registration_routine, revenue_routine, transfer_routine, 33 }, 34 server::TestServer, 35 tasks, 36 }; 37 use tokio::sync::watch::Sender; 38 39 use crate::{ 40 api::TalerRouter, 41 auth::AuthMethod, 42 constants::MAX_BODY_LENGTH, 43 db::TypeHelper as _, 44 test::{api::TestApi, db::notification_listener}, 45 }; 46 47 mod api; 48 mod db; 49 50 static PAYTO: LazyLock<FullIbanPayto> = LazyLock::new(|| { 51 FullIbanPayto::from_str("payto://iban/HU02162000031000164800000000?receiver-name=Smith") 52 .unwrap() 53 }); 54 static EXCHANGE: LazyLock<PaytoURI> = LazyLock::new(|| PAYTO.as_uri()); 55 static UNKNOWN: LazyLock<PaytoURI> = 56 LazyLock::new(|| payto("payto://iban/HU60162006491000639900000000?receiver-name=Unknown")); 57 58 fn test_api(pool: PgPool, currency: Currency) -> Router { 59 let outgoing_channel = Sender::new(0); 60 let incoming_channel = Sender::new(0); 61 let wg = TestApi { 62 currency, 63 pool: pool.clone(), 64 payto: PAYTO.clone(), 65 outgoing_channel: outgoing_channel.clone(), 66 incoming_channel: incoming_channel.clone(), 67 }; 68 tokio::spawn(notification_listener( 69 pool, 70 outgoing_channel, 71 incoming_channel, 72 )); 73 let state = Arc::new(wg); 74 Router::new() 75 .wire_gateway(state.clone(), AuthMethod::None) 76 .prepared_transfer(state.clone()) 77 .revenue(state, AuthMethod::None) 78 } 79 80 async fn setup() -> (Router, PgPool) { 81 let (_, pool) = db_test_setup_manual("db".as_ref(), "taler-api").await; 82 ( 83 test_api(pool.clone(), "EUR".parse().unwrap()).finalize(), 84 pool, 85 ) 86 } 87 88 #[tokio::test] 89 async fn body_parsing() { 90 let (server, _) = setup().await; 91 let normal_body = TransferRequest { 92 request_uid: Base32::rand(), 93 amount: Amount::zero(&Currency::EUR), 94 exchange_base_url: url("https://test.com"), 95 wtid: Base32::rand(), 96 credit_account: EXCHANGE.clone(), 97 metadata: None, 98 }; 99 100 // Check OK 101 server 102 .post("/taler-wire-gateway/transfer") 103 .json(&normal_body) 104 .deflate() 105 .await 106 .assert_ok_json::<TransferResponse>(); 107 108 // Headers check 109 server 110 .post("/taler-wire-gateway/transfer") 111 .json(&normal_body) 112 .remove(header::CONTENT_TYPE) 113 .await 114 .assert_error_status( 115 ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED, 116 StatusCode::UNSUPPORTED_MEDIA_TYPE, 117 ); 118 server 119 .post("/taler-wire-gateway/transfer") 120 .json(&normal_body) 121 .deflate() 122 .remove(header::CONTENT_ENCODING) 123 .await 124 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 125 server 126 .post("/taler-wire-gateway/transfer") 127 .json(&normal_body) 128 .header(header::CONTENT_TYPE, "invalid") 129 .await 130 .assert_error_status( 131 ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED, 132 StatusCode::UNSUPPORTED_MEDIA_TYPE, 133 ); 134 server 135 .post("/taler-wire-gateway/transfer") 136 .json(&normal_body) 137 .header(header::CONTENT_ENCODING, "deflate") 138 .await 139 .assert_error(ErrorCode::GENERIC_COMPRESSION_INVALID); 140 server 141 .post("/taler-wire-gateway/transfer") 142 .json(&normal_body) 143 .header(header::CONTENT_ENCODING, "invalid") 144 .await 145 .assert_error_status( 146 ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED, 147 StatusCode::UNSUPPORTED_MEDIA_TYPE, 148 ); 149 150 // Body size limit 151 let huge_body = TransferRequest { 152 credit_account: payto(format!( 153 "payto:://test?message={:A<1$}", 154 "payout", MAX_BODY_LENGTH 155 )), 156 ..normal_body 157 }; 158 server 159 .post("/taler-wire-gateway/transfer") 160 .json(&huge_body) 161 .await 162 .assert_error(ErrorCode::GENERIC_UPLOAD_EXCEEDS_LIMIT); 163 server 164 .post("/taler-wire-gateway/transfer") 165 .json(&huge_body) 166 .deflate() 167 .await 168 .assert_error(ErrorCode::GENERIC_UPLOAD_EXCEEDS_LIMIT); 169 } 170 171 #[tokio::test] 172 async fn errors() { 173 let (server, _) = setup().await; 174 server 175 .get("/unknown") 176 .await 177 .assert_error(ErrorCode::GENERIC_ENDPOINT_UNKNOWN); 178 server 179 .post("/taler-revenue/config") 180 .await 181 .assert_error(ErrorCode::GENERIC_METHOD_INVALID); 182 } 183 184 #[tokio::test] 185 async fn config() { 186 let (server, _) = setup().await; 187 server 188 .get("/taler-wire-gateway/config") 189 .await 190 .assert_ok_json::<WireConfig>(); 191 server 192 .get("/taler-prepared-transfer/config") 193 .await 194 .assert_ok_json::<PreparedTransferConfig>(); 195 server 196 .get("/taler-revenue/config") 197 .await 198 .assert_ok_json::<RevenueConfig>(); 199 } 200 201 #[tokio::test] 202 async fn transfer() { 203 let (server, _) = setup().await; 204 transfer_routine( 205 &server.prefix("/taler-wire-gateway"), 206 TransferState::success, 207 &EXCHANGE, 208 ) 209 .await; 210 } 211 212 #[tokio::test] 213 async fn outgoing_history() { 214 let (server, _) = &setup().await; 215 out_history_routine( 216 &server.prefix("/taler-wire-gateway"), 217 tasks!({ 218 server 219 .post("/taler-wire-gateway/transfer") 220 .json(json!({ 221 "request_uid": HashCode::rand(), 222 "amount": amount("EUR:1"), 223 "exchange_base_url": url("http://exchange.taler"), 224 "wtid": ShortHashCode::rand(), 225 "credit_account": EXCHANGE.clone(), 226 })) 227 .await 228 .assert_ok_json::<TransferResponse>(); 229 }), 230 tasks!(), 231 ) 232 .await; 233 } 234 235 #[tokio::test] 236 async fn admin_add_incoming() { 237 let (server, _) = setup().await; 238 admin_add_incoming_routine( 239 &server.prefix("/taler-wire-gateway"), 240 &server.prefix("/taler-prepared-transfer"), 241 &EXCHANGE, 242 &EXCHANGE, 243 true, 244 ) 245 .await; 246 } 247 248 #[tokio::test] 249 async fn in_history() { 250 let (server, _) = setup().await; 251 in_history_routine( 252 &server.prefix("/taler-wire-gateway"), 253 &server.prefix("/taler-prepared-transfer"), 254 &EXCHANGE, 255 &EXCHANGE, 256 true, 257 tasks!(), 258 tasks!(), 259 ) 260 .await; 261 } 262 263 #[tokio::test] 264 async fn revenue() { 265 let (server, _) = setup().await; 266 revenue_routine( 267 &server.prefix("/taler-wire-gateway"), 268 &server.prefix("/taler-revenue"), 269 &EXCHANGE, 270 true, 271 tasks!(), 272 tasks!(), 273 ) 274 .await; 275 } 276 277 #[tokio::test] 278 async fn account_check() { 279 let (server, _) = setup().await; 280 server 281 .get("/taler-wire-gateway/account/check") 282 .query("account", "payto://test") 283 .await 284 .assert_status(StatusCode::NOT_IMPLEMENTED); 285 } 286 287 async fn check_in(pool: &PgPool) -> Vec<Status> { 288 sqlx::query( 289 " 290 SELECT pending_recurrent_in.authorization_pub IS NOT NULL, bounced.tx_in_id IS NOT NULL, type, taler_in.account_pub 291 FROM tx_in 292 LEFT JOIN taler_in USING (tx_in_id) 293 LEFT JOIN pending_recurrent_in USING (tx_in_id) 294 LEFT JOIN bounced USING (tx_in_id) 295 ORDER BY tx_in.tx_in_id 296 ", 297 ) 298 .try_map(|r: PgRow| { 299 Ok( 300 if r.try_get_flag(0)? { 301 Status::Pending 302 } else if r.try_get_flag(1)? { 303 Status::Bounced 304 } else { 305 match r.try_get(2)? { 306 None => Status::Simple, 307 Some(IncomingType::reserve) => Status::Reserve(r.try_get(3)?), 308 Some(IncomingType::kyc) => Status::Kyc(r.try_get(3)?), 309 Some(e) => unreachable!("{e:?}") 310 } 311 } 312 ) 313 }) 314 .fetch_all(pool) 315 .await 316 .unwrap() 317 } 318 319 #[tokio::test] 320 async fn registration() { 321 let (server, pool) = setup().await; 322 registration_routine( 323 &server.prefix("/taler-wire-gateway"), 324 &server.prefix("/taler-prepared-transfer"), 325 &EXCHANGE, 326 &EXCHANGE, 327 &UNKNOWN, 328 || check_in(&pool), 329 ) 330 .await; 331 }