taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit 121d46756e448b35397662e519fa2f3cacb9868a
parent 149b730bedd237f705b05d0bce34a035133a9fea
Author: Antoine A <>
Date:   Fri,  7 Nov 2025 16:20:55 +0100

magnet-bank: we all love tests

Diffstat:
MCargo.lock | 29++++++++++-------------------
Mtaler-magnet-bank/src/bin/magnet-bank-harness.rs | 284++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mtaler-magnet-bank/src/dev.rs | 14++++++++++++--
Mtaler-magnet-bank/src/magnet_api/client.rs | 10+++++++---
Mtaler-magnet-bank/src/magnet_api/types.rs | 6++++++
Mtaler-magnet-bank/src/worker.rs | 13++++++++++---
6 files changed, 248 insertions(+), 108 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -211,9 +211,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.44" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "shlex", @@ -1148,23 +1148,23 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", - "windows-sys 0.59.0", + "serde_core", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", @@ -1542,9 +1542,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -2811,15 +2811,6 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" diff --git a/taler-magnet-bank/src/bin/magnet-bank-harness.rs b/taler-magnet-bank/src/bin/magnet-bank-harness.rs @@ -32,13 +32,13 @@ use taler_common::{ types::{self, amount::amount, url}, }; use taler_magnet_bank::{ - CONFIG_SOURCE, FullHuPayto, + CONFIG_SOURCE, FullHuPayto, HuIban, config::{HarnessCfg, parse_db_cfg}, db::{self, TransferResult}, failure_injection::{FailureLogic, InjectedErr, set_failure_logic}, magnet_api::{ client::{ApiClient, AuthClient}, - types::Account, + types::{Account, Direction, Order, TxDto, TxStatus}, }, setup, worker::{Worker, WorkerError}, @@ -66,18 +66,19 @@ struct HarnessClient<'a> { } impl HarnessClient<'_> { - async fn balance(&self) -> anyhow::Result<(u32, u32)> { + async fn balance(&self) -> (u32, u32) { let (exchange_balance, client_balance) = tokio::try_join!( self.api.balance_mini(self.exchange.iban.bban()), self.api.balance_mini(self.client.iban.bban()) - )?; - Ok(( + ) + .unwrap(); + ( exchange_balance.balance as u32, client_balance.balance as u32, - )) + ) } - async fn custom_transfer(&self, forint: u32, creditor: FullHuPayto) -> anyhow::Result<u64> { + async fn custom_transfer(&self, forint: u32, creditor: &FullHuPayto) -> u64 { let res = db::make_transfer( self.pool, &TransferRequest { @@ -90,36 +91,31 @@ impl HarnessClient<'_> { &creditor, &types::timestamp::Timestamp::now(), ) - .await?; + .await + .unwrap(); match res { - TransferResult::Success { id, .. } => Ok(id), + TransferResult::Success { id, .. } => id, TransferResult::RequestUidReuse | TransferResult::WtidReuse => unreachable!(), } } - async fn transfer(&self, forint: u32) -> anyhow::Result<u64> { + async fn transfer(&self, forint: u32) -> u64 { self.custom_transfer( forint, - FullHuPayto::new(self.client.iban.clone(), "Name".to_owned()), + &FullHuPayto::new(self.client.iban.clone(), "Name".to_owned()), ) .await } - async fn assert_transfer_status( - &self, - id: u64, - status: TransferState, - msg: Option<&str>, - ) -> anyhow::Result<()> { - let transfer = db::transfer_by_id(self.pool, id).await?.unwrap(); + async fn expect_transfer_status(&self, id: u64, status: TransferState, msg: Option<&str>) { + let transfer = db::transfer_by_id(self.pool, id).await.unwrap().unwrap(); assert_eq!( (transfer.status, transfer.status_msg.as_deref()), (status, msg) ); - Ok(()) } - async fn assert_incoming(&self, key: EddsaPublicKey) -> anyhow::Result<()> { + async fn expect_incoming(&self, key: EddsaPublicKey) { let transfer = db::incoming_history( self.pool, &History { @@ -131,22 +127,16 @@ impl HarnessClient<'_> { }, || tokio::sync::watch::channel(0).1, ) - .await?; + .await + .unwrap(); assert!(matches!( transfer.first().unwrap(), IncomingBankTransaction::Reserve { reserve_pub, .. } if *reserve_pub == key, )); - Ok(()) } /// Send a transaction between two magnet accounts - async fn send_tx( - &self, - from: &Account, - to: &Account, - subject: &str, - amount: u32, - ) -> anyhow::Result<()> { + async fn send_tx(&self, from: &Account, to: &HuIban, subject: &str, amount: u32) -> u64 { let now = Zoned::now(); let info = self .api @@ -156,9 +146,10 @@ impl HarnessClient<'_> { subject, &now.date(), "Name", - to.iban.bban(), + to.bban(), ) - .await?; + .await + .unwrap(); self.api .submit_tx( self.signing_key, @@ -166,23 +157,77 @@ impl HarnessClient<'_> { info.code, info.amount, &now.date(), - to.iban.bban(), + to.bban(), ) - .await?; - Ok(()) + .await + .unwrap(); + info.code + } + + async fn latest_tx(&self, account: &Account) -> TxDto { + self.api + .page_tx( + Direction::Both, + Order::Descending, + 1, + account.iban.bban(), + &None, + true, + ) + .await + .unwrap() + .list + .pop() + .unwrap() + .tx + } + + async fn expect_latest_tx(&self, account: &Account, mut check: impl FnMut(&TxDto) -> bool) { + let mut attempts = 0; + loop { + let current = self.latest_tx(account).await; + if check(&current) { + return; + } + if attempts > 20 { + assert!(check(&current), "{current:?}"); + } + attempts += 1; + tokio::time::sleep(Duration::from_millis(200)).await; + } } /// Send transaction from client to exchange - async fn client_send(&self, subject: &str, amount: u32) -> anyhow::Result<()> { - self.send_tx(self.client, self.exchange, subject, amount) + async fn client_send(&self, subject: &str, amount: u32) -> u64 { + self.send_tx(self.client, &self.exchange.iban, subject, amount) .await } /// Send transaction from exchange to client - async fn exchange_send(&self, subject: &str, amount: u32) -> anyhow::Result<()> { - self.send_tx(self.exchange, self.client, subject, amount) + async fn exchange_send_to(&self, subject: &str, amount: u32, to: &HuIban) -> u64 { + self.send_tx(self.exchange, to, subject, amount).await + } + + /// Send transaction from exchange to client + async fn exchange_send(&self, subject: &str, amount: u32) -> u64 { + self.exchange_send_to(subject, amount, &self.client.iban) .await } + + async fn expect_status(&self, code: u64, status: TxStatus) { + let mut attempts = 0; + loop { + let current = self.api.get_tx(code).await.unwrap().status; + if current == status { + return; + } + if attempts > 20 { + assert_eq!(current, status, "{code}"); + } + attempts += 1; + tokio::time::sleep(Duration::from_millis(200)).await; + } + } } struct Balances<'a> { @@ -192,23 +237,23 @@ struct Balances<'a> { } impl<'a> Balances<'a> { - pub async fn new(client: &'a HarnessClient<'a>) -> anyhow::Result<Self> { - let (exchange_balance, client_balance) = client.balance().await?; - Ok(Self { + pub async fn new(client: &'a HarnessClient<'a>) -> Self { + let (exchange_balance, client_balance) = client.balance().await; + Self { client, exchange_balance, client_balance, - }) + } } - async fn expect(&mut self, diff: i32) -> anyhow::Result<()> { + async fn expect(&mut self, diff: i32) { self.exchange_balance = (self.exchange_balance as i32 + diff) as u32; self.client_balance = (self.client_balance as i32 - diff) as u32; let mut attempts = 0; loop { - let current = self.client.balance().await?; + let current = self.client.balance().await; if current == (self.exchange_balance, self.client_balance) { - return Ok(()); + return; } if attempts > 20 { assert_eq!( @@ -218,7 +263,7 @@ impl<'a> Balances<'a> { ); } attempts += 1; - tokio::time::sleep(Duration::from_secs(1)).await; + tokio::time::sleep(Duration::from_millis(200)).await; } } } @@ -271,100 +316,177 @@ fn main() { worker.run().await?; } + let unknown_account = FullHuPayto::new( + HuIban::from_bban("1620000310991642").unwrap(), + "Unknown".to_string(), + ); let now = Timestamp::now(); - let balance = &mut Balances::new(&harness).await?; + let balance = &mut Balances::new(&harness).await; step("Test incoming talerable transaction"); // Send talerable transaction let reserve_pub = rand_edsa_pub_key(); harness .client_send(&format!("Taler {reserve_pub}"), 33) - .await?; + .await; // Wait for transaction to finalize - balance.expect(33).await?; + balance.expect(33).await; // Sync and register worker.run().await?; - harness.assert_incoming(reserve_pub).await?; + harness.expect_incoming(reserve_pub).await; step("Test incoming malformed transaction"); // Send malformed transaction harness .client_send(&format!("Malformed test {now}"), 34) - .await?; + .await; // Wait for transaction to finalize - balance.expect(34).await?; + balance.expect(34).await; // Sync and bounce worker.run().await?; // Wait for bounce to finalize - balance.expect(-34).await?; + balance.expect(-34).await; worker.run().await?; - step("Test outgoing transactions to self"); + step("Test transfer to self"); + // Init a transfer to self let transfer_id = harness .custom_transfer( 101, - FullHuPayto::new(exchange_account.iban.clone(), "Self".to_string()), + &FullHuPayto::new(exchange_account.iban.clone(), "Self".to_string()), ) - .await?; + .await; + // Should failed worker.run().await?; + // Check transfer failed harness - .assert_transfer_status( + .expect_transfer_status( transfer_id, TransferState::permanent_failure, Some("409 FORRAS_SZAMLA_ESZAMLA_EGYEZIK 'A forrás és az ellenszámla egyezik!'"), ) - .await?; - balance.expect(0).await?; - - step("Test unexpected outgoing"); - harness - .exchange_send(&format!("What is this ? {now}"), 4) - .await?; - worker.run().await?; - balance.expect(-4).await?; - worker.run().await?; + .await; - step("Test transfer transactions to self"); + step("Test transfer transactions"); + // Init a transfer to client let transfer_id = harness .custom_transfer( 102, - FullHuPayto::new(client_account.iban.clone(), "Client".to_string()), + &FullHuPayto::new(client_account.iban.clone(), "Client".to_string()), ) - .await?; + .await; + // Should send worker.run().await?; + // Check transfer is still pending harness - .assert_transfer_status(transfer_id, TransferState::pending, None) - .await?; - balance.expect(-102).await?; + .expect_transfer_status(transfer_id, TransferState::pending, None) + .await; + // Wait for transaction to finalize + balance.expect(-102).await; + // Should register worker.run().await?; + // Check transfer is now successful + harness + .expect_transfer_status(transfer_id, TransferState::success, None) + .await; + step("Test transfer to unknown account"); + let transfer_id = harness.custom_transfer(103, &unknown_account).await; + worker.run().await?; + harness + .expect_transfer_status(transfer_id, TransferState::pending, None) + .await; + balance.expect(0).await; + worker.run().await?; harness - .assert_transfer_status(transfer_id, TransferState::success, None) - .await?; + .expect_transfer_status(transfer_id, TransferState::permanent_failure, None) + .await; + + step("Test unexpected outgoing"); + // Manual tx from the exchange + harness + .exchange_send(&format!("What is this ? {now}"), 4) + .await; + worker.run().await?; + // Wait for transaction to finalize + balance.expect(-4).await; + worker.run().await?; step("Test transfer failure init-tx"); - harness.transfer(10).await?; + harness.transfer(10).await; set_failure_logic(FailureLogic::History(vec!["init-tx"])); assert!(matches!( worker.run().await, Err(WorkerError::Injected(InjectedErr("init-tx"))) )); - balance.expect(0).await?; worker.run().await?; - balance.expect(-10).await?; + balance.expect(-10).await; worker.run().await?; step("Test transfer failure submit-tx"); - harness.transfer(11).await?; + harness.transfer(11).await; set_failure_logic(FailureLogic::History(vec!["submit-tx"])); assert!(matches!( worker.run().await, Err(WorkerError::Injected(InjectedErr("submit-tx"))) )); - balance.expect(0).await?; worker.run().await?; - balance.expect(-11).await?; + balance.expect(-11).await; + worker.run().await?; + + step("Test transfer all failures"); + harness.transfer(13).await; + set_failure_logic(FailureLogic::History(vec!["init-tx", "submit-tx"])); + assert!(matches!( + worker.run().await, + Err(WorkerError::Injected(InjectedErr("init-tx"))) + )); + assert!(matches!( + worker.run().await, + Err(WorkerError::Injected(InjectedErr("submit-tx"))) + )); + worker.run().await?; + balance.expect(-13).await; + worker.run().await?; + + step("Test recover successful bounces"); + let code = harness + .client_send(&format!("will be bounced {now}"), 2) + .await; + balance.expect(2).await; + harness + .exchange_send(&format!("bounced: {}", code + 1), 2) + .await; + balance.expect(-2).await; + worker.run().await?; + + step("Test recover failed bounces"); + // Send malformed transaction + harness + .client_send(&format!("will be failed bounced {now}"), 3) + .await; + // Wait for it to be received because rejected transaction take too much time to appear in the transactions log + balance.expect(3).await; + // Bounce it manually + let received = harness.latest_tx(&exchange_account).await; + let bounce_code = harness + .exchange_send_to( + &format!("bounce manualy: {}", received.code), + 3, + &unknown_account, + ) + .await; + harness.expect_status(bounce_code, TxStatus::Rejected).await; + // Should not bounce and catch the failure + worker.run().await?; + // Wait for it to be bounce regardless because rejected transaction take too much time to appear in the transactions log + // TODO fix this + balance.expect(-3).await; + + step("Finish"); + tokio::time::sleep(Duration::from_secs(10)).await; worker.run().await?; + balance.expect(0).await; Ok(()) }); } diff --git a/taler-magnet-bank/src/dev.rs b/taler-magnet-bank/src/dev.rs @@ -26,7 +26,10 @@ use tracing::info; use crate::{ HuIban, HuPayto, TransferHuPayto, config::WorkerCfg, - magnet_api::{client::AuthClient, types::Direction}, + magnet_api::{ + client::AuthClient, + types::{Direction, Order}, + }, setup, worker::{Tx, extract_tx_info}, }; @@ -86,7 +89,14 @@ pub async fn dev(cfg: &Config, cmd: DevCmd) -> anyhow::Result<()> { let mut next = None; loop { let page = client - .page_tx(dir, 100, account.bban(), &next, next.is_none()) + .page_tx( + dir, + Order::Ascending, + 100, + account.bban(), + &next, + next.is_none(), + ) .await?; next = page.next; for item in page.list { diff --git a/taler-magnet-bank/src/magnet_api/client.rs b/taler-magnet-bank/src/magnet_api/client.rs @@ -30,7 +30,7 @@ use crate::magnet_api::{ api::{ApiResult, MagnetRequest}, oauth::{Token, TokenAuth}, types::{ - Account, BalanceMini, Direction, Next, PartnerList, SmsCodeSubmission, TokenInfo, + Account, BalanceMini, Direction, Next, Order, PartnerList, SmsCodeSubmission, TokenInfo, TransactionPage, Tx, }, }; @@ -208,10 +208,11 @@ impl ApiClient<'_> { pub async fn page_tx( &self, direction: Direction, + order: Order, limit: u16, bban: &str, next: &Option<Next>, - refresh: bool, + sync: bool, ) -> ApiResult<TransactionPage> { let mut req = self.request( Method::GET, @@ -223,7 +224,10 @@ impl ApiClient<'_> { .query(&[("nextTipus", &next.next_type)]); } req.query(&[("terheles", direction)]) - .query(&[("tranzakciofrissites", refresh), ("ascending", true)]) + .query(&[ + ("tranzakciofrissites", sync), + ("ascending", order == Order::Ascending), + ]) .parse_json() .await } diff --git a/taler-magnet-bank/src/magnet_api/types.rs b/taler-magnet-bank/src/magnet_api/types.rs @@ -175,6 +175,12 @@ pub enum Direction { Both, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum Order { + Ascending, + Descending, +} + #[derive(Debug, Deserialize)] pub struct Tx { #[serde(rename = "alairas1idopont")] diff --git a/taler-magnet-bank/src/worker.rs b/taler-magnet-bank/src/worker.rs @@ -35,7 +35,7 @@ use crate::{ magnet_api::{ api::{ApiErr, ErrKind}, client::ApiClient, - types::{Direction, Next, TxDto, TxStatus}, + types::{Direction, Next, Order, TxDto, TxStatus}, }, }; @@ -74,7 +74,14 @@ impl Worker<'_> { loop { let page = self .client - .page_tx(Direction::Both, 100, self.account_number, &next, first) + .page_tx( + Direction::Both, + Order::Ascending, + 100, + self.account_number, + &next, + first, + ) .await?; first = false; next = page.next; @@ -266,7 +273,7 @@ impl Worker<'_> { .await?; if let Some(id) = res.initiated_id { if res.new { - error!(target: "worker", "initiated tx {id}: {:?}", tx_out.status); + error!(target: "worker", "initiated tx {id} failed: {:?}", tx_out.status); } else { trace!(target: "worker", "initiated tx {id} already seen {:?}", tx_out.status); }