libeufin

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

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 }