libeufin

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

testing.rs (11855B)


      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::io::{Cursor, Read};
     21 
     22 use anyhow::{anyhow, bail};
     23 use compact_str::CompactString;
     24 use jiff::{Timestamp, civil::Date, tz::TimeZone};
     25 use libeufin_ebics::{
     26     ebics::{
     27         EbicsErrKind,
     28         order::{BTF, Order, OrderDoc},
     29         tx_check,
     30     },
     31     iso20022::model::{InId, InTx},
     32     ws::listen_for_notification,
     33 };
     34 use taler_common::{
     35     config::Config,
     36     types::{
     37         amount::Amount,
     38         iban::{Country, IBAN},
     39         payto::{BankID, ParsedPayto, PaytoImpl},
     40     },
     41 };
     42 use tracing::debug;
     43 
     44 use crate::{
     45     EbicsClient, EbicsLogs, config::NexusCfg, db::pool, fetch::register_incoming, list::ListCmd,
     46     rand_ebics_id,
     47 };
     48 
     49 #[derive(clap::Subcommand, Debug)]
     50 pub enum IbanCmd {
     51     /// Generate fake IBANs for testing
     52     Gen {
     53         #[arg(long)]
     54         country: Country,
     55     },
     56 }
     57 
     58 impl IbanCmd {
     59     pub fn run(self) -> anyhow::Result<()> {
     60         match self {
     61             IbanCmd::Gen { country } => {
     62                 println!("{}", IBAN::random(country))
     63             }
     64         }
     65         Ok(())
     66     }
     67 }
     68 
     69 /// Testing helper commands
     70 #[derive(clap::Subcommand, Debug)]
     71 pub enum TestingCmd {
     72     /// List incoming transactions
     73     #[clap(subcommand)]
     74     Iban(IbanCmd),
     75     /// Genere a fake incoming payment
     76     FakeIncoming {
     77         /// The amount to transfer, payto 'amount' parameter takes the precedence
     78         #[arg(long)]
     79         amount: Option<Amount>,
     80 
     81         /// The payment credit fee
     82         #[arg(long)]
     83         credit_fee: Option<Amount>,
     84 
     85         /// The payment subject, payto 'message' parameter takes the precedence
     86         #[arg(long)]
     87         subject: Option<CompactString>,
     88 
     89         /// The payment reference, credit-payto 'ch-qrr' parameter takes precedence
     90         #[arg(long)]
     91         ch_qrr: Option<CompactString>,
     92 
     93         /// The credited account IBAN payto URI
     94         #[arg(long)]
     95         credit_payto: Option<ParsedPayto<BankID>>,
     96 
     97         /// The debited account IBAN payto URI
     98         #[arg(long)]
     99         debit_payto: Option<ParsedPayto<BankID>>,
    100 
    101         /// The debited account IBAN payto URI
    102         payto: Option<ParsedPayto<BankID>>,
    103     },
    104     #[clap(subcommand)]
    105     List(ListCmd),
    106     /// Perform EBICS requests
    107     EbicsBtd {
    108         #[arg(long("type"), default_value_t = CompactString::const_new("BTD"))]
    109         ty: CompactString,
    110         #[arg(long)]
    111         name: CompactString,
    112         #[arg(long)]
    113         scope: Option<CompactString>,
    114         #[arg(long)]
    115         message_name: CompactString,
    116         #[arg(long)]
    117         message_version: Option<CompactString>,
    118         #[arg(long)]
    119         container: Option<CompactString>,
    120         #[arg(long)]
    121         option: Option<CompactString>,
    122         #[clap(flatten)]
    123         logs: EbicsLogs,
    124         /// Erliest timestamp of the downloaded documents
    125         #[arg(long, value_name = "YYYY-MM-DD")]
    126         pinned_start: Option<Date>,
    127         /// Do not consume fetched documents
    128         #[arg(long)]
    129         peek: bool,
    130         #[arg(long)]
    131         dry_run: bool,
    132     },
    133     /// Check transaction semantic
    134     TxCheck {
    135         #[clap(flatten)]
    136         logs: EbicsLogs,
    137     },
    138     /// Listen to EBICS instant notification over websocket
    139     Wss {
    140         #[clap(flatten)]
    141         logs: EbicsLogs,
    142     },
    143 }
    144 
    145 impl TestingCmd {
    146     pub async fn run(self, cfg: &Config) -> anyhow::Result<()> {
    147         match self {
    148             TestingCmd::Iban(cmd) => cmd.run()?,
    149             TestingCmd::FakeIncoming {
    150                 amount,
    151                 credit_fee,
    152                 subject,
    153                 ch_qrr,
    154                 credit_payto,
    155                 debit_payto,
    156                 payto,
    157             } => {
    158                 let cfg = NexusCfg::parse(cfg)?;
    159                 let ebics_cfg = cfg.ebics()?;
    160                 let db = pool(&cfg.db_cfg).await?;
    161                 let amount = credit_payto
    162                     .as_ref()
    163                     .and_then(|p| p.amount)
    164                     .or_else(|| payto.as_ref().and_then(|p| p.amount))
    165                     .or(amount)
    166                     .ok_or(anyhow!("Missing amount"))?;
    167                 let subject = credit_payto
    168                     .as_ref()
    169                     .and_then(|p| p.subject.clone())
    170                     .or_else(|| payto.as_ref().and_then(|p| p.subject.clone()))
    171                     .or_else(|| subject.clone());
    172                 let reference = credit_payto
    173                     .as_ref()
    174                     .and_then(|p| p.ch_qrr.clone())
    175                     .or_else(|| payto.as_ref().and_then(|p| p.ch_qrr.clone()))
    176                     .or_else(|| ch_qrr.clone());
    177 
    178                 if cfg.currency != amount.currency {
    179                     bail!(
    180                         "Wrong currency: expected {} got {}",
    181                         cfg.currency,
    182                         amount.currency
    183                     );
    184                 }
    185 
    186                 if let Some(ref credit_payto) = credit_payto {
    187                     if reference.is_some() {
    188                         if let Some(qr_iban) = ebics_cfg.qr_iban {
    189                             if credit_payto.iban != qr_iban {
    190                                 bail!(
    191                                     "Creditor must be the exchange QRR account expected {qr_iban} got {}",
    192                                     credit_payto.iban
    193                                 )
    194                             }
    195                         } else {
    196                             bail!("QR reference is not supported without a configured QR IBAN")
    197                         }
    198                     } else {
    199                         if credit_payto.iban != ebics_cfg.account.iban {
    200                             bail!(
    201                                 "Creditor must be the exchange expected {} got {}",
    202                                 ebics_cfg.account.iban,
    203                                 credit_payto.iban
    204                             )
    205                         }
    206                     }
    207                 }
    208 
    209                 register_incoming(
    210                     &db,
    211                     &cfg.ingest()?,
    212                     &InTx {
    213                         id: InId::new(None, Some(rand_ebics_id()), None),
    214                         amount,
    215                         credit_fee: credit_fee.unwrap_or(Amount::zero(&cfg.currency)),
    216                         subject: reference.or(subject).map(|it| it.into_string()),
    217                         execution_time: Timestamp::now(),
    218                         debtor: Some(
    219                             debit_payto
    220                                 .map(|it| it.as_uri())
    221                                 .or(payto.map(|it| it.as_uri()))
    222                                 .unwrap_or_else(|| {
    223                                     IBAN::random(ebics_cfg.account.iban.country())
    224                                         .as_full_uri("Testing Account")
    225                                 }),
    226                         ),
    227                     },
    228                 )
    229                 .await?;
    230             }
    231             TestingCmd::List(list_cmd) => {
    232                 let cfg = NexusCfg::parse(cfg)?;
    233                 let db = pool(&cfg.db_cfg).await?;
    234                 list_cmd.run(&db, &cfg.currency).await?;
    235             }
    236             TestingCmd::EbicsBtd {
    237                 ty,
    238                 name,
    239                 scope,
    240                 message_name,
    241                 message_version,
    242                 container,
    243                 option,
    244                 logs,
    245                 pinned_start,
    246                 peek,
    247                 dry_run,
    248             } => {
    249                 let cfg = NexusCfg::parse(cfg)?;
    250                 let db = pool(&cfg.db_cfg).await?;
    251                 let order = Order::from_parts(
    252                     &ty,
    253                     Some(BTF {
    254                         service: name,
    255                         scope,
    256                         option,
    257                         container: container.clone(),
    258                         msg: message_name,
    259                         version: message_version,
    260                     }),
    261                 )
    262                 .ok_or(anyhow!("Unknown ebics order"))?;
    263                 let (client, bank) = cfg.expect_full_keys()?;
    264                 let ebics = EbicsClient::new(cfg.host()?.ebics(), logs)?;
    265                 ebics
    266                     .download(
    267                         &db,
    268                         &client,
    269                         &bank,
    270                         &order,
    271                         &pinned_start.map(|date| {
    272                             (
    273                                 date.to_zoned(TimeZone::UTC).unwrap().timestamp(),
    274                                 Timestamp::now(),
    275                             )
    276                         }),
    277                         peek,
    278                         async move |content| {
    279                             if container.as_deref() == Some("ZIP") {
    280                                 let mut z = zip::ZipArchive::new(Cursor::new(content))?;
    281                                 for i in 0..z.len() {
    282                                     let mut file = z.by_index(i)?;
    283                                     let mut buf = Vec::new();
    284                                     file.read_to_end(&mut buf)?;
    285                                     println!("{}\n{}", file.name(), String::from_utf8_lossy(&buf));
    286                                 }
    287                             } else {
    288                                 println!("{}", String::from_utf8_lossy(&content));
    289                             }
    290                             if dry_run {
    291                                 Err(EbicsErrKind::Custom("dry run".into()))
    292                             } else {
    293                                 Ok(())
    294                             }
    295                         },
    296                     )
    297                     .await?;
    298             }
    299             TestingCmd::TxCheck { logs } => {
    300                 let cfg = NexusCfg::parse(cfg)?;
    301                 let db = pool(&cfg.db_cfg).await?;
    302                 let (client, bank) = cfg.expect_full_keys()?;
    303                 let ebics = EbicsClient::new(cfg.host()?.ebics(), logs)?;
    304                 let dialect = cfg.ebics()?.dialect.standard();
    305                 let res = tx_check(
    306                     &ebics,
    307                     &db,
    308                     &client,
    309                     &bank,
    310                     &dialect.downloads(&OrderDoc::acknowledgement)[0],
    311                     &dialect.direct_debit(),
    312                 )
    313                 .await?;
    314                 println!("{res:?}")
    315             }
    316             TestingCmd::Wss { logs } => {
    317                 let cfg = NexusCfg::parse(cfg)?;
    318                 let db = pool(&cfg.db_cfg).await?;
    319                 let (client, bank) = cfg.expect_full_keys()?;
    320                 let ebics = EbicsClient::new(cfg.host()?.ebics(), logs)?;
    321                 let (sender, mut receiver) = tokio::sync::mpsc::channel(10);
    322                 tokio::join!(
    323                     listen_for_notification(&ebics, &db, &client, &bank, sender),
    324                     async {
    325                         while let Some(orders) = receiver.recv().await {
    326                             debug!(target: "testing", "{orders:?}")
    327                         }
    328                     }
    329                 );
    330             }
    331         }
    332         Ok(())
    333     }
    334 }