libeufin

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

bench.rs (20838B)


      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::Write as _;
     21 
     22 use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _};
     23 use axum::http::header::AUTHORIZATION;
     24 use compact_str::{CompactString, format_compact};
     25 use jiff::{Timestamp, Zoned, tz::TimeZone};
     26 use sqlx::postgres::PgConnectOptions;
     27 use taler_common::{
     28     api::{
     29         EddsaPublicKey, EddsaSignature, HashCode, ShortHashCode,
     30         prepared::{PublicKeyAlg, RegistrationRequest, TransferType, Unregistration},
     31         wire::TransferResponse,
     32     },
     33     bench::{Bench, h16, h32, h64},
     34     encoding::hex,
     35     error_code::ErrorCode,
     36     types::{
     37         amount::{Amount, Currency},
     38         time::TalerTimestamp,
     39     },
     40 };
     41 use taler_macros::db_test;
     42 use taler_test_utils::{json, server::TestServer};
     43 use uuid::Uuid;
     44 
     45 use crate::{
     46     api::{
     47         account::RegisterAccountResponse,
     48         cashout::CashoutResponse,
     49         conversion::ConversionRateClassResponse,
     50         tan::ChallengeResponse,
     51         test::{bank_setup, tan_code},
     52         token::TokenSuccessResponse,
     53         tx::TransactionCreateResponse,
     54         withdrawal::BankAccountCreateWithdrawalResponse,
     55     },
     56     db::gc::collect,
     57     pw::PwCrypto,
     58 };
     59 
     60 #[db_test(raw)]
     61 pub async fn bench_db(db: PgConnectOptions) {
     62     let mut ctx = bank_setup(db).await;
     63     let mut b = Bench::new(&ctx.state.db);
     64     // Skip 4 accounts created by bank_setup
     65     let skip = 4;
     66     // Customer account will be used in tests so we want to generate more data for him
     67     let customer = 3;
     68     let exchange = 2;
     69     // In general half of the data is for generated account and half is for customer
     70     let mid = b.amount / 2;
     71 
     72     let password = PwCrypto::Bcrypt { cost: 4 }.hashpw("password");
     73     let accounts_pubs: Vec<_> = (0..b.amount * 2)
     74         .map(|_| {
     75             let key_pair = Ed25519KeyPair::generate().unwrap();
     76             let pub_key = EddsaPublicKey::try_from(key_pair.public_key().as_ref()).unwrap();
     77             (key_pair, pub_key)
     78         })
     79         .collect();
     80 
     81     b.table(
     82         "customers(username, name, password_hash, cashout_payto)",
     83         |f, i| {
     84             writeln!(
     85                 f,
     86                 "account_{i}\t{password}\tMr n°{i}\t{}",
     87                 ctx.unknown_payto
     88             )
     89         },
     90     )
     91     .await;
     92     b.table("conversion_rate_classes(name)", |f, i| {
     93         writeln!(f, "Class n°{i}")
     94     })
     95     .await;
     96     b.table(
     97         "bank_accounts(internal_payto,owning_customer_id,is_public,conversion_rate_class_id)",
     98         |f, i| {
     99             let id = match i % 5 {
    100                 0 => "\\N",
    101                 1 | 2 => "1",
    102                 3 => "2",
    103                 _ => &(i % 10).to_string(),
    104             };
    105             writeln!(
    106                 f,
    107                 "payto://x-taler-bank/localhost/account_{i}\t{}\t{}\t{id}",
    108                 i + skip,
    109                 i % 3 == 0
    110             )
    111         },
    112     )
    113     .await;
    114     b.table(
    115         "bearer_tokens(content, creation_time, expiration_time, scope, is_refreshable, bank_customer, description, last_access)",
    116         |f, i| {
    117             let account =  if i > mid { customer} else {i+4};
    118             writeln!(
    119                 f,
    120                 "\\\\x{}\t0\t0\treadonly\tfalse\t{account}\t\\N\t0",
    121                h32(),
    122             )
    123         },
    124     )
    125     .await;
    126     b.table(
    127         "bank_account_transactions(creditor_payto, creditor_name, debtor_payto, debtor_name, subject, amount, transaction_date, direction, bank_account_id)",
    128         |f, i| {
    129                    let account =  if i > mid { customer} else {i+4};
    130             writeln!(
    131                 f,
    132                 "{}\tcreditor_name\t{}\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t{exchange}",
    133                 ctx.unknown_payto, ctx.unknown_payto
    134             )?;
    135             writeln!(
    136                 f,
    137                 "{}\tcreditor_name\t{}\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t{account}",
    138                 ctx.unknown_payto, ctx.unknown_payto
    139             )
    140         },
    141     )
    142     .await;
    143     b.table("bank_transaction_operations", |f, i| {
    144         writeln!(f, "\\\\x{}\t{i}", h32())
    145     })
    146     .await;
    147     b.table("tan_challenges(uuid, hbody, salt, op, code, creation_date, expiration_date, retry_counter, customer, tan_channel, tan_info)", |f, i| {
    148          let account =  if i > mid { customer} else {i+4};
    149                 writeln!(f, "{}\t\\\\x{}\t\\\\x{}\taccount_reconfig\tcode\t0\t0\t0\t{account}\tsms\tinfo",  Uuid::new_v4(),  h64(), h16())
    150     })
    151     .await;
    152     b.table("taler_withdrawal_operations(withdrawal_uuid, wallet_bank_account, reserve_pub, creation_date)", |f, i| {
    153          let account =  if i > mid { customer} else {i+4};
    154                 writeln!(f, "{}\t{account}\t\\\\x{}\t0",  Uuid::new_v4(),  h32())
    155     })
    156     .await;
    157     b.table("prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, registered_at, bank_transaction_id)", |f, i| {
    158         let ty = if i%2==0 {"reserve"} else {"kyc"};
    159         let recurrent = if i%3 == 0 {"true" } else {"false"};
    160         let incoming_transaction_id = if i % 5 == 0 { CompactString::const_new("\\N") }else {format_compact!("{}", i*2)};
    161 
    162         let key = hex::encode( accounts_pubs[i].1.as_ref());
    163         let sig =  h64();
    164         writeln!(f, "{ty}\t\\\\x{key}\t\\\\x{key}\t\\\\x{sig}\t{recurrent}\t0\t{incoming_transaction_id}")
    165     }).await;
    166     b.table(
    167         "pending_recurrent_incoming_transactions(bank_transaction_id, debtor_account_id, authorization_pub)",
    168         |f, i| {
    169             let key = hex::encode(accounts_pubs[i].1.as_ref());
    170             writeln!(f, "{}\t{i}\t\\\\x{key}", i * 2)
    171         },
    172     )
    173     .await;
    174     b.table("taler_exchange_outgoing(bank_transaction)", |f, i| {
    175         writeln!(f, "{}", i * 2 - 1)
    176     })
    177     .await;
    178     b.table("transfer_operations(wtid, request_uid, amount, exchange_base_url, exchange_outgoing_id, exchange_id, transfer_date, creditor_payto, status, status_msg)", |f, i| {
    179         if i % 2 == 0 {
    180             writeln!(f, "\\\\x{}\t\\\\x{}\t(42, 0)\thttps://url\t{i}\t{i}\t0\tpayto://x-taler-bank/localhost/10\tsuccess\t\\N", h32(), h64())
    181         } else {
    182             writeln!(f, "\\\\x{}\t\\\\x{}\t(42, 0)\thttps://url\t\\N\t{i}\t0\tpayto://x-taler-bank/localhost/10\tpermanent_failure\tfailure", h32(), h64())
    183         }
    184     })
    185     .await;
    186     b.table(
    187         "taler_exchange_incoming(type, metadata, bank_transaction)",
    188         |f, i| {
    189             let hex = h32();
    190             let ty = if i % 2 == 0 { "reserve" } else { "kyc" };
    191             writeln!(f, "{ty}\t\\\\x{hex}\t{}", i * 2)
    192         },
    193     )
    194     .await;
    195     b.table("bank_stats(timeframe, start_time)", |f, i| {
    196         writeln!(
    197             f,
    198             "day\t{}",
    199             Zoned::new(Timestamp::from_second(i as i64).unwrap(), TimeZone::UTC).datetime()
    200         )
    201     })
    202     .await;
    203     b.table("cashout_operations(request_uid,amount_debit,amount_credit,subject,creation_time,bank_account,local_transaction)", |f, i| {
    204            let account =  if i > mid { customer} else {i+4};
    205         writeln!(f, "\\\\x{}\t(0,0)\t(0,0)\tsubject\t0\t{account}\t{i}", h32())
    206     })
    207     .await;
    208 
    209     // Prepare customer accounts
    210     ctx.fill_cashout_info("customer").await;
    211     ctx.set_max_debt("customer", "1000000").await;
    212 
    213     // Accounts
    214     let mut names = Vec::new();
    215     b.measure("account_create", async |i| {
    216         let username = format!("account_bench_{i}");
    217         ctx.post("/accounts")
    218             .json(json!({
    219                 "username": username,
    220                 "password": format!("account_bench_{i}-password"),
    221                 "name": format!("Bench Account {i}")
    222             }))
    223             .await
    224             .assert_ok_json::<RegisterAccountResponse>();
    225         names.push(username);
    226     })
    227     .await;
    228     ctx.client
    229         .cache_tokens(&names.iter().map(|it| it.as_str()).collect::<Vec<_>>())
    230         .await;
    231     b.measure("account_reconfig", async |i| {
    232         ctx.patcha(format!("/accounts/account_bench_{i}"))
    233             .json(json!({
    234                 "name": format!("New Bench Account {i}")
    235             }))
    236             .await
    237             .assert_no_content();
    238     })
    239     .await;
    240     b.measure("account_reconfig_auth", async |i| {
    241         ctx.patcha(format!("/accounts/account_bench_{i}/auth"))
    242             .json(json!({
    243                 "old_password": format!("account_bench_{i}-password"),
    244                 "new_password": format!("account_bench_{i}-password"),
    245             }))
    246             .await
    247             .assert_no_content();
    248     })
    249     .await;
    250     b.measure("account_list", async |_| {
    251         ctx.get_admin("/accounts").await.assert_ok()
    252     })
    253     .await;
    254     b.measure("account_list_class", async |i| {
    255         ctx.get_admin(format!("/accounts?conversion_rate_class={}", i % 10))
    256             .await
    257             .assert_ok()
    258     })
    259     .await;
    260     b.measure("account_list_name", async |_| {
    261         ctx.get_admin("/accounts?name=Mr").await.assert_ok()
    262     })
    263     .await;
    264     b.measure("account_list_public", async |_| {
    265         ctx.get("/public-accounts").await.assert_ok()
    266     })
    267     .await;
    268     b.measure("account_get", async |i| {
    269         ctx.geta(format!("/accounts/account_bench_{i}"))
    270             .await
    271             .assert_ok()
    272     })
    273     .await;
    274 
    275     // Token
    276     let mut t = Vec::new();
    277     b.measure("token_create", async |_| {
    278         t.push(
    279             ctx.postpw("/accounts/customer/token")
    280                 .json(json!({
    281                     "scope": "readonly",
    282                     "refreshable": true
    283                 }))
    284                 .await
    285                 .assert_ok_json::<TokenSuccessResponse>()
    286                 .access_token,
    287         )
    288     })
    289     .await;
    290     b.measure("token_refresh", async |i| {
    291         ctx.post("/accounts/customer/token")
    292             .header(AUTHORIZATION, format!("Bearer {}", t[i]))
    293             .json(json!({
    294                 "scope": "readonly"
    295             }))
    296             .await
    297             .assert_ok()
    298     })
    299     .await;
    300     b.measure("token_list", async |_| {
    301         ctx.geta("/accounts/customer/tokens").await.assert_ok()
    302     })
    303     .await;
    304     b.measure("token_delete", async |i| {
    305         ctx.delete("/accounts/customer/token")
    306             .header(AUTHORIZATION, format!("Bearer {}", t[i]))
    307             .await
    308             .assert_no_content();
    309     })
    310     .await;
    311 
    312     // Conversion rate classes
    313     let mut c = Vec::new();
    314     b.measure("class_create", async |i| {
    315         c.push(
    316             ctx.post_admin("/conversion-rate-classes")
    317                 .json(json!({
    318                     "name": format!("Gen class {i}")
    319                 }))
    320                 .await
    321                 .assert_ok_json::<ConversionRateClassResponse>()
    322                 .conversion_rate_class_id,
    323         )
    324     })
    325     .await;
    326     b.measure("class_patch", async |i| {
    327         ctx.patch_admin(format!("/conversion-rate-classes/{}", c[i]))
    328             .json(json!({
    329                 "name": format!("Gen class {i}"),
    330                 "description": format!("test {i}")
    331             }))
    332             .await
    333             .assert_no_content()
    334     })
    335     .await;
    336     b.measure("class_get", async |i| {
    337         ctx.get_admin(format!("/conversion-rate-classes/{}", c[i]))
    338             .await
    339             .assert_ok()
    340     })
    341     .await;
    342     b.measure("class_list", async |_| {
    343         ctx.get_admin("/conversion-rate-classes").await.assert_ok()
    344     })
    345     .await;
    346     b.measure("class_delete", async |i| {
    347         ctx.delete_admin(format!("/conversion-rate-classes/{}", c[i]))
    348             .await
    349             .assert_no_content()
    350     })
    351     .await;
    352 
    353     // Transaction
    354     let mut tx = Vec::new();
    355     b.measure("transaction_create", async |_| {
    356         tx.push(
    357             ctx.posta("/accounts/customer/transactions")
    358                 .json(json!({
    359                     "payto_uri": format!("{}?receiver-name=Test&message=payout", ctx.merchant_payto),
    360                     "amount": "KUDOS:0.001"
    361                 }))
    362                 .await
    363                 .assert_ok_json::<TransactionCreateResponse>()
    364                 .row_id,
    365         )
    366     })
    367     .await;
    368     b.measure("transaction_get", async |i| {
    369         ctx.geta(format!("/accounts/customer/transactions/{}", tx[i]))
    370             .await
    371             .assert_ok()
    372     })
    373     .await;
    374     b.measure("transaction_history", async |_| {
    375         ctx.geta("/accounts/customer/transactions")
    376             .await
    377             .assert_ok()
    378     })
    379     .await;
    380     b.measure("transaction_revenue", async |_| {
    381         ctx.geta("/accounts/merchant/taler-revenue/history")
    382             .await
    383             .assert_ok()
    384     })
    385     .await;
    386 
    387     // Withdrawal
    388     let mut w = Vec::new();
    389     b.measure("withdrawal_create", async |_| {
    390         w.push(
    391             ctx.posta("/accounts/customer/withdrawals")
    392                 .json(json!({
    393                     "amount": "KUDOS:0.0001"
    394                 }))
    395                 .await
    396                 .assert_ok_json::<BankAccountCreateWithdrawalResponse>()
    397                 .withdrawal_id,
    398         )
    399     })
    400     .await;
    401     b.measure("withdrawal_get", async |i| {
    402         ctx.get(format!("/withdrawals/{}", w[i])).await.assert_ok()
    403     })
    404     .await;
    405     b.measure("withdrawal_status", async |i| {
    406         ctx.get(format!("/taler-integration/withdrawal-operation/{}", w[i]))
    407             .await
    408             .assert_ok()
    409     })
    410     .await;
    411     b.measure("withdrawal_select", async |i| {
    412         ctx.post(format!("/taler-integration/withdrawal-operation/{}", w[i]))
    413             .json(json!({
    414                 "reserve_pub": &EddsaPublicKey::rand(),
    415                 "selected_exchange": &ctx.exchange_payto
    416             }))
    417             .await
    418             .assert_ok()
    419     })
    420     .await;
    421     b.measure("withdrawal_confirm", async |i| {
    422         ctx.posta(format!("/accounts/customer/withdrawals/{}/confirm", w[i]))
    423             .json(json!({}))
    424             .await
    425             .assert_no_content()
    426     })
    427     .await;
    428     b.measure("withdrawal_abort", async |i| {
    429         ctx.posta(format!("/accounts/customer/withdrawals/{}/abort", w[i]))
    430             .await
    431             .assert_error(ErrorCode::BANK_ABORT_CONFIRM_CONFLICT);
    432     })
    433     .await;
    434 
    435     // Cashout
    436     let mut c = Vec::new();
    437     b.measure("cashout_create", async |_| {
    438         c.push(
    439             ctx.posta("/accounts/customer/cashouts")
    440                 .json(json!({
    441                     "request_uid":  ShortHashCode::rand(),
    442                     "amount_debit": "KUDOS:0.1",
    443                     "amount_credit": &ctx.convert("KUDOS:0.1").await
    444                 }))
    445                 .await
    446                 .assert_ok_json::<CashoutResponse>()
    447                 .cashout_id,
    448         )
    449     })
    450     .await;
    451     b.measure("cashout_get", async |i| {
    452         ctx.geta(format!("/accounts/customer/cashouts/{}", c[i]))
    453             .await
    454             .assert_ok()
    455     })
    456     .await;
    457     b.measure("cashout_history", async |_| {
    458         ctx.geta("/accounts/customer/cashouts").await.assert_ok()
    459     })
    460     .await;
    461     b.measure("cashout_history_admin", async |_| {
    462         ctx.get_admin("/cashouts").await.assert_ok()
    463     })
    464     .await;
    465 
    466     // Wire gateway
    467     let mut t = Vec::new();
    468     b.measure("wg_transfer", async |_| {
    469         t.push(
    470             ctx.posta("/accounts/exchange/taler-wire-gateway/transfer")
    471                 .json(json!({
    472                     "request_uid": HashCode::rand(),
    473                     "amount": "KUDOS:0.0001",
    474                     "exchange_base_url": "http://exchange.example.com/",
    475                     "wtid": ShortHashCode::rand(),
    476                     "credit_account": &ctx.customer_payto
    477                 }))
    478                 .await
    479                 .assert_ok_json::<TransferResponse>()
    480                 .row_id,
    481         )
    482     })
    483     .await;
    484     b.measure("wg_transfer_get", async |i| {
    485         ctx.geta(format!(
    486             "/accounts/exchange/taler-wire-gateway/transfers/{}",
    487             t[i]
    488         ))
    489         .await
    490         .assert_ok()
    491     })
    492     .await;
    493     b.measure("wg_transfer_page", async |_| {
    494         ctx.geta("/accounts/exchange/taler-wire-gateway/transfers")
    495             .await
    496             .assert_ok()
    497     })
    498     .await;
    499     b.measure("wg_transfer_page_filter", async |_| {
    500         ctx.geta("/accounts/exchange/taler-wire-gateway/transfers?status=success")
    501             .await
    502             .assert_ok()
    503     })
    504     .await;
    505     b.measure("wg_add", async |_| {
    506         ctx.posta("/accounts/exchange/taler-wire-gateway/admin/add-incoming")
    507             .json(json!({
    508                 "amount": "KUDOS:0.0001",
    509                 "reserve_pub": EddsaPublicKey::rand(),
    510                 "debit_account": &ctx.customer_payto
    511             }))
    512             .await
    513             .assert_ok()
    514     })
    515     .await;
    516     b.measure("wg_incoming", async |_| {
    517         ctx.geta("/accounts/exchange/taler-wire-gateway/history/incoming")
    518             .await
    519             .assert_ok()
    520     })
    521     .await;
    522     b.measure("wg_outgoing", async |_| {
    523         ctx.geta("/accounts/exchange/taler-wire-gateway/history/outgoing")
    524             .await
    525             .assert_ok()
    526     })
    527     .await;
    528 
    529     // TAN challenges
    530     let mut c = Vec::new();
    531     b.measure("tan_send", async |i| {
    532         let res = ctx
    533             .patcha(format!("/accounts/account_bench_{i}"))
    534             .json(json!({
    535                 "contact_data" : {
    536                     "phone": "+99",
    537                     "email": "email@example.com"
    538                 },
    539                 "tan_channel": "sms"
    540             }))
    541             .await
    542             .assert_accepted_json::<ChallengeResponse>();
    543         let challenge = &res.challenges[0];
    544         ctx.posta(format!(
    545             "/accounts/account_bench_{i}/challenge/{}",
    546             challenge.challenge_id
    547         ))
    548         .await
    549         .assert_ok();
    550         let code = tan_code(&challenge.tan_info);
    551         c.push((challenge.challenge_id.clone(), code))
    552     })
    553     .await;
    554     b.measure("tan_confirm", async |i| {
    555         let (id, code) = &c[i];
    556         ctx.posta(format!(
    557             "/accounts/account_bench_{i}/challenge/{id}/confirm"
    558         ))
    559         .json(json!({
    560             "tan": code
    561         }))
    562         .await
    563         .assert_no_content();
    564     })
    565     .await;
    566 
    567     // Revenue api
    568     b.measure("transaction_revenue", async |_| {
    569         ctx.geta("/accounts/exchange/taler-revenue/history")
    570             .await
    571             .assert_ok()
    572     })
    573     .await;
    574 
    575     // Wire transfer
    576     b.measure("wt_register", async |i| {
    577         let (pair, key) = &accounts_pubs[i];
    578         let req = RegistrationRequest {
    579             credit_account: ctx.exchange_payto.as_uri(),
    580             r#type: TransferType::reserve,
    581             recurrent: false,
    582             credit_amount: Amount::new(&Currency::KUDOS, 55, 0),
    583             alg: PublicKeyAlg::EdDSA,
    584             account_pub: *key,
    585             authorization_pub: *key,
    586             authorization_sig: EddsaSignature::ZEROED,
    587         }
    588         .signed(pair);
    589         for _ in 0..2 {
    590             ctx.post("/taler-prepared-transfer/registration")
    591                 .json(&req)
    592                 .await
    593                 .assert_ok()
    594         }
    595     })
    596     .await;
    597     b.measure("wt_unregister", async |i| {
    598         let (pair, key) = &accounts_pubs[i];
    599         let req = Unregistration {
    600             timestamp: TalerTimestamp::Timestamp(Timestamp::now()),
    601             authorization_pub: *key,
    602             authorization_sig: EddsaSignature::ZEROED,
    603         }
    604         .signed(pair);
    605         ctx.post("/taler-prepared-transfer/unregistration")
    606             .json(&req)
    607             .await
    608             .assert_no_content();
    609         ctx.post("/taler-prepared-transfer/unregistration")
    610             .json(&req)
    611             .await
    612             .assert_error(ErrorCode::BANK_TRANSACTION_NOT_FOUND);
    613     })
    614     .await;
    615 
    616     // Delete accounts
    617     b.measure("account_delete", async |i| {
    618         ctx.deletea(format!("/accounts/account_bench_{i}"))
    619             .await
    620             .assert_no_content()
    621     })
    622     .await;
    623 
    624     // Other
    625     b.measure("monitor", async |_| {
    626         ctx.get_admin("/monitor").await.assert_ok();
    627     })
    628     .await;
    629     let now = Timestamp::MIN;
    630     collect(&ctx.state.db, &now, &now, &now).await.unwrap();
    631     b.measure("gc", async |_| {
    632         collect(&ctx.state.db, &now, &now, &now).await.unwrap();
    633     })
    634     .await;
    635 }