routine.rs (18874B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2024-2025 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 borrow::Cow, 19 fmt::Debug, 20 future::Future, 21 time::{Duration, Instant}, 22 }; 23 24 use axum::Router; 25 use serde::{Deserialize, de::DeserializeOwned}; 26 use taler_api::db::IncomingType; 27 use taler_common::{ 28 api_common::{EddsaPublicKey, HashCode, ShortHashCode}, 29 api_params::PageParams, 30 api_revenue::RevenueIncomingHistory, 31 api_wire::{ 32 IncomingBankTransaction, IncomingHistory, TransferList, TransferResponse, TransferState, 33 TransferStatus, 34 }, 35 error_code::ErrorCode, 36 types::{amount::amount, base32::Base32, payto::PaytoURI, url}, 37 }; 38 use tokio::time::sleep; 39 40 use crate::{ 41 json, 42 server::{TestResponse, TestServer as _}, 43 }; 44 45 pub async fn routine_pagination<'a, T: DeserializeOwned, F: Future<Output = ()>>( 46 server: &'a Router, 47 url: &str, 48 ids: fn(T) -> Vec<i64>, 49 mut register: impl FnMut(&'a Router, usize) -> F, 50 ) { 51 // Check supported 52 if !server.get(url).await.is_implemented() { 53 return; 54 } 55 56 // Check history is following specs 57 let assert_history = |args: Cow<'static, str>, size: usize| async move { 58 let resp = server.get(&format!("{url}?{args}")).await; 59 assert_history_ids(&resp, ids, size) 60 }; 61 // Get latest registered id 62 let latest_id = || async move { assert_history("limit=-1".into(), 1).await[0] }; 63 64 for i in 0..20 { 65 register(server, i).await; 66 } 67 68 let id = latest_id().await; 69 70 // default 71 assert_history("".into(), 20).await; 72 73 // forward range 74 assert_history("limit=10".into(), 10).await; 75 assert_history("limit=10&offset=4".into(), 10).await; 76 77 // backward range 78 assert_history("limit=-10".into(), 10).await; 79 assert_history(format!("limit=-10&{}", id - 4).into(), 10).await; 80 } 81 82 pub async fn assert_time<R: Debug>(range: std::ops::Range<u128>, task: impl Future<Output = R>) { 83 let start = Instant::now(); 84 task.await; 85 let elapsed = start.elapsed().as_millis(); 86 if !range.contains(&elapsed) { 87 panic!("Expected to last {range:?} got {elapsed:?}") 88 } 89 } 90 91 pub async fn routine_history< 92 'a, 93 T: DeserializeOwned, 94 FR: Future<Output = ()>, 95 FI: Future<Output = ()>, 96 >( 97 server: &'a Router, 98 url: &str, 99 ids: fn(T) -> Vec<i64>, 100 nb_register: usize, 101 mut register: impl FnMut(&'a Router, usize) -> FR, 102 nb_ignore: usize, 103 mut ignore: impl FnMut(&'a Router, usize) -> FI, 104 ) { 105 // Check history is following specs 106 macro_rules! assert_history { 107 ($args:expr, $size:expr) => { 108 async { 109 let resp = server.get(&format!("{url}?{}", $args)).await; 110 assert_history_ids(&resp, ids, $size) 111 } 112 }; 113 } 114 // Get latest registered id 115 let latest_id = || async { assert_history!("limit=-1", 1).await[0] }; 116 117 // Check error when no transactions 118 assert_history!("limit=7".to_owned(), 0).await; 119 120 let mut register_iter = (0..nb_register).peekable(); 121 let mut ignore_iter = (0..nb_ignore).peekable(); 122 while register_iter.peek().is_some() || ignore_iter.peek().is_some() { 123 if let Some(idx) = register_iter.next() { 124 register(server, idx).await 125 } 126 if let Some(idx) = ignore_iter.next() { 127 ignore(server, idx).await 128 } 129 } 130 let nb_total = nb_register + nb_ignore; 131 132 // Check ignored 133 assert_history!(format_args!("limit={nb_total}"), nb_register).await; 134 // Check skip ignored 135 assert_history!(format_args!("limit={nb_register}"), nb_register).await; 136 137 // Check no polling when we cannot have more transactions 138 assert_time( 139 0..100, 140 assert_history!( 141 format_args!("limit=-{}&timeout_ms=1000", nb_register + 1), 142 nb_register 143 ), 144 ) 145 .await; 146 // Check no polling when already find transactions even if less than delta 147 assert_time( 148 0..100, 149 assert_history!( 150 format_args!("limit={}&timeout_ms=1000", nb_register + 1), 151 nb_register 152 ), 153 ) 154 .await; 155 156 // Check polling 157 let id = latest_id().await; 158 tokio::join!( 159 // Check polling succeed 160 assert_time( 161 100..300, 162 assert_history!(format_args!("limit=2&offset={id}&timeout_ms=1000"), 1) 163 ), 164 assert_time( 165 200..300, 166 assert_history!( 167 format_args!( 168 "limit=1&offset={}&timeout_ms=200", 169 id as usize + nb_total * 3 170 ), 171 0 172 ) 173 ), 174 async { 175 sleep(Duration::from_millis(100)).await; 176 register(server, 0).await 177 } 178 ); 179 180 // Test triggers 181 for i in 0..nb_register { 182 let id = latest_id().await; 183 tokio::join!( 184 // Check polling succeed 185 assert_time( 186 100..300, 187 assert_history!(format_args!("limit=7&offset={id}&timeout_ms=1000"), 1) 188 ), 189 async { 190 sleep(Duration::from_millis(100)).await; 191 register(server, i).await 192 } 193 ); 194 } 195 196 // Test doesn't trigger 197 let id = latest_id().await; 198 tokio::join!( 199 // Check polling succeed 200 assert_time( 201 200..300, 202 assert_history!(format_args!("limit=7&offset={id}&timeout_ms=200"), 0) 203 ), 204 async { 205 sleep(Duration::from_millis(100)).await; 206 for i in 0..nb_ignore { 207 ignore(server, i).await 208 } 209 } 210 ); 211 212 routine_pagination(server, url, ids, register).await; 213 } 214 215 #[track_caller] 216 fn assert_history_ids<'de, T: Deserialize<'de>>( 217 resp: &'de TestResponse, 218 ids: impl Fn(T) -> Vec<i64>, 219 size: usize, 220 ) -> Vec<i64> { 221 if size == 0 { 222 resp.assert_no_content(); 223 return vec![]; 224 } 225 let body = resp.assert_ok_json::<T>(); 226 let history: Vec<_> = ids(body); 227 let params = resp.query::<PageParams>().check(1024).unwrap(); 228 229 // testing the size is like expected 230 assert_eq!(size, history.len(), "bad history length: {history:?}"); 231 if params.limit < 0 { 232 // testing that the first id is at most the 'offset' query param. 233 assert!( 234 params 235 .offset 236 .map(|offset| history[0] <= offset) 237 .unwrap_or(true), 238 "bad history offset: {params:?} {history:?}" 239 ); 240 // testing that the id decreases. 241 assert!( 242 history.as_slice().is_sorted_by(|a, b| a > b), 243 "bad history order: {history:?}" 244 ) 245 } else { 246 // testing that the first id is at least the 'offset' query param. 247 assert!( 248 params 249 .offset 250 .map(|offset| history[0] >= offset) 251 .unwrap_or(true), 252 "bad history offset: {params:?} {history:?}" 253 ); 254 // testing that the id increases. 255 assert!( 256 history.as_slice().is_sorted(), 257 "bad history order: {history:?}" 258 ) 259 } 260 history 261 } 262 263 // Get currency from config 264 async fn get_currency(server: &Router) -> String { 265 let config = server 266 .get("/taler-wire-gateway/config") 267 .await 268 .assert_ok_json::<serde_json::Value>(); 269 let currency = config["currency"].as_str().unwrap(); 270 currency.to_owned() 271 } 272 273 /// Test standard behavior of the transfer endpoints 274 pub async fn transfer_routine( 275 server: &Router, 276 default_status: TransferState, 277 credit_account: &PaytoURI, 278 ) { 279 let currency = &get_currency(server).await; 280 let default_amount = amount(format!("{currency}:42")); 281 let transfer_request = json!({ 282 "request_uid": HashCode::rand(), 283 "amount": default_amount, 284 "exchange_base_url": "http://exchange.taler/", 285 "wtid": ShortHashCode::rand(), 286 "credit_account": credit_account, 287 }); 288 289 // Check empty db 290 { 291 server 292 .get("/taler-wire-gateway/transfers") 293 .await 294 .assert_no_content(); 295 server 296 .get(&format!( 297 "/taler-wire-gateway/transfers?status={}", 298 default_status.as_ref() 299 )) 300 .await 301 .assert_no_content(); 302 } 303 304 // Check create transfer 305 { 306 // Check OK 307 let first = server 308 .post("/taler-wire-gateway/transfer") 309 .json(&transfer_request) 310 .await 311 .assert_ok_json::<TransferResponse>(); 312 // Check idempotent 313 let second = server 314 .post("/taler-wire-gateway/transfer") 315 .json(&transfer_request) 316 .await 317 .assert_ok_json::<TransferResponse>(); 318 assert_eq!(first.row_id, second.row_id); 319 assert_eq!(first.timestamp, second.timestamp); 320 321 // Check request uid reuse 322 server 323 .post("/taler-wire-gateway/transfer") 324 .json(&json!(transfer_request + { 325 "wtid": ShortHashCode::rand() 326 })) 327 .await 328 .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED); 329 // Check wtid reuse 330 server 331 .post("/taler-wire-gateway/transfer") 332 .json(&json!(transfer_request + { 333 "request_uid": HashCode::rand(), 334 })) 335 .await 336 .assert_error(ErrorCode::BANK_TRANSFER_WTID_REUSED); 337 338 // Check currency mismatch 339 server 340 .post("/taler-wire-gateway/transfer") 341 .json(&json!(transfer_request + { 342 "amount": "BAD:42" 343 })) 344 .await 345 .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); 346 } 347 348 // Check transfer by id 349 { 350 let wtid = ShortHashCode::rand(); 351 let resp = server 352 .post("/taler-wire-gateway/transfer") 353 .json(&json!(transfer_request + { 354 "request_uid": HashCode::rand(), 355 "wtid": wtid, 356 })) 357 .await 358 .assert_ok_json::<TransferResponse>(); 359 360 // Check OK 361 let tx = server 362 .get(&format!("/taler-wire-gateway/transfers/{}", resp.row_id)) 363 .await 364 .assert_ok_json::<TransferStatus>(); 365 assert_eq!(default_status, tx.status); 366 assert_eq!(default_amount, tx.amount); 367 assert_eq!("http://exchange.taler/", tx.origin_exchange_url); 368 assert_eq!(wtid, tx.wtid); 369 assert_eq!(resp.timestamp, tx.timestamp); 370 371 // Check unknown transaction 372 server 373 .get("/taler-wire-gateway/transfers/42") 374 .await 375 .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); 376 } 377 378 // Check transfer page 379 { 380 for _ in 0..4 { 381 server 382 .post("/taler-wire-gateway/transfer") 383 .json(&json!(transfer_request + { 384 "request_uid": HashCode::rand(), 385 "wtid": ShortHashCode::rand(), 386 })) 387 .await 388 .assert_ok_json::<TransferResponse>(); 389 } 390 { 391 let list = server 392 .get("/taler-wire-gateway/transfers") 393 .await 394 .assert_ok_json::<TransferList>(); 395 assert_eq!(list.transfers.len(), 6); 396 assert_eq!( 397 list, 398 server 399 .get(&format!( 400 "/taler-wire-gateway/transfers?status={}", 401 default_status.as_ref() 402 )) 403 .await 404 .assert_ok_json::<TransferList>() 405 ) 406 } 407 408 // Pagination test 409 routine_pagination::<TransferList, _>( 410 server, 411 "/taler-wire-gateway/transfers", 412 |it| { 413 it.transfers 414 .into_iter() 415 .map(|it| *it.row_id as i64) 416 .collect() 417 }, 418 |server, i| async move { 419 server 420 .post("/taler-wire-gateway/transfer") 421 .json(&json!({ 422 "request_uid": HashCode::rand(), 423 "amount": amount(format!("{currency}:0.0{i}")), 424 "exchange_base_url": url("http://exchange.taler"), 425 "wtid": ShortHashCode::rand(), 426 "credit_account": credit_account, 427 })) 428 .await 429 .assert_ok_json::<TransferResponse>(); 430 }, 431 ) 432 .await; 433 } 434 } 435 436 async fn add_incoming_routine( 437 server: &Router, 438 currency: &str, 439 kind: IncomingType, 440 debit_acount: &PaytoURI, 441 ) { 442 let (path, key) = match kind { 443 IncomingType::reserve => ("/taler-wire-gateway/admin/add-incoming", "reserve_pub"), 444 IncomingType::kyc => ("/taler-wire-gateway/admin/add-kycauth", "account_pub"), 445 IncomingType::wad => unreachable!(), 446 }; 447 let valid_req = json!({ 448 "amount": format!("{currency}:44"), 449 key: EddsaPublicKey::rand(), 450 "debit_account": debit_acount, 451 }); 452 453 // Check OK 454 server.post(path).json(&valid_req).await.assert_ok(); 455 456 match kind { 457 IncomingType::reserve => { 458 // Trigger conflict due to reused reserve_pub 459 server 460 .post(path) 461 .json(&json!(valid_req + { 462 "amount": format!("{currency}:44.1"), 463 })) 464 .await 465 .assert_error(ErrorCode::BANK_DUPLICATE_RESERVE_PUB_SUBJECT) 466 } 467 IncomingType::kyc => { 468 // Non conflict on reuse 469 server.post(path).json(&valid_req).await.assert_ok(); 470 } 471 IncomingType::wad => unreachable!(), 472 } 473 474 // Currency mismatch 475 server 476 .post(path) 477 .json(&json!(valid_req + { 478 "amount": "BAD:33" 479 })) 480 .await 481 .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); 482 483 // Bad BASE32 reserve_pub 484 server 485 .post(path) 486 .json(&json!(valid_req + { 487 key: "I love chocolate" 488 })) 489 .await 490 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 491 492 server 493 .post(path) 494 .json(&json!(valid_req + { 495 key: Base32::<31>::rand() 496 })) 497 .await 498 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 499 500 // Bad payto kind 501 server 502 .post(path) 503 .json(&json!(valid_req + { 504 "debit_account": "http://email@test.com" 505 })) 506 .await 507 .assert_error(ErrorCode::GENERIC_JSON_INVALID); 508 } 509 510 /// Test standard behavior of the revenue endpoints 511 pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) { 512 let currency = &get_currency(server).await; 513 514 routine_history( 515 server, 516 "/taler-revenue/history", 517 |it: RevenueIncomingHistory| { 518 it.incoming_transactions 519 .into_iter() 520 .map(|it| *it.row_id as i64) 521 .collect() 522 }, 523 2, 524 |server, i| async move { 525 if i % 2 == 0 || !kyc { 526 server 527 .post("/taler-wire-gateway/admin/add-incoming") 528 .json(&json!({ 529 "amount": format!("{currency}:0.0{i}"), 530 "reserve_pub": EddsaPublicKey::rand(), 531 "debit_account": debit_acount, 532 })) 533 .await 534 .assert_ok_json::<TransferResponse>(); 535 } else { 536 server 537 .post("/taler-wire-gateway/admin/add-kycauth") 538 .json(&json!({ 539 "amount": format!("{currency}:0.0{i}"), 540 "account_pub": EddsaPublicKey::rand(), 541 "debit_account": debit_acount, 542 })) 543 .await 544 .assert_ok_json::<TransferResponse>(); 545 } 546 }, 547 0, 548 |_, _| async move {}, 549 ) 550 .await; 551 } 552 553 /// Test standard behavior of the admin add incoming endpoints 554 pub async fn admin_add_incoming_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) { 555 let currency = &get_currency(server).await; 556 557 // History 558 // TODO check non taler some are ignored 559 routine_history( 560 server, 561 "/taler-wire-gateway/history/incoming", 562 |it: IncomingHistory| { 563 it.incoming_transactions 564 .into_iter() 565 .map(|it| match it { 566 IncomingBankTransaction::Reserve { row_id, .. } 567 | IncomingBankTransaction::Wad { row_id, .. } 568 | IncomingBankTransaction::Kyc { row_id, .. } => *row_id as i64, 569 }) 570 .collect() 571 }, 572 2, 573 |server, i| async move { 574 if i % 2 == 0 || !kyc { 575 server 576 .post("/taler-wire-gateway/admin/add-incoming") 577 .json(&json!({ 578 "amount": format!("{currency}:0.0{i}"), 579 "reserve_pub": EddsaPublicKey::rand(), 580 "debit_account": debit_acount, 581 })) 582 .await 583 .assert_ok_json::<TransferResponse>(); 584 } else { 585 server 586 .post("/taler-wire-gateway/admin/add-kycauth") 587 .json(&json!({ 588 "amount": format!("{currency}:0.0{i}"), 589 "account_pub": EddsaPublicKey::rand(), 590 "debit_account": debit_acount, 591 })) 592 .await 593 .assert_ok_json::<TransferResponse>(); 594 } 595 }, 596 0, 597 |_, _| async move {}, 598 ) 599 .await; 600 // Add incoming reserve 601 add_incoming_routine(server, currency, IncomingType::reserve, debit_acount).await; 602 if kyc { 603 // Add incoming kyc 604 add_incoming_routine(server, currency, IncomingType::kyc, debit_acount).await; 605 } 606 }