taler-rust

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

commit fe2d3cb9080ca9c9548591e2829a7e8faaeacc6a
parent dc223fd8aa6b1c27be78484ceb7a8efbb1c42cef
Author: Antoine A <>
Date:   Thu, 26 Mar 2026 18:48:00 +0100

common: improve routine tests

Diffstat:
Mcommon/taler-api/tests/api.rs | 8+-------
Mcommon/taler-test-utils/src/routine.rs | 334++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcommon/taler-test-utils/src/server.rs | 2+-
Mtaler-cyclos/src/api.rs | 8+-------
Mtaler-magnet-bank/src/api.rs | 8+-------
5 files changed, 207 insertions(+), 153 deletions(-)

diff --git a/common/taler-api/tests/api.rs b/common/taler-api/tests/api.rs @@ -84,13 +84,7 @@ async fn outgoing_history() { routine_pagination::<OutgoingHistory, _>( &server, "/taler-wire-gateway/history/outgoing", - |it| { - it.outgoing_transactions - .into_iter() - .map(|it| *it.row_id as i64) - .collect() - }, - async |server, i| { + async |i| { server .post("/taler-wire-gateway/transfer") .json(&json!({ diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -21,9 +21,9 @@ use std::{ }; use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair as _}; -use axum::Router; +use axum::{Router, http::StatusCode}; use jiff::{SignedDuration, Timestamp}; -use serde::{Deserialize, de::DeserializeOwned}; +use serde::de::DeserializeOwned; use taler_api::{ crypto::{check_eddsa_signature, eddsa_sign}, subject::fmt_in_subject, @@ -34,8 +34,8 @@ use taler_common::{ api_revenue::RevenueIncomingHistory, api_transfer::{RegistrationResponse, TransferSubject, TransferType}, api_wire::{ - IncomingBankTransaction, IncomingHistory, TransferList, TransferRequest, TransferResponse, - TransferState, TransferStatus, + IncomingBankTransaction, IncomingHistory, OutgoingHistory, TransferList, TransferRequest, + TransferResponse, TransferState, TransferStatus, }, db::IncomingType, error_code::ErrorCode, @@ -48,11 +48,60 @@ use crate::{ server::{TestResponse, TestServer as _}, }; -pub async fn routine_pagination<'a, T: DeserializeOwned, F: Future<Output = ()>>( - server: &'a Router, +pub trait Page: DeserializeOwned { + fn ids(&self) -> Vec<i64>; +} + +impl Page for IncomingHistory { + fn ids(&self) -> Vec<i64> { + self.incoming_transactions + .iter() + .map(|it| match it { + IncomingBankTransaction::Reserve { row_id, .. } + | IncomingBankTransaction::Wad { row_id, .. } + | IncomingBankTransaction::Kyc { row_id, .. } => **row_id as i64, + }) + .collect() + } +} + +impl Page for OutgoingHistory { + fn ids(&self) -> Vec<i64> { + self.outgoing_transactions + .iter() + .map(|it| *it.row_id as i64) + .collect() + } +} + +impl Page for RevenueIncomingHistory { + fn ids(&self) -> Vec<i64> { + self.incoming_transactions + .iter() + .map(|it| *it.row_id as i64) + .collect() + } +} + +impl Page for TransferList { + fn ids(&self) -> Vec<i64> { + self.transfers.iter().map(|it| *it.row_id as i64).collect() + } +} + +pub async fn latest_id<T: Page>(router: &Router, url: &str) -> i64 { + let res = router.get(&format!("{url}?limit=-1")).await; + if res.status == StatusCode::NO_CONTENT { + 0 + } else { + res.assert_ids::<T>(1)[0] + } +} + +pub async fn routine_pagination<T: Page, F: Future<Output = ()>>( + server: &Router, url: &str, - ids: fn(T) -> Vec<i64>, - mut register: impl FnMut(&'a Router, usize) -> F, + mut register: impl FnMut(usize) -> F, ) { // Check supported if !server.get(url).await.is_implemented() { @@ -61,14 +110,16 @@ pub async fn routine_pagination<'a, T: DeserializeOwned, F: Future<Output = ()>> // Check history is following specs let assert_history = async |args: &str, size: usize| { - let resp = server.get(&format!("{url}?{args}")).await; - assert_history_ids(&resp, ids, size) + server + .get(&format!("{url}?{args}")) + .await + .assert_ids::<T>(size) }; // Get latest registered id - let latest_id = async || assert_history("limit=-1", 1).await[0]; + let latest_id = async || latest_id::<T>(server, url).await; for i in 0..20 { - register(server, i).await; + register(i).await; } let id = latest_id().await; @@ -85,7 +136,7 @@ pub async fn routine_pagination<'a, T: DeserializeOwned, F: Future<Output = ()>> assert_history(&format!("limit=-10&{}", id - 4), 10).await; } -pub async fn assert_time<R: Debug>(range: std::ops::Range<u128>, task: impl Future<Output = R>) { +async fn assert_time<R: Debug>(range: std::ops::Range<u128>, task: impl Future<Output = R>) { let start = Instant::now(); task.await; let elapsed = start.elapsed().as_millis(); @@ -94,26 +145,75 @@ pub async fn assert_time<R: Debug>(range: std::ops::Range<u128>, task: impl Futu } } -pub async fn routine_history< - 'a, - T: DeserializeOwned, - FR: Future<Output = ()>, - FI: Future<Output = ()>, ->( - server: &'a Router, +async fn check_history_trigger<T: Page, F: Future<Output = ()>>( + server: &Router, + url: &str, + lambda: impl FnOnce() -> F, +) { + // Check history is following specs + macro_rules! assert_history { + ($args:expr, $size:expr) => { + async { + server + .get(&format!("{url}?{}", $args)) + .await + .assert_ids::<T>($size) + } + }; + } + // Get latest registered id + let latest_id = latest_id::<T>(server, url).await; + tokio::join!( + // Check polling succeed + assert_time( + 100..400, + assert_history!( + format_args!("limit=2&offset={latest_id}&timeout_ms=1000"), + 1 + ) + ), + assert_time( + 200..500, + assert_history!( + format_args!("limit=1&offset={}&timeout_ms=200", latest_id + 3), + 0 + ) + ), + async { + sleep(Duration::from_millis(100)).await; + lambda().await + } + ); +} + +async fn check_history_in_trigger<F: Future<Output = ()>>( + server: &Router, + lambda: impl FnOnce() -> F, +) { + check_history_trigger::<IncomingHistory, _>( + server, + "/taler-wire-gateway/history/incoming", + lambda, + ) + .await; +} + +pub async fn routine_history<T: Page, FR: Future<Output = ()>, FI: Future<Output = ()>>( + server: &Router, url: &str, - ids: fn(T) -> Vec<i64>, nb_register: usize, - mut register: impl FnMut(&'a Router, usize) -> FR, + mut register: impl FnMut(usize) -> FR, nb_ignore: usize, - mut ignore: impl FnMut(&'a Router, usize) -> FI, + mut ignore: impl FnMut(usize) -> FI, ) { // Check history is following specs macro_rules! assert_history { ($args:expr, $size:expr) => { async { - let resp = server.get(&format!("{url}?{}", $args)).await; - assert_history_ids(&resp, ids, $size) + server + .get(&format!("{url}?{}", $args)) + .await + .assert_ids::<T>($size) } }; } @@ -127,10 +227,10 @@ pub async fn routine_history< let mut ignore_iter = (0..nb_ignore).peekable(); while register_iter.peek().is_some() || ignore_iter.peek().is_some() { if let Some(idx) = register_iter.next() { - register(server, idx).await + register(idx).await } if let Some(idx) = ignore_iter.next() { - ignore(server, idx).await + ignore(idx).await } } let nb_total = nb_register + nb_ignore; @@ -179,7 +279,7 @@ pub async fn routine_history< ), async { sleep(Duration::from_millis(100)).await; - register(server, 0).await + register(0).await } ); @@ -194,7 +294,7 @@ pub async fn routine_history< ), async { sleep(Duration::from_millis(100)).await; - register(server, i).await + register(i).await } ); } @@ -210,60 +310,55 @@ pub async fn routine_history< async { sleep(Duration::from_millis(100)).await; for i in 0..nb_ignore { - ignore(server, i).await + ignore(i).await } } ); - routine_pagination(server, url, ids, register).await; + routine_pagination::<T, _>(server, url, register).await; } -#[track_caller] -fn assert_history_ids<'de, T: Deserialize<'de>>( - resp: &'de TestResponse, - ids: impl Fn(T) -> Vec<i64>, - size: usize, -) -> Vec<i64> { - if size == 0 { - resp.assert_no_content(); - return vec![]; - } - let body = resp.assert_ok_json::<T>(); - let history: Vec<_> = ids(body); - let params = resp.query::<PageParams>().check(1024).unwrap(); - - // testing the size is like expected - assert_eq!(size, history.len(), "bad history length: {history:?}"); - if params.limit < 0 { - // testing that the first id is at most the 'offset' query param. - assert!( - params - .offset - .map(|offset| history[0] <= offset) - .unwrap_or(true), - "bad history offset: {params:?} {history:?}" - ); - // testing that the id decreases. - assert!( - history.as_slice().is_sorted_by(|a, b| a > b), - "bad history order: {history:?}" - ) - } else { - // testing that the first id is at least the 'offset' query param. - assert!( - params - .offset - .map(|offset| history[0] >= offset) - .unwrap_or(true), - "bad history offset: {params:?} {history:?}" - ); - // testing that the id increases. - assert!( - history.as_slice().is_sorted(), - "bad history order: {history:?}" - ) +impl TestResponse { + #[track_caller] + fn assert_ids<T: Page>(&self, size: usize) -> Vec<i64> { + if size == 0 { + self.assert_no_content(); + return vec![]; + } + let body = self.assert_ok_json::<T>(); + let page = body.ids(); + let params = self.query::<PageParams>().check(1024).unwrap(); + + // testing the size is like expected + assert_eq!(size, page.len(), "bad page length: {page:?}"); + if params.limit < 0 { + // testing that the first id is at most the 'offset' query param. + assert!( + params + .offset + .map(|offset| page[0] <= offset) + .unwrap_or(true), + "bad page offset: {params:?} {page:?}" + ); + // testing that the id decreases. + assert!( + page.as_slice().is_sorted_by(|a, b| a > b), + "bad page order: {page:?}" + ) + } else { + // testing that the first id is at least the 'offset' query param. + assert!( + params + .offset + .map(|offset| page[0] >= offset) + .unwrap_or(true), + "bad page offset: {params:?} {page:?}" + ); + // testing that the id increases. + assert!(page.as_slice().is_sorted(), "bad page order: {page:?}") + } + page } - history } // Get currency from config @@ -452,29 +547,19 @@ pub async fn transfer_routine( } // Pagination test - routine_pagination::<TransferList, _>( - server, - "/taler-wire-gateway/transfers", - |it| { - it.transfers - .into_iter() - .map(|it| *it.row_id as i64) - .collect() - }, - async |server, i| { - server - .post("/taler-wire-gateway/transfer") - .json(&json!({ - "request_uid": HashCode::rand(), - "amount": amount(format!("{currency}:0.0{i}")), - "exchange_base_url": url("http://exchange.taler"), - "wtid": ShortHashCode::rand(), - "credit_account": credit_account, - })) - .await - .assert_ok_json::<TransferResponse>(); - }, - ) + routine_pagination::<TransferList, _>(server, "/taler-wire-gateway/transfers", async |i| { + server + .post("/taler-wire-gateway/transfer") + .json(&json!({ + "request_uid": HashCode::rand(), + "amount": amount(format!("{currency}:0.0{i}")), + "exchange_base_url": url("http://exchange.taler"), + "wtid": ShortHashCode::rand(), + "credit_account": credit_account, + })) + .await + .assert_ok_json::<TransferResponse>(); + }) .await; } } @@ -557,17 +642,11 @@ async fn add_incoming_routine( pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool) { let currency = &get_wire_currency(server).await; - routine_history( + routine_history::<RevenueIncomingHistory, _, _>( server, "/taler-revenue/history", - |it: RevenueIncomingHistory| { - it.incoming_transactions - .into_iter() - .map(|it| *it.row_id as i64) - .collect() - }, 2, - async |server, i| { + async |i| { if i % 2 == 0 || !kyc { server .post("/taler-wire-gateway/admin/add-incoming") @@ -591,7 +670,7 @@ pub async fn revenue_routine(server: &Router, debit_acount: &PaytoURI, kyc: bool } }, 0, - async |_, _| {}, + async |_| {}, ) .await; } @@ -602,21 +681,11 @@ pub async fn admin_add_incoming_routine(server: &Router, debit_acount: &PaytoURI // History // TODO check non taler some are ignored - routine_history( + routine_history::<IncomingHistory, _, _>( server, "/taler-wire-gateway/history/incoming", - |it: IncomingHistory| { - it.incoming_transactions - .into_iter() - .map(|it| match it { - IncomingBankTransaction::Reserve { row_id, .. } - | IncomingBankTransaction::Wad { row_id, .. } - | IncomingBankTransaction::Kyc { row_id, .. } => *row_id as i64, - }) - .collect() - }, 2, - async |server, i| { + async |i| { if i % 2 == 0 || !kyc { server .post("/taler-wire-gateway/admin/add-incoming") @@ -640,7 +709,7 @@ pub async fn admin_add_incoming_routine(server: &Router, debit_acount: &PaytoURI } }, 0, - async |_, _| {}, + async |_| {}, ) .await; // Add incoming reserve @@ -787,7 +856,7 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>, F2: Future<O })) .await .assert_ok_json::<RegistrationResponse>(); - register(&auth_pub1).await; + check_history_in_trigger(server, async || register(&auth_pub1).await).await; check_in(&[Reserve(acc_pub1.clone())]).await; register(&auth_pub1).await; check_in(&[Reserve(acc_pub1.clone()), Bounced]).await; @@ -859,15 +928,18 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>, F2: Future<O })) .await .assert_ok_json::<RegistrationResponse>(); - server - .post("/taler-wire-transfer-gateway/registration") - .json(&json!(req + { - "account_pub": acc_pub4, - "authorization_sig": eddsa_sign(&key_pair1, acc_pub4.as_ref()), - "recurrent": true - })) - .await - .assert_ok_json::<RegistrationResponse>(); + check_history_in_trigger(server, async || { + server + .post("/taler-wire-transfer-gateway/registration") + .json(&json!(req + { + "account_pub": acc_pub4, + "authorization_sig": eddsa_sign(&key_pair1, acc_pub4.as_ref()), + "recurrent": true + })) + .await + .assert_ok_json::<RegistrationResponse>(); + }) + .await; check_in(&[ Reserve(acc_pub1.clone()), Bounced, diff --git a/common/taler-test-utils/src/server.rs b/common/taler-test-utils/src/server.rs @@ -158,7 +158,7 @@ pub struct TestResponse { bytes: Bytes, method: Method, uri: Uri, - status: StatusCode, + pub status: StatusCode, } impl TestResponse { diff --git a/taler-cyclos/src/api.rs b/taler-cyclos/src/api.rs @@ -390,13 +390,7 @@ mod test { routine_pagination::<OutgoingHistory, _>( &server, "/taler-wire-gateway/history/outgoing", - |it| { - it.outgoing_transactions - .into_iter() - .map(|it| *it.row_id as i64) - .collect() - }, - async |_, i| { + async |i| { db::register_tx_out( &mut pool.acquire().await.unwrap(), &db::TxOut { diff --git a/taler-magnet-bank/src/api.rs b/taler-magnet-bank/src/api.rs @@ -364,13 +364,7 @@ mod test { routine_pagination::<OutgoingHistory, _>( &server, "/taler-wire-gateway/history/outgoing", - |it| { - it.outgoing_transactions - .into_iter() - .map(|it| *it.row_id as i64) - .collect() - }, - async |_, i| { + async |i| { let mut conn = pool.acquire().await.unwrap(); let now = Zoned::now().date(); db::register_tx_out(