commit 121d46756e448b35397662e519fa2f3cacb9868a
parent 149b730bedd237f705b05d0bce34a035133a9fea
Author: Antoine A <>
Date: Fri, 7 Nov 2025 16:20:55 +0100
magnet-bank: we all love tests
Diffstat:
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(¤t) {
+ return;
+ }
+ if attempts > 20 {
+ assert!(check(¤t), "{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);
}