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