test.rs (9999B)
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_macros::db_test; 45 use taler_test_utils::{ 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(db: &PgPool) -> Router { 98 test_api(db.clone(), "EUR".parse().unwrap()).finalize() 99 } 100 101 #[db_test(sql_dir = "db", component = "taler-api")] 102 async fn body_parsing(db: PgPool) { 103 let server = setup(&db).await; 104 let normal_body = TransferRequest { 105 request_uid: Base32::rand(), 106 amount: Amount::zero(&Currency::EUR), 107 exchange_base_url: url("https://test.com"), 108 wtid: Base32::rand(), 109 credit_account: EXCHANGE.clone(), 110 metadata: None, 111 }; 112 113 // Check OK 114 server 115 .post("/taler-wire-gateway/transfer") 116 .json(&normal_body) 117 .deflate() 118 .await 119 .assert_ok_json::<TransferResponse>(); 120 121 // Headers check 122 server 123 .post("/taler-wire-gateway/transfer") 124 .json(&normal_body) 125 .remove(header::CONTENT_TYPE) 126 .await 127 .assert_error_status( 128 ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED, 129 StatusCode::UNSUPPORTED_MEDIA_TYPE, 130 ); 131 server 132 .post("/taler-wire-gateway/transfer") 133 .json(&normal_body) 134 .deflate() 135 .remove(header::CONTENT_ENCODING) 136 .await 137 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 138 server 139 .post("/taler-wire-gateway/transfer") 140 .json(&normal_body) 141 .header(header::CONTENT_TYPE, "invalid") 142 .await 143 .assert_error_status( 144 ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED, 145 StatusCode::UNSUPPORTED_MEDIA_TYPE, 146 ); 147 server 148 .post("/taler-wire-gateway/transfer") 149 .json(&normal_body) 150 .header(header::CONTENT_ENCODING, "deflate") 151 .await 152 .assert_error(ErrorCode::GENERIC_COMPRESSION_INVALID); 153 server 154 .post("/taler-wire-gateway/transfer") 155 .json(&normal_body) 156 .header(header::CONTENT_ENCODING, "invalid") 157 .await 158 .assert_error_status( 159 ErrorCode::GENERIC_HTTP_HEADERS_MALFORMED, 160 StatusCode::UNSUPPORTED_MEDIA_TYPE, 161 ); 162 163 // Body size limit 164 let huge_body = TransferRequest { 165 credit_account: payto(format!( 166 "payto:://test?message={:A<1$}", 167 "payout", MAX_BODY_LENGTH 168 )), 169 ..normal_body 170 }; 171 server 172 .post("/taler-wire-gateway/transfer") 173 .json(&huge_body) 174 .await 175 .assert_error(ErrorCode::GENERIC_UPLOAD_EXCEEDS_LIMIT); 176 server 177 .post("/taler-wire-gateway/transfer") 178 .json(&huge_body) 179 .deflate() 180 .await 181 .assert_error(ErrorCode::GENERIC_UPLOAD_EXCEEDS_LIMIT); 182 } 183 184 #[db_test(sql_dir = "db", component = "taler-api")] 185 async fn errors(db: PgPool) { 186 let server = setup(&db).await; 187 server 188 .get("/unknown") 189 .await 190 .assert_error(ErrorCode::GENERIC_ENDPOINT_UNKNOWN); 191 server 192 .post("/taler-revenue/config") 193 .await 194 .assert_error(ErrorCode::GENERIC_METHOD_INVALID); 195 } 196 197 #[db_test(sql_dir = "db", component = "taler-api")] 198 async fn config(db: PgPool) { 199 let server = setup(&db).await; 200 server 201 .get("/taler-wire-gateway/config") 202 .await 203 .assert_ok_json::<WireConfig>(); 204 server 205 .get("/taler-prepared-transfer/config") 206 .await 207 .assert_ok_json::<PreparedTransferConfig>(); 208 server 209 .get("/taler-revenue/config") 210 .await 211 .assert_ok_json::<RevenueConfig>(); 212 } 213 214 #[db_test(sql_dir = "db", component = "taler-api")] 215 async fn transfer(db: PgPool) { 216 let server = setup(&db).await; 217 transfer_routine( 218 &server.prefix("/taler-wire-gateway"), 219 TransferState::success, 220 &EXCHANGE, 221 ) 222 .await; 223 } 224 225 #[db_test(sql_dir = "db", component = "taler-api")] 226 async fn outgoing_history(db: PgPool) { 227 let server = setup(&db).await; 228 out_history_routine( 229 &server.prefix("/taler-wire-gateway"), 230 tasks!({ 231 server 232 .post("/taler-wire-gateway/transfer") 233 .json(json!({ 234 "request_uid": HashCode::rand(), 235 "amount": amount("EUR:1"), 236 "exchange_base_url": url("http://exchange.taler"), 237 "wtid": ShortHashCode::rand(), 238 "credit_account": EXCHANGE.clone(), 239 })) 240 .await 241 .assert_ok_json::<TransferResponse>(); 242 }), 243 tasks!(), 244 ) 245 .await; 246 } 247 248 #[db_test(sql_dir = "db", component = "taler-api")] 249 async fn admin_add_incoming(db: PgPool) { 250 let server = setup(&db).await; 251 admin_add_incoming_routine( 252 &server.prefix("/taler-wire-gateway"), 253 &server.prefix("/taler-prepared-transfer"), 254 &EXCHANGE, 255 &EXCHANGE, 256 ) 257 .await; 258 } 259 260 #[db_test(sql_dir = "db", component = "taler-api")] 261 async fn in_history(db: PgPool) { 262 let server = setup(&db).await; 263 in_history_routine( 264 &server.prefix("/taler-wire-gateway"), 265 &server.prefix("/taler-prepared-transfer"), 266 &EXCHANGE, 267 &EXCHANGE, 268 tasks!(), 269 tasks!(), 270 ) 271 .await; 272 } 273 274 #[db_test(sql_dir = "db", component = "taler-api")] 275 async fn revenue(db: PgPool) { 276 let server = setup(&db).await; 277 revenue_routine( 278 &server.prefix("/taler-wire-gateway"), 279 &server.prefix("/taler-revenue"), 280 &EXCHANGE, 281 tasks!(), 282 tasks!(), 283 ) 284 .await; 285 } 286 287 #[db_test(sql_dir = "db", component = "taler-api")] 288 async fn account_check(db: PgPool) { 289 let server = setup(&db).await; 290 server 291 .get("/taler-wire-gateway/account/check") 292 .query("account", "payto://test") 293 .await 294 .assert_status(StatusCode::NOT_IMPLEMENTED); 295 } 296 297 async fn check_in(pool: &PgPool) -> Vec<Status> { 298 sqlx::query( 299 " 300 SELECT pending_recurrent_in.authorization_pub IS NOT NULL, bounced.tx_in_id IS NOT NULL, type, taler_in.account_pub 301 FROM tx_in 302 LEFT JOIN taler_in USING (tx_in_id) 303 LEFT JOIN pending_recurrent_in USING (tx_in_id) 304 LEFT JOIN bounced USING (tx_in_id) 305 ORDER BY tx_in.tx_in_id 306 ", 307 ) 308 .try_map(|r: PgRow| { 309 Ok( 310 if r.try_get_flag(0)? { 311 Status::Pending 312 } else if r.try_get_flag(1)? { 313 Status::Bounced 314 } else { 315 match r.try_get(2)? { 316 None => Status::Simple, 317 Some(IncomingType::reserve) => Status::Reserve(r.try_get(3)?), 318 Some(IncomingType::kyc) => Status::Kyc(r.try_get(3)?), 319 Some(e) => unreachable!("{e:?}") 320 } 321 } 322 ) 323 }) 324 .fetch_all(pool) 325 .await 326 .unwrap() 327 } 328 329 #[db_test(sql_dir = "db", component = "taler-api")] 330 async fn registration(db: PgPool) { 331 let server = setup(&db).await; 332 registration_routine( 333 &server.prefix("/taler-wire-gateway"), 334 &server.prefix("/taler-prepared-transfer"), 335 &EXCHANGE, 336 &EXCHANGE, 337 &UNKNOWN, 338 || check_in(&db), 339 ) 340 .await; 341 }