libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit f1b08eed8215d1f866ef35fe86f728a2aabd5e3c
parent 28b9f91c8566ac7e414f826fd82f03195eadb2cd
Author: Antoine A <>
Date:   Fri, 24 Apr 2026 10:38:00 +0200

nexus: add bench

Diffstat:
MCargo.lock | 3---
Msrc/api.rs | 43++++++++++++-------------------------------
Asrc/bench.rs | 285+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/config.rs | 6+++---
Msrc/db.rs | 96++++---------------------------------------------------------------------------
Msrc/db/initiated.rs | 13+++++++------
Msrc/db/payment.rs | 46++++++++++++++++++++++------------------------
Msrc/lib.rs | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 593 insertions(+), 159 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3110,9 +3110,6 @@ dependencies = [ [[package]] name = "taler-build" version = "1.5.0" -dependencies = [ - "const_format", -] [[package]] name = "taler-common" diff --git a/src/api.rs b/src/api.rs @@ -309,11 +309,8 @@ impl PreparedTransfer for NexusApi { } #[cfg(test)] -mod test { - use std::{ - str::FromStr as _, - sync::{Arc, LazyLock}, - }; +pub mod test { + use std::sync::Arc; use sqlx::PgPool; use taler_api::{api::TalerRouter as _, auth::AuthMethod, subject::OutgoingSubject}; @@ -321,10 +318,6 @@ mod test { api_revenue::RevenueConfig, api_transfer::PreparedTransferConfig, api_wire::{OutgoingHistory, TransferState, WireConfig}, - types::{ - amount::Currency, - payto::{PaytoURI, payto}, - }, }; use taler_test_utils::{ Router, @@ -339,25 +332,13 @@ mod test { use crate::{ CONFIG_SOURCE, api::NexusApi, - db::{ - payment::register_out_tx, - test::{check_in, gen_out_pay}, - }, + db::{payment::register_out_tx, test::check_in}, + test::{ACCOUNT, CURR, gen_out_pay}, }; - static ACCOUNT: LazyLock<PaytoURI> = - LazyLock::new(|| payto("payto://iban/CH4189144589712575493?receiver-name=Test")); - - async fn setup() -> (Router, PgPool) { + pub async fn api_setup() -> (Router, PgPool) { let (_, pool) = db_test_setup(CONFIG_SOURCE).await; - let api = Arc::new( - NexusApi::start( - pool.clone(), - ACCOUNT.clone(), - Currency::from_str("TEST").unwrap(), - ) - .await, - ); + let api = Arc::new(NexusApi::start(pool.clone(), ACCOUNT.clone(), CURR).await); let server = Router::new() .wire_gateway(api.clone(), AuthMethod::None) .prepared_transfer(api.clone()) @@ -369,7 +350,7 @@ mod test { #[tokio::test] async fn config() { - let (server, _) = setup().await; + let (server, _) = api_setup().await; server .get("/taler-wire-gateway/config") .await @@ -386,7 +367,7 @@ mod test { #[tokio::test] async fn transfer() { - let (server, _) = setup().await; + let (server, _) = api_setup().await; transfer_routine(&server, TransferState::pending, &ACCOUNT).await; // TODO /*db.initiated.batchSubmissionSuccess(1, Instant.now(), "ORDER1") @@ -402,7 +383,7 @@ mod test { #[tokio::test] async fn outgoing_history() { - let (server, pool) = setup().await; + let (server, pool) = api_setup().await; routine_pagination::<OutgoingHistory>( &server, "/taler-wire-gateway/history/outgoing", @@ -421,19 +402,19 @@ mod test { #[tokio::test] async fn admin_add_incoming() { - let (server, _) = setup().await; + let (server, _) = api_setup().await; admin_add_incoming_routine(&server, &ACCOUNT, true).await; } #[tokio::test] async fn revenue() { - let (server, _) = setup().await; + let (server, _) = api_setup().await; revenue_routine(&server, &ACCOUNT, true).await; } #[tokio::test] async fn registration() { - let (server, pool) = setup().await; + let (server, pool) = api_setup().await; registration_routine(&server, &ACCOUNT, || check_in(&pool)).await; } } diff --git a/src/bench.rs b/src/bench.rs @@ -0,0 +1,285 @@ +/* +* This file is part of LibEuFin. +* Copyright (C) 2026 Taler Systems S.A. + +* LibEuFin is free software; you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation; either version 3, or +* (at your option) any later version. + +* LibEuFin is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General +* Public License for more details. + +* You should have received a copy of the GNU Affero General Public +* License along with LibEuFin; see the file COPYING. If not, see +* <http://www.gnu.org/licenses/> +*/ + +#[cfg(test)] +mod test { + use std::fmt::Write as _; + + use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _}; + use compact_str::{CompactString, format_compact}; + use jiff::Timestamp; + use serde_json::json; + use taler_api::{crypto::eddsa_sign, subject::subject_fmt_qr_bill}; + use taler_common::{ + api_common::{EddsaPublicKey, HashCode, ShortHashCode}, + bench::{Bench, h32, h64}, + error_code::ErrorCode, + }; + use taler_test_utils::server::TestServer as _; + + use crate::{ + api::test::api_setup, + test::{ + ACCOUNT, malformed_completeted_in, malformed_in, malformed_incomplete_in, + prepared_completeted_in, prepared_in, prepared_incomplete_in, register_incomplete_out, + register_out, talerable_completeted_in, talerable_in, talerable_incomplete_in, + }, + }; + + #[tokio::test] + pub async fn bench_db() { + let (server, db) = api_setup().await; + let amount = 100; + let iter = 100; + let amount = amount.max(10); + let accounts_pubs: Vec<_> = (0..amount * 2) + .map(|_| { + let key_pair = Ed25519KeyPair::generate().unwrap(); + let pub_key = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap(); + (key_pair, pub_key) + }) + .collect(); + let mut b = Bench::new(&db, iter, amount); + b.table("incoming_transactions(amount, subject, execution_time, debit_payto, uetr, tx_id, acct_svcr_ref)", |f, i| { + let subject = if i % 4 == 0 { CompactString::const_new("\\N")} else {format_compact!("subject {i}")}; + let debtor = ACCOUNT.as_ref().as_str() ; + + if i % 3 == 0 { + writeln!(f, "(20,0)\t{subject}\t0\t{debtor}\t{}\t\\N\t\\N", uuid::Uuid::new_v4())?; + writeln!(f, "(21,0)\t{subject}\t0\t{debtor}\t\\N\tTX_ID{}\t\\N", i*2)?; + writeln!(f, "(22,0)\t{subject}\t0\t{debtor}\t\\N\t\\N\tREF{}", i*2) + } else if i%3 == 1 { + writeln!(f, "(30,0)\t{subject}\t0\t{debtor}\t{}\tTX_ID{}\t\\N", uuid::Uuid::new_v4(), i*2)?; + writeln!(f, "(31,0)\t{subject}\t0\t{debtor}\t\\N\tTX_ID{}\tREF{}", i*2+1, i*2)?; + writeln!(f, "(32,0)\t{subject}\t0\t{debtor}\t{}\t\\N\tREF{}", uuid::Uuid::new_v4(), i*2+1) + } else { + writeln!(f, "(40,0)\t{subject}\t0\t{debtor}\t{}\tTX_ID{}\tREF{}", uuid::Uuid::new_v4(), i*2, i*2)?; + writeln!(f, "(40,0)\t{subject}\t0\t{debtor}\t{}\tTX_ID{}\tREF{}", uuid::Uuid::new_v4(), i*2+1, i*2+1) + } + }).await; + b.table("outgoing_transactions(amount, subject, execution_time, credit_payto, end_to_end_id, acct_svcr_ref)", |f, i| { + let subject = if i % 4 == 0 { CompactString::const_new("\\N")} else {format_compact!("subject {i}")}; + let creditor =ACCOUNT.as_ref().as_str(); + + if i % 2 == 0 { + writeln!(f, "(40,0)\t{subject}\t0\t{creditor}\t\\N\tREF{}", i*2)?; + writeln!(f, "(41,0)\t{subject}\t0\t{creditor}\tE2E_ID{}\t\\N", i*2) + } else { + writeln!(f, "(40,0)\t{subject}\t0\t{creditor}\tE2E_ID{}\tREF{}", i*2, i*2)?; + writeln!(f, "(41,0)\t{subject}\t0\t{creditor}\tE2E_ID{}\tREF{}", i*2+1, i*2+1) + } + }).await; + b.table("initiated_outgoing_transactions(amount, subject, initiation_time, credit_payto, outgoing_transaction_id, end_to_end_id)", |f, i| { + writeln!(f, "(42,0)\tsubject\t0\t{}\t{}\tE2E_ID{i}", &*ACCOUNT , i*2) + }).await; + b.table("prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, reference_number, registered_at, incoming_transaction_id)", |f, i| { + let ty = if i%2==0 {"reserve"} else {"kyc"}; + let recurrent = if i%3 == 0 {"true" } else {"false"}; + let incoming_transaction_id = if i % 5 == 0 { CompactString::const_new("\\N") }else {format_compact!("{}", i*2)}; + + let reference_number = subject_fmt_qr_bill(accounts_pubs[i].1.as_ref()); + let key = hex::encode( accounts_pubs[i].1.as_ref()); + let sig = h64(); + writeln!(f, "{ty}\t\\\\x{key}\t\\\\x{key}\t\\\\x{sig}\t{recurrent}\t{reference_number}\t0\t{incoming_transaction_id}") + }).await; + b.table( + "pending_recurrent_incoming_transactions(incoming_transaction_id, authorization_pub)", + |f, i| { + let key = hex::encode(accounts_pubs[i].1.as_ref()); + writeln!(f, "{}\t\\\\x{key}", i * 2) + }, + ) + .await; + b.table( + "bounced_transactions(incoming_transaction_id, initiated_outgoing_transaction_id)", + |f, i| { + if i % 10 == 0 { + writeln!(f, "{}\t{}", i / 2, i / 2) + } else { + Ok(()) + } + }, + ) + .await; + b.table( + "talerable_incoming_transactions(type, metadata, incoming_transaction_id)", + |f, i| { + let hex = h32(); + let ty = if i % 2 == 0 { "reserve" } else { "kyc" }; + writeln!(f, "{ty}\t\\\\x{hex}\t{}", i * 2) + }, + ) + .await; + b.table( + "talerable_outgoing_transactions(wtid, exchange_base_url, outgoing_transaction_id)", + |f, i| { + let hex = h32(); + writeln!(f, "\\\\x{hex}\thttp://exchange.example.com/\t{}", i * 2 - 1) + }, + ) + .await; + b.table("transfer_operations(initiated_outgoing_transaction_id, request_uid, wtid, exchange_base_url)", |f, i| { + let h32 = h32(); + let h64 = h64(); + writeln!(f, "{i}\t\\\\x{h64}\t\\\\x{h32}\turl") + }).await; + + // Warm HTTP client + server.get("/taler-revenue/config").await.assert_ok(); + + // Register + b.measure("register_in", |_| malformed_in(&db)).await; + b.measure("register_incomplete_in", |_| malformed_incomplete_in(&db)) + .await; + b.measure("register_completed_in", |_| malformed_completeted_in(&db)) + .await; + b.measure("register_talerable_in", |_| talerable_in(&db)) + .await; + b.measure("register_talerable_incomplete_in", |_| { + talerable_incomplete_in(&db) + }) + .await; + b.measure("register_talerable_completed_in", |_| { + talerable_completeted_in(&db) + }) + .await; + b.measure("register_prepared_in", |_| prepared_in(&db)) + .await; + b.measure("register_prepared_incomplete_in", |_| { + prepared_incomplete_in(&db) + }) + .await; + b.measure("register_prepared_completed_in", |_| { + prepared_completeted_in(&db) + }) + .await; + b.measure("register_out", |_| register_out(&db)).await; + b.measure("register_incomplete_out", |_| register_incomplete_out(&db)) + .await; + + // Revenue api + b.measure("transaction_revenue", async |_| { + server.get("/taler-revenue/history").await.assert_ok() + }) + .await; + + // Wire gateway + b.measure("wg_transfer", async |_| { + server + .post("/taler-wire-gateway/transfer") + .json(&json!({ + "request_uid": HashCode::rand(), + "amount": "KUDOS:0.0001", + "exchange_base_url": "http://exchange.example.com/", + "wtid": ShortHashCode::rand(), + "credit_account": &*ACCOUNT + })) + .await + .assert_ok() + }) + .await; + b.measure("wg_transfer_get", async |i| { + server + .get(&format!("/taler-wire-gateway/transfers/{}", i + 1)) + .await + .assert_ok() + }) + .await; + b.measure("wg_transfer_page", async |_| { + server + .get("/taler-wire-gateway/transfers") + .await + .assert_ok() + }) + .await; + b.measure("wg_transfer_page_filter", async |_| { + server + .get("/taler-wire-gateway/transfers?status=success") + .await + .assert_no_content() + }) + .await; + b.measure("wg_add", async |_| { + server + .post("/taler-wire-gateway/admin/add-incoming") + .json(&json!({ + "amount": "KUDOS:0.0001", + "reserve_pub": EddsaPublicKey::rand(), + "debit_account": &*ACCOUNT + })) + .await + .assert_ok() + }) + .await; + b.measure("wg_incoming", async |_| { + server + .get("/taler-wire-gateway/history/incoming") + .await + .assert_ok() + }) + .await; + b.measure("wg_outgoing", async |_| { + server + .get("/taler-wire-gateway/history/outgoing") + .await + .assert_ok() + }) + .await; + + // Wire transfer + b.measure("wt_register", async |i| { + let (pair, key) = &accounts_pubs[i]; + + server + .post("/taler-prepared-transfer/registration") + .json(&json!({ + "credit_amount": "KUDOS:55", + "type": "reserve", + "alg": "EdDSA", + "account_pub": key, + "authorization_pub": key, + "authorization_sig": eddsa_sign(&pair, key.as_ref()), + "recurrent":false + })) + .await + .assert_ok() + }) + .await; + b.measure("wt_unregister", async |i| { + let (pair, key) = &accounts_pubs[i]; + let now = Timestamp::now().to_string(); + let req = json!({ + "timestamp": &now, + "authorization_pub": key, + "authorization_sig": eddsa_sign(&pair, now.as_ref()), + }); + server + .post("/taler-prepared-transfer/unregistration") + .json(&req) + .await + .assert_no_content(); + server + .post("/taler-prepared-transfer/unregistration") + .json(&req) + .await + .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND); + }) + .await; + } +} diff --git a/src/config.rs b/src/config.rs @@ -17,7 +17,7 @@ * <http://www.gnu.org/licenses/> */ -use std::{cell::OnceCell, str::FromStr, time::Duration}; +use std::{cell::OnceCell, time::Duration}; use jiff::{ Timestamp, @@ -92,7 +92,7 @@ pub struct NexusIngestCfg { } impl NexusIngestCfg { - pub fn simple(account_type: AccountType, currency: &Currency) -> Self { + pub const fn simple(account_type: AccountType, currency: &Currency) -> Self { Self { account_type, ignore_txs_before: Timestamp::UNIX_EPOCH, @@ -100,7 +100,7 @@ impl NexusIngestCfg { restriction_payto_regex: None, bounce_deduce_fee: false, bounce_fee: Amount::zero(currency), - currency: Currency::from_str("KUDOS").unwrap(), + currency: Currency::KUDOS, } } } diff --git a/src/db.rs b/src/db.rs @@ -93,108 +93,20 @@ pub async fn ebics_first(db: &PgPool) -> sqlx::Result<Option<CompactString>> { #[cfg(test)] pub mod test { - use std::{str::FromStr, sync::LazyLock}; - - use compact_str::CompactString; - use jiff::Timestamp; use sqlx::{PgPool, Postgres, Row, pool::PoolConnection, postgres::PgRow}; use taler_api::db::TypeHelper; - use taler_common::{ - db::IncomingType, - types::{ - amount::{Amount, Currency}, - payto::IbanPayto, - }, - }; + use taler_common::db::IncomingType; use taler_test_utils::routine::Status; use crate::{ CONFIG_SOURCE, - db::{ - ebics_first, ebics_register, ebics_remove, - initiated::{PaymentInitiationResult, initiate}, - }, - model::{InId, InTx, Initiated, OutId, OutTx}, - rand_ebics_id, + db::{ebics_first, ebics_register, ebics_remove}, }; - pub static CURRENCY: LazyLock<Currency> = LazyLock::new(|| "KUDOS".parse().unwrap()); - - pub async fn setup() -> (PoolConnection<Postgres>, PgPool) { + pub async fn db_setup() -> (PoolConnection<Postgres>, PgPool) { taler_test_utils::db::db_test_setup(CONFIG_SOURCE).await } - /** Generates an outgoing payment, given its subject */ - pub fn gen_out_pay(subject: impl Into<Box<str>>) -> OutTx { - OutTx { - id: OutId { - msg_id: None, - e2e_id: Some(rand_ebics_id()), - sref: None, - }, - amount: Amount::new(&CURRENCY, 44, 0), - debit_fee: Amount::zero(&CURRENCY), - creditor: Some( - IbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Test") - .unwrap() - .as_payto(), - ), - subject: Some(subject.into()), - execution_time: Timestamp::now(), - } - } - - /** Generates a payment initiation, given its subject and end-to-end ID */ - pub fn gen_init_pay( - end_to_end_id: impl Into<CompactString>, - subject: impl Into<String>, - ) -> Initiated { - Initiated { - id: 0, - amount: Amount::new(&CURRENCY, 44, 0), - creditor: IbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Test") - .unwrap() - .as_payto(), - subject: subject.into(), - initiation_time: Timestamp::now(), - e2e_id: end_to_end_id.into(), - } - } - - /** Generates an incoming payment, given its subject */ - pub fn gen_in_pay(subject: impl Into<Box<str>>) -> InTx { - InTx { - id: InId::new(None, Some(rand_ebics_id()), None), - amount: Amount::new(&CURRENCY, 44, 0), - credit_fee: Amount::zero(&CURRENCY), - debtor: Some( - IbanPayto::from_str("payto://iban/DE84500105177118117964?receiver-name=John+Smith") - .unwrap() - .as_payto(), - ), - subject: Some(subject.into()), - execution_time: Timestamp::now(), - } - } - - pub async fn gen_initiate( - db: &PgPool, - end_to_end_id: impl Into<CompactString>, - subject: impl Into<String>, - ) -> PaymentInitiationResult { - let init = gen_init_pay(end_to_end_id, subject); - initiate( - &db, - &init.amount, - &init.subject, - &init.creditor, - &init.initiation_time, - &init.e2e_id, - ) - .await - .unwrap() - } - pub async fn check_count(db: &PgPool, nb_tx: usize, nb_bounce: usize) { sqlx::query( " @@ -298,7 +210,7 @@ pub mod test { #[tokio::test] pub async fn ebics_pending() { - let (_, db) = setup().await; + let (_, db) = db_setup().await; let ids = ["first", "second", "third"]; for id in ids { diff --git a/src/db/initiated.rs b/src/db/initiated.rs @@ -459,16 +459,17 @@ mod test { batch_sub_success, initiated_submittable, order_failure, order_step, order_success, tx_status_update, }, - test::{CURRENCY, check_count, gen_in_pay, gen_initiate, gen_out_pay, setup}, + test::{check_count, db_setup}, }, model::{SubmissionState, Tx}, rand_ebics_id, + test::{CURR, gen_in_pay, gen_initiate, gen_out_pay}, worker::{register_outgoing, register_tx}, }; #[tokio::test] pub async fn initiated_skip() { - let (_, db) = setup().await; + let (_, db) = db_setup().await; let cfg = Config::from_file(CONFIG_SOURCE, Some("libeufin-nexus/conf/skip.conf")).unwrap(); let cfg = NexusCfg::parse(cfg).unwrap(); let cfg = cfg.ingest().unwrap(); @@ -521,7 +522,7 @@ mod test { pub async fn initiated_status() { use SubmissionState::*; - let (_, db) = setup().await; + let (_, db) = db_setup().await; let check_parts = async |batch_id: u64, batch_status: SubmissionState, @@ -636,7 +637,7 @@ mod test { assert!(r.try_get_flag(0).unwrap()); Ok(()) }).fetch_one(db).await.unwrap(); - let submitibale = initiated_submittable(db, &CURRENCY).await.unwrap(); + let submitibale = initiated_submittable(db, &CURR).await.unwrap(); lambda( submitibale .iter() @@ -835,7 +836,7 @@ mod test { #[tokio::test] pub async fn initiated_submittables() { - let (_, db) = setup().await; + let (_, db) = db_setup().await; let now = Timestamp::now(); for i in 0..6 { assert!(matches!( @@ -850,7 +851,7 @@ mod test { let check_ids = async |ids: &[&str]| { assert_eq!( ids, - initiated_submittable(&db, &CURRENCY) + initiated_submittable(&db, &CURR) .await .unwrap() .iter() diff --git a/src/db/payment.rs b/src/db/payment.rs @@ -321,20 +321,18 @@ mod test { InResult, IncomingBounceRegistrationResult, OutgoingRegistrationResult, register_in_malformed, }, - test::{ - CURRENCY, check_in_count, check_in_state, check_out_count, gen_in_pay, - gen_initiate, gen_out_pay, setup, - }, + test::{check_in_count, check_in_state, check_out_count, db_setup}, transfer::{RegistrationResult, transfer_register}, }, model::{InId, InTx, OutBatch, OutId, OutTx}, rand_ebics_id, + test::{CURR, gen_in_pay, gen_initiate, gen_out_pay}, worker::{register_incoming, register_outgoing, register_outgoing_batch}, }; #[tokio::test] async fn out_tx() { - let (_, db) = setup().await; + let (_, db) = db_setup().await; // Register initiated transactions for subject in [ "initiated by nexus".to_owned(), @@ -446,7 +444,7 @@ mod test { #[tokio::test] async fn out_batch() { - let (_, db) = setup().await; + let (_, db) = db_setup().await; // Init batch let wtid = ShortHashCode::rand(); for subject in [ @@ -467,7 +465,7 @@ mod test { // Register batch register_outgoing_batch( &db, - &CURRENCY, + &CURR, &OutBatch { msg_id: "BATCH".into(), execution_time: Timestamp::now(), @@ -494,7 +492,7 @@ mod test { .unwrap(); register_outgoing_batch( &db, - &CURRENCY, + &CURR, &OutBatch { msg_id: "BATCH_MANUAL".into(), execution_time: Timestamp::now(), @@ -513,7 +511,7 @@ mod test { .unwrap(); register_outgoing_batch( &db, - &CURRENCY, + &CURR, &OutBatch { msg_id: "BATCH_MANUAL".into(), execution_time: Timestamp::now(), @@ -526,7 +524,7 @@ mod test { #[tokio::test] async fn in_bounce() { - let (_, db) = setup().await; + let (_, db) = db_setup().await; // Creating and bouncing one incoming transaction let payment = gen_in_pay("incoming and bounce"); @@ -593,8 +591,8 @@ mod test { ", ) .try_map(|r: PgRow| { - assert_eq!(r.try_get_amount("in_amount", &CURRENCY)?, payment.amount); - assert_eq!(r.try_get_amount("bounce_amount", &CURRENCY)?, bounce_amount); + assert_eq!(r.try_get_amount("in_amount", &CURR)?, payment.amount); + assert_eq!(r.try_get_amount("bounce_amount", &CURR)?, bounce_amount); Ok(()) }) .fetch_one(&db) @@ -604,9 +602,9 @@ mod test { #[tokio::test] async fn in_simple() { - let (_, db) = setup().await; + let (_, db) = db_setup().await; - let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURRENCY); + let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURR); // Register let incoming = gen_in_pay("test".to_owned()); @@ -650,9 +648,9 @@ mod test { #[tokio::test] async fn in_talerable() { - let (_, db) = setup().await; + let (_, db) = db_setup().await; - let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURRENCY); + let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURR); let key = EddsaPublicKey::rand(); let subject = format!("test with {key} reserve pub"); @@ -713,8 +711,8 @@ mod test { #[tokio::test] async fn in_mapping() { - let (_, db) = setup().await; - let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURRENCY); + let (_, db) = db_setup().await; + let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURR); let first = EddsaPublicKey::rand(); let auth_pub = EddsaPublicKey::rand(); let auth_sig = EddsaSignature::rand(); @@ -838,8 +836,8 @@ mod test { #[tokio::test] async fn in_reference() { - let (_, db) = setup().await; - let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURRENCY); + let (_, db) = db_setup().await; + let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURR); let first = EddsaPublicKey::rand(); let auth_pub = EddsaPublicKey::rand(); let auth_sig = EddsaSignature::rand(); @@ -962,8 +960,8 @@ mod test { #[tokio::test] async fn in_recover_info() { - let (_, db) = setup().await; - let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURRENCY); + let (_, db) = db_setup().await; + let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURR); async fn check_content(db: &PgPool, p: &InTx) { sqlx::query( @@ -1087,8 +1085,8 @@ mod test { #[tokio::test] pub async fn in_horror() { - let (_, db) = setup().await; - let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURRENCY); + let (_, db) = db_setup().await; + let cfg = NexusIngestCfg::simple(AccountType::Exchange, &CURR); // Check we do not bounce already registered talerable transaction let key = EddsaPublicKey::rand(); diff --git a/src/lib.rs b/src/lib.rs @@ -80,6 +80,7 @@ use crate::{ }; pub mod api; +pub mod bench; pub mod config; pub mod crypto; pub mod db; @@ -656,3 +657,262 @@ pub async fn run(cfg: Config, cmd: &Cmd, ebics_log: &EbicsLogger) -> anyhow::Res } Ok(()) } + +#[cfg(test)] +pub mod test { + use std::{str::FromStr as _, sync::LazyLock}; + + use compact_str::CompactString; + use jiff::Timestamp; + use sqlx::PgPool; + use taler_api::subject::{fmt_in_subject, subject_fmt_qr_bill}; + use taler_common::{ + api_common::{EddsaPublicKey, EddsaSignature}, + db::IncomingType, + types::{ + amount::{Amount, Currency}, + payto::{IbanPayto, PaytoURI, payto}, + }, + }; + + use crate::{ + config::{AccountType, NexusIngestCfg}, + db::{ + initiated::{PaymentInitiationResult, initiate}, + transfer::{RegistrationResult, transfer_register}, + }, + model::{InId, InTx, Initiated, OutId, OutTx}, + rand_ebics_id, + worker::{register_incoming, register_outgoing}, + }; + + pub const CURR: Currency = Currency::KUDOS; + pub static ACCOUNT: LazyLock<PaytoURI> = + LazyLock::new(|| payto("payto://iban/CH4189144589712575493?receiver-name=Test")); + + /** Generates an outgoing payment, given its subject */ + pub fn gen_out_pay(subject: impl Into<Box<str>>) -> OutTx { + OutTx { + id: OutId { + msg_id: None, + e2e_id: Some(rand_ebics_id()), + sref: None, + }, + amount: Amount::new(&CURR, 44, 0), + debit_fee: Amount::zero(&CURR), + creditor: Some( + IbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Test") + .unwrap() + .as_payto(), + ), + subject: Some(subject.into()), + execution_time: Timestamp::now(), + } + } + + /** Generates a payment initiation, given its subject and end-to-end ID */ + pub fn gen_init_pay( + end_to_end_id: impl Into<CompactString>, + subject: impl Into<String>, + ) -> Initiated { + Initiated { + id: 0, + amount: Amount::new(&CURR, 44, 0), + creditor: IbanPayto::from_str("payto://iban/CH4189144589712575493?receiver-name=Test") + .unwrap() + .as_payto(), + subject: subject.into(), + initiation_time: Timestamp::now(), + e2e_id: end_to_end_id.into(), + } + } + + /** Generates an incoming payment, given its subject */ + pub fn gen_in_pay(subject: impl Into<Box<str>>) -> InTx { + InTx { + id: InId::new(None, Some(rand_ebics_id()), None), + amount: Amount::new(&CURR, 44, 0), + credit_fee: Amount::zero(&CURR), + debtor: Some( + IbanPayto::from_str("payto://iban/DE84500105177118117964?receiver-name=John+Smith") + .unwrap() + .as_payto(), + ), + subject: Some(subject.into()), + execution_time: Timestamp::now(), + } + } + + pub async fn gen_initiate( + db: &PgPool, + end_to_end_id: impl Into<CompactString>, + subject: impl Into<String>, + ) -> PaymentInitiationResult { + let init = gen_init_pay(end_to_end_id, subject); + initiate( + &db, + &init.amount, + &init.subject, + &init.creditor, + &init.initiation_time, + &init.e2e_id, + ) + .await + .unwrap() + } + + const CFG: NexusIngestCfg = NexusIngestCfg::simple(AccountType::Exchange, &CURR); + + async fn prepare(db: &PgPool) -> String { + let key = EddsaPublicKey::rand(); + let sig = EddsaSignature::rand(); + let reference_number = subject_fmt_qr_bill(key.as_ref()); + assert_eq!( + RegistrationResult::Success, + transfer_register( + db, + IncomingType::reserve, + &key, + &key, + &sig, + false, + &reference_number, + &Timestamp::now() + ) + .await + .unwrap() + ); + return reference_number; + } + + /// Register a talerable reserve prepared incoming transaction + pub async fn prepared_in(db: &PgPool) { + let ref_nb = prepare(db).await; + register_incoming(db, &CFG, &gen_in_pay(ref_nb)) + .await + .unwrap(); + } + + /// Register an incomplete talerable reserve prepared incoming transaction + pub async fn prepared_incomplete_in(db: &PgPool) { + let ref_nb = prepare(db).await; + let incomplete = InTx { + subject: None, + debtor: None, + ..gen_in_pay(ref_nb) + }; + register_incoming(db, &CFG, &incomplete).await.unwrap(); + } + + /// Register a completed talerable reserve prepared incoming transaction + pub async fn prepared_completeted_in(db: &PgPool) { + let ref_nb = prepare(db).await; + let original = gen_in_pay(ref_nb); + let incomplete = InTx { + subject: None, + debtor: None, + ..original.clone() + }; + register_incoming(db, &CFG, &incomplete).await.unwrap(); + register_incoming(db, &CFG, &original).await.unwrap(); + } + + /// Register a talerable reserve incoming transaction + pub async fn talerable_in(db: &PgPool) { + register_incoming( + db, + &CFG, + &gen_in_pay(fmt_in_subject( + IncomingType::reserve, + &EddsaPublicKey::rand(), + )), + ) + .await + .unwrap(); + } + + /// Register a talerable kyc incoming transaction + pub async fn talerable_kyc_in(db: &PgPool) { + register_incoming( + db, + &CFG, + &gen_in_pay(fmt_in_subject(IncomingType::kyc, &EddsaPublicKey::rand())), + ) + .await + .unwrap(); + } + + /// Register an incomplete talerable reserve incoming transaction + pub async fn talerable_incomplete_in(db: &PgPool) { + let incomplete = InTx { + subject: None, + debtor: None, + ..gen_in_pay(fmt_in_subject( + IncomingType::reserve, + &EddsaPublicKey::rand(), + )) + }; + register_incoming(db, &CFG, &incomplete).await.unwrap(); + } + + /// Register a completed talerable reserve incoming transaction + pub async fn talerable_completeted_in(db: &PgPool) { + let original = gen_in_pay(fmt_in_subject( + IncomingType::reserve, + &EddsaPublicKey::rand(), + )); + let incomplete = InTx { + subject: None, + debtor: None, + ..original.clone() + }; + register_incoming(db, &CFG, &incomplete).await.unwrap(); + register_incoming(db, &CFG, &original).await.unwrap(); + } + + /// Register incoming malformed transaction + pub async fn malformed_in(db: &PgPool) { + register_incoming(db, &CFG, &gen_in_pay("ignored")) + .await + .unwrap(); + } + + /// Register incoming incomplete malformed incoming transaction + pub async fn malformed_incomplete_in(db: &PgPool) { + let incomplete = InTx { + subject: None, + debtor: None, + ..gen_in_pay("ignored") + }; + register_incoming(db, &CFG, &incomplete).await.unwrap(); + } + + /// Register incoming completed malformed transaction + pub async fn malformed_completeted_in(db: &PgPool) { + let original = gen_in_pay("ignored"); + let incomplete = InTx { + subject: None, + debtor: None, + ..original.clone() + }; + register_incoming(db, &CFG, &incomplete).await.unwrap(); + register_incoming(db, &CFG, &original).await.unwrap(); + } + + /** Register an outgoing transaction */ + pub async fn register_out(db: &PgPool) { + register_outgoing(db, &gen_out_pay("ignored")) + .await + .unwrap(); + } + + /** Register an incomplete outgoing transaction */ + pub async fn register_incomplete_out(db: &PgPool) { + let incomplete = OutTx { + subject: None, + creditor: None, + ..gen_out_pay("ignored") + }; + register_outgoing(db, &incomplete).await.unwrap(); + } +}