test.rs (14362B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 use std::{fmt::Display, str::FromStr as _, sync::LazyLock}; 21 22 use compact_str::CompactString; 23 use jiff::Timestamp; 24 use libeufin_ebics::iso20022::model::{InId, InTx, OutId, OutTx}; 25 use sqlx::PgPool; 26 use taler_api::subject::{fmt_in_subject, fmt_out_subject, subject_fmt_qr_bill}; 27 use taler_common::{ 28 api::{EddsaPublicKey, EddsaSignature}, 29 db::IncomingType, 30 types::{ 31 amount::{Amount, Currency}, 32 base32::Base32, 33 payto::{FullIbanPayto, PaytoURI, payto}, 34 }, 35 }; 36 37 use crate::{ 38 config::{AccountType, NexusIngestCfg}, 39 db::{ 40 initiated::{PaymentInitiationResult, initiate}, 41 transfer::{RegistrationResult, transfer_register}, 42 }, 43 fetch::{register_incoming, register_outgoing}, 44 model::Initiated, 45 rand_ebics_id, 46 }; 47 48 pub const CURR: Currency = Currency::KUDOS; 49 pub static EXCHANGE: LazyLock<FullIbanPayto> = LazyLock::new(|| { 50 FullIbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Test").unwrap() 51 }); 52 pub static CLIENT: LazyLock<FullIbanPayto> = LazyLock::new(|| { 53 FullIbanPayto::from_str("payto://iban/DE84500105177118117964?receiver-name=John+Smith").unwrap() 54 }); 55 pub static UNKNOWN: LazyLock<PaytoURI> = 56 LazyLock::new(|| payto("payto://iban/HU60162006491000639900000000?receiver-name=Unknown")); 57 58 /** Generates an outgoing payment, given its subject */ 59 pub fn gen_out_pay(subject: impl Into<String>) -> OutTx { 60 OutTx { 61 id: OutId { 62 msg_id: None, 63 e2e_id: Some(rand_ebics_id()), 64 sref: None, 65 }, 66 amount: Amount::new(&CURR, 44, 0), 67 debit_fee: Amount::zero(&CURR), 68 creditor: Some(EXCHANGE.as_uri()), 69 subject: Some(subject.into()), 70 execution_time: Timestamp::now(), 71 } 72 } 73 74 /** Generates a payment initiation for testing */ 75 #[macro_export] 76 macro_rules! gen_init_pay { 77 ($end_to_end_id:expr) => { 78 gen_init_pay!($end_to_end_id, "init payment") 79 }; 80 ($end_to_end_id:expr, $subject:expr) => { 81 gen_init_pay!( 82 $end_to_end_id, 83 $subject, 84 taler_common::types::amount::Amount::new(&$crate::test::CURR, 44, 0) 85 ) 86 }; 87 ($end_to_end_id:expr, $subject:expr, $amount:expr) => { 88 Initiated { 89 id: 0, 90 amount: $amount, 91 creditor: $crate::test::EXCHANGE.clone(), 92 subject: $subject.into(), 93 initiation_time: jiff::Timestamp::now(), 94 e2e_id: $end_to_end_id.into(), 95 } 96 }; 97 } 98 99 /** Generates an incoming payment, given its subject */ 100 pub fn gen_in_pay(subject: impl Display) -> InTx { 101 InTx { 102 id: InId::new(None, Some(rand_ebics_id()), None), 103 amount: Amount::new(&CURR, 44, 0), 104 credit_fee: Amount::zero(&CURR), 105 debtor: Some(CLIENT.as_uri()), 106 subject: Some(subject.to_string()), 107 execution_time: Timestamp::now(), 108 } 109 } 110 111 pub async fn gen_initiate( 112 db: &PgPool, 113 end_to_end_id: impl Into<CompactString>, 114 subject: impl Into<String>, 115 ) -> PaymentInitiationResult { 116 let init = gen_init_pay!(end_to_end_id, subject); 117 initiate( 118 db, 119 &init.amount, 120 &init.subject, 121 &init.creditor, 122 &init.initiation_time, 123 &init.e2e_id, 124 ) 125 .await 126 .unwrap() 127 } 128 129 const CFG: NexusIngestCfg = NexusIngestCfg::simple(AccountType::exchange, &CURR); 130 131 async fn prepare(db: &PgPool) -> String { 132 let key = EddsaPublicKey::rand(); 133 let sig = EddsaSignature::rand(); 134 let reference_number = subject_fmt_qr_bill(key.as_ref()); 135 assert_eq!( 136 RegistrationResult::Success, 137 transfer_register( 138 db, 139 IncomingType::reserve, 140 &key, 141 &key, 142 &sig, 143 false, 144 Some(&reference_number), 145 &Timestamp::now() 146 ) 147 .await 148 .unwrap() 149 ); 150 reference_number 151 } 152 153 /// Register a talerable reserve prepared incoming transaction 154 pub async fn prepared_in(db: &PgPool) { 155 let ref_nb = prepare(db).await; 156 register_incoming(db, &CFG, &gen_in_pay(ref_nb)) 157 .await 158 .unwrap(); 159 } 160 161 /// Register an incomplete talerable reserve prepared incoming transaction 162 pub async fn prepared_incomplete_in(db: &PgPool) { 163 let ref_nb = prepare(db).await; 164 let incomplete = InTx { 165 subject: None, 166 debtor: None, 167 ..gen_in_pay(ref_nb) 168 }; 169 register_incoming(db, &CFG, &incomplete).await.unwrap(); 170 } 171 172 /// Register a completed talerable reserve prepared incoming transaction 173 pub async fn prepared_completeted_in(db: &PgPool) { 174 let ref_nb = prepare(db).await; 175 let original = gen_in_pay(ref_nb); 176 let incomplete = InTx { 177 subject: None, 178 debtor: None, 179 ..original.clone() 180 }; 181 register_incoming(db, &CFG, &incomplete).await.unwrap(); 182 register_incoming(db, &CFG, &original).await.unwrap(); 183 } 184 185 /// Register a talerable reserve incoming transaction 186 pub async fn talerable_in(db: &PgPool) { 187 register_incoming( 188 db, 189 &CFG, 190 &gen_in_pay(fmt_in_subject( 191 IncomingType::reserve, 192 &EddsaPublicKey::rand(), 193 )), 194 ) 195 .await 196 .unwrap(); 197 } 198 199 /// Register a talerable kyc incoming transaction 200 pub async fn talerable_kyc_in(db: &PgPool) { 201 register_incoming( 202 db, 203 &CFG, 204 &gen_in_pay(fmt_in_subject(IncomingType::kyc, &EddsaPublicKey::rand())), 205 ) 206 .await 207 .unwrap(); 208 } 209 210 /// Register an incomplete talerable reserve incoming transaction 211 pub async fn talerable_incomplete_in(db: &PgPool) { 212 let incomplete = InTx { 213 subject: None, 214 debtor: None, 215 ..gen_in_pay(fmt_in_subject( 216 IncomingType::reserve, 217 &EddsaPublicKey::rand(), 218 )) 219 }; 220 register_incoming(db, &CFG, &incomplete).await.unwrap(); 221 } 222 223 /// Register a completed talerable reserve incoming transaction 224 pub async fn talerable_completeted_in(db: &PgPool) { 225 let original = gen_in_pay(fmt_in_subject( 226 IncomingType::reserve, 227 &EddsaPublicKey::rand(), 228 )); 229 let incomplete = InTx { 230 subject: None, 231 debtor: None, 232 ..original.clone() 233 }; 234 register_incoming(db, &CFG, &incomplete).await.unwrap(); 235 register_incoming(db, &CFG, &original).await.unwrap(); 236 } 237 238 /// Register incoming malformed transaction 239 pub async fn malformed_in(db: &PgPool) { 240 register_incoming(db, &CFG, &gen_in_pay("ignored")) 241 .await 242 .unwrap(); 243 } 244 245 /// Register incoming incomplete malformed incoming transaction 246 pub async fn malformed_incomplete_in(db: &PgPool) { 247 let incomplete = InTx { 248 subject: None, 249 debtor: None, 250 ..gen_in_pay("ignored") 251 }; 252 register_incoming(db, &CFG, &incomplete).await.unwrap(); 253 } 254 255 /// Register incoming completed malformed transaction 256 pub async fn malformed_completeted_in(db: &PgPool) { 257 let original = gen_in_pay("ignored"); 258 let incomplete = InTx { 259 subject: None, 260 debtor: None, 261 ..original.clone() 262 }; 263 register_incoming(db, &CFG, &incomplete).await.unwrap(); 264 register_incoming(db, &CFG, &original).await.unwrap(); 265 } 266 267 /** Register an outgoing transaction */ 268 pub async fn malformed_out(db: &PgPool) { 269 register_outgoing(db, &gen_out_pay("ignored")) 270 .await 271 .unwrap(); 272 } 273 274 /** Register an incomplete outgoing transaction */ 275 pub async fn incomplete_out(db: &PgPool) { 276 let incomplete = OutTx { 277 subject: None, 278 creditor: None, 279 ..gen_out_pay("ignored") 280 }; 281 register_outgoing(db, &incomplete).await.unwrap(); 282 } 283 284 /// Register outgoing talerable transaction 285 pub async fn talerable_out(db: &PgPool) { 286 register_outgoing( 287 db, 288 &gen_out_pay(fmt_out_subject( 289 &Base32::rand(), 290 "https://exchange.test.com", 291 None, 292 )), 293 ) 294 .await 295 .unwrap(); 296 } 297 298 #[cfg(test)] 299 mod ebics { 300 use clap::Parser as _; 301 use libeufin_ebics::test::{EbicsState, TestBank}; 302 use sqlx::PgPool; 303 use taler_common::config::Config; 304 use taler_macros::db_test; 305 306 use crate::{ 307 Args, 308 constants::{CHECKPOINT_KEY, CONFIG_SOURCE}, 309 run, 310 }; 311 312 pub async fn nexus_cmd(cfg: &Config, cmd: &str) -> anyhow::Result<()> { 313 let parts = shlex::split(cmd).unwrap(); 314 let args = std::iter::once("libeufin_nexus").chain(parts.iter().map(|it| it.as_str())); 315 316 let cmd = Args::try_parse_from(args).unwrap(); 317 run(cfg, cmd.cmd).await 318 } 319 320 async fn test_setup(db: &PgPool) -> (TestBank, Config) { 321 let test = TestBank::new().await; 322 let cfg = Config::from_mem_with_env( 323 CONFIG_SOURCE, 324 &format!( 325 " 326 [paths] 327 LIBEUFIN_NEXUS_HOME = {:?} 328 329 {} 330 331 [nexus-ebics] 332 UNIXPATH = {} 333 334 [libeufin-nexusdb-postgres] 335 CONFIG = postgresql:///{} 336 ", 337 test.dir.path(), 338 include_str!("../../testbench/conf/mini.conf"), 339 test.sock_path, 340 db.connect_options().get_database().unwrap() 341 ), 342 ) 343 .unwrap(); 344 test.sequences(&[ 345 EbicsState::hev, 346 EbicsState::ini, 347 EbicsState::hia, 348 EbicsState::hpb, 349 EbicsState::hkd, 350 EbicsState::receipt_ok, 351 ]); 352 nexus_cmd(&cfg, "ebics-setup --auto-accept-keys") 353 .await 354 .unwrap(); 355 356 (test, cfg) 357 } 358 359 #[db_test] 360 async fn setup(db: PgPool) { 361 test_setup(&db).await; 362 } 363 364 #[db_test] 365 async fn fetch_pinned_date(db: PgPool) { 366 let (test, cfg) = test_setup(&db).await; 367 368 let reset_checkpoint = async || { 369 let res = sqlx::query("DELETE FROM kv WHERE key=$1") 370 .bind(CHECKPOINT_KEY) 371 .execute(&db) 372 .await 373 .unwrap(); 374 assert_eq!(res.rows_affected(), 1); 375 }; 376 377 // Default transient 378 test.sequences(&[ 379 EbicsState::haa, 380 EbicsState::receipt_ok, 381 EbicsState::btd_no_data, 382 ]); 383 nexus_cmd(&cfg, "ebics-fetch --transient").await.unwrap(); 384 385 // Pinned transient 386 test.sequences(&[ 387 EbicsState::haa, 388 EbicsState::receipt_ok, 389 EbicsState::btd_no_data_pinned, 390 ]); 391 nexus_cmd(&cfg, "ebics-fetch --transient --pinned-start 2024-06-05") 392 .await 393 .unwrap(); 394 395 // Init checkpoint 396 test.sequences(&[ 397 EbicsState::hkd, 398 EbicsState::receipt_ok, 399 EbicsState::btd_no_data, 400 ]); 401 nexus_cmd(&cfg, "ebics-fetch --transient --checkpoint") 402 .await 403 .unwrap(); 404 405 // Default checkpoint 406 test.sequences(&[ 407 EbicsState::hkd, 408 EbicsState::receipt_ok, 409 EbicsState::btd_no_data_now, 410 ]); 411 nexus_cmd(&cfg, "ebics-fetch --transient --checkpoint") 412 .await 413 .unwrap(); 414 415 // Pinned checkpoint 416 test.sequences(&[ 417 EbicsState::hkd, 418 EbicsState::receipt_ok, 419 EbicsState::btd_no_data_pinned, 420 ]); 421 nexus_cmd( 422 &cfg, 423 "ebics-fetch --transient --checkpoint --pinned-start 2024-06-05", 424 ) 425 .await 426 .unwrap(); 427 428 // Reset checkpoint 429 reset_checkpoint().await; 430 test.sequences(&[ 431 EbicsState::hkd, 432 EbicsState::receipt_ok, 433 EbicsState::btd_no_data, 434 ]); 435 nexus_cmd(&cfg, "ebics-fetch --transient --checkpoint") 436 .await 437 .unwrap(); 438 439 // Reset pinned checkpoint 440 reset_checkpoint().await; 441 test.sequences(&[ 442 EbicsState::hkd, 443 EbicsState::receipt_ok, 444 EbicsState::btd_no_data_pinned, 445 ]); 446 nexus_cmd( 447 &cfg, 448 "ebics-fetch --transient --checkpoint --pinned-start 2024-06-05", 449 ) 450 .await 451 .unwrap(); 452 } 453 454 #[db_test] 455 async fn close_pending_transaction(db: PgPool) { 456 let (test, cfg) = test_setup(&db).await; 457 458 // Failure before first segment 459 test.sequences(&[ 460 // Failure to perform download 461 EbicsState::failure, 462 // Then continue 463 EbicsState::haa, 464 EbicsState::receipt_ok, 465 EbicsState::btd_no_data, 466 ]); 467 nexus_cmd(&cfg, "ebics-fetch --transient") 468 .await 469 .unwrap_err(); 470 nexus_cmd(&cfg, "ebics-fetch --transient").await.unwrap(); 471 472 // Compliant server 473 test.sequences(&[ 474 EbicsState::haa, 475 EbicsState::receipt_ok, 476 // Failure to perform download 477 EbicsState::init_tx, 478 EbicsState::failure, 479 // Retry fail once 480 EbicsState::failure, 481 // Retry fail twice 482 EbicsState::failure, 483 // Retry succeed 484 EbicsState::bad_request, 485 // Then continue 486 EbicsState::haa, 487 EbicsState::receipt_ok, 488 EbicsState::btd_no_data, 489 ]); 490 nexus_cmd(&cfg, "ebics-fetch --transient") 491 .await 492 .unwrap_err(); 493 nexus_cmd(&cfg, "ebics-fetch --transient") 494 .await 495 .unwrap_err(); 496 nexus_cmd(&cfg, "ebics-fetch --transient") 497 .await 498 .unwrap_err(); 499 nexus_cmd(&cfg, "ebics-fetch --transient").await.unwrap(); 500 } 501 }