taler-rust

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

commit 91ff47a5fc84ee85c8c54eab7e03b649d9608ef8
parent b73126f1a2ff05f433a8b2558195043fa4c5e580
Author: Antoine A <>
Date:   Thu,  8 Oct 2026 17:48:18 +0200

common: improve test routine

Diffstat:
Mcommon/taler-test-utils/src/routine.rs | 195+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
1 file changed, 122 insertions(+), 73 deletions(-)

diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs @@ -122,28 +122,57 @@ pub async fn routine_pagination<T: Page>( return; } - // Check history is following specs let assert_history = async |args: &str, size: usize| server.get(format!("?{args}")).await.assert_ids::<T>(size); - // Get latest registered id - let latest_id = async || latest_id::<T>(server).await; + let before = latest_id::<T>(server).await; for i in 0..20 { (register.lambda)(i).await; } - let id = latest_id().await; + // Use actual row IDs: earlier tests and ignored transactions can leave gaps + let ids = assert_history(&format!("limit=20&offset={before}"), 20).await; + let reverse: Vec<_> = ids.iter().copied().rev().collect(); + assert_eq!(assert_history("", 20).await, reverse); + assert_eq!(assert_history("limit=-10", 10).await, reverse[..10]); - // default - assert_history("", 20).await; - - // forward range - assert_history("limit=10", 10).await; - assert_history("limit=10&offset=4", 10).await; + // Cursors are exclusive in both directions, including single-row pages. + for limit in [1, 10] { + assert_eq!( + assert_history(&format!("limit={limit}&offset={}", ids[4]), limit).await, + ids[5..5 + limit] + ); + assert_eq!( + assert_history(&format!("limit=-{limit}&offset={}", ids[15]), limit).await, + reverse[5..5 + limit] + ); + } - // backward range - assert_history("limit=-10", 10).await; - assert_history(&format!("limit=-10&{}", id - 4), 10).await; + // Partial pages and exhausted cursors. + assert_eq!( + assert_history(&format!("limit=10&offset={}", ids[18]), 1).await, + ids[19..] + ); + assert_history(&format!("limit=1&offset={}", ids[19]), 0).await; + let first = assert_history("limit=1", 1).await[0]; + assert_history(&format!("limit=-1&offset={first}"), 0).await; + assert_history("limit=-1&offset=0", 0).await; + assert_history(&format!("limit=1&offset={}", i64::MAX), 0).await; + + for args in [ + "limit=0", + "limit=1025", + "limit=-1025", + "limit=-9223372036854775808", + "limit=invalid", + "offset=-1", + "offset=invalid", + ] { + server + .get(format!("?{args}")) + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); + } } pub async fn assert_time<R>(range: std::ops::Range<u128>, task: impl Future<Output = R>) -> R { @@ -167,31 +196,20 @@ pub async fn routine_history<T: Page>( mut register: Tasks<impl AsyncFnMut(usize)>, mut ignore: Tasks<impl AsyncFnMut(usize)>, ) { - // Check history is following specs - macro_rules! assert_history { - ($args:expr, $size:expr) => { - async { - server - .get(&format!("?{}", $args)) - .await - .assert_ids::<T>($size) - } - }; - } - // Get latest registered id - let latest_id = async || assert_history!("limit=-1", 1).await[0]; + let assert_history = async |args: String, size: usize| { + server.get(format!("?{args}")).await.assert_ids::<T>(size) + }; // Check error when no transactions - assert_history!("limit=7".to_owned(), 0).await; + assert_history("limit=7".to_owned(), 0).await; - let mut register_iter = (0..register.len).peekable(); - let mut ignore_iter = (0..ignore.len).peekable(); - while register_iter.peek().is_some() || ignore_iter.peek().is_some() { - if let Some(idx) = register_iter.next() { - (register.lambda)(idx).await + // Interleave visible and ignored transactions to exercise gaps in row IDs. + for idx in 0..register.len.max(ignore.len) { + if idx < register.len { + (register.lambda)(idx).await; } - if let Some(idx) = ignore_iter.next() { - (ignore.lambda)(idx).await + if idx < ignore.len { + (ignore.lambda)(idx).await; } } let nb_register = register.len; @@ -199,43 +217,43 @@ pub async fn routine_history<T: Page>( let nb_total = nb_register + nb_ignore; // Check ignored - assert_history!(format_args!("limit={nb_total}"), nb_register).await; + assert_history(format!("limit={nb_total}"), nb_register).await; // Check skip ignored - assert_history!(format_args!("limit={nb_register}"), nb_register).await; + assert_history(format!("limit={nb_register}"), nb_register).await; // Allow CI scheduling and database delays, but require completion before // the long-poll timeout so broken notification delivery still fails. // Check no polling when we cannot have more transactions assert_time( 0..5000, - assert_history!( - format_args!("limit=-{}&timeout_ms=10000", nb_register + 1), - nb_register + assert_history( + format!("limit=-{}&timeout_ms=10000", nb_register + 1), + nb_register, ), ) .await; // Check no polling when already find transactions even if less than delta assert_time( 0..5000, - assert_history!( - format_args!("limit={}&timeout_ms=10000", nb_register + 1), - nb_register + assert_history( + format!("limit={}&timeout_ms=10000", nb_register + 1), + nb_register, ), ) .await; // Check polling - let id = latest_id().await; + let id = latest_id::<T>(server).await; tokio::join!( // Check polling succeed assert_time( 100..5000, - assert_history!(format_args!("limit=2&offset={id}&timeout_ms=10000"), 1) + assert_history(format!("limit=2&offset={id}&timeout_ms=10000"), 1) ), assert_time( 200..5000, - assert_history!( - format_args!( + assert_history( + format!( "limit=1&offset={}&timeout_ms=200", id as usize + nb_total * 3 ), @@ -250,12 +268,12 @@ pub async fn routine_history<T: Page>( // Test triggers for i in 0..register.len { - let id = latest_id().await; + let id = latest_id::<T>(server).await; tokio::join!( // Check polling succeed assert_time( 100..5000, - assert_history!(format_args!("limit=7&offset={id}&timeout_ms=10000"), 1) + assert_history(format!("limit=7&offset={id}&timeout_ms=10000"), 1) ), async { sleep(Duration::from_millis(100)).await; @@ -265,12 +283,12 @@ pub async fn routine_history<T: Page>( } // Test doesn't trigger - let id = latest_id().await; + let id = latest_id::<T>(server).await; tokio::join!( // Check polling succeed assert_time( 200..5000, - assert_history!(format_args!("limit=7&offset={id}&timeout_ms=200"), 0) + assert_history(format!("limit=7&offset={id}&timeout_ms=200"), 0) ), async { sleep(Duration::from_millis(100)).await; @@ -297,12 +315,9 @@ impl TestResponse { // testing the size is like expected assert_eq!(size, page.len(), "bad page length: {page:?}\n{body:?}"); if params.limit < 0 { - // testing that the first id is at most the 'offset' query param. + // The cursor row must not be repeated on the next page assert!( - params - .offset - .map(|offset| page[0] <= offset) - .unwrap_or(true), + params.offset.map(|offset| page[0] < offset).unwrap_or(true), "bad page offset: {params:?} {page:?}" ); // testing that the id decreases. @@ -311,16 +326,16 @@ impl TestResponse { "bad page order: {page:?}" ) } else { - // testing that the first id is at least the 'offset' query param. + // The cursor row must not be repeated on the next page. assert!( - params - .offset - .map(|offset| page[0] >= offset) - .unwrap_or(true), + 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:?}") + assert!( + page.as_slice().is_sorted_by(|a, b| a < b), + "bad page order: {page:?}" + ) } page } @@ -399,7 +414,9 @@ pub async fn transfer_routine( .get("/transfers?limit=-1") .await .assert_ok_json::<TransferList>(); - let tx = &list.transfers[0]; + let [tx] = list.transfers.as_slice() else { + panic!("expected one transfer, got {list:?}"); + }; assert_eq!(first.row_id, tx.row_id); assert_eq!(default_status, tx.status); assert_eq!(default_amount, tx.amount); @@ -445,14 +462,27 @@ pub async fn transfer_routine( // Check create transfer errors { - // Check request uid reuse - wire_gateway - .post("/transfer") - .json(json!(valid_req + { - "wtid": ShortHashCode::rand() - })) - .await - .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED); + // Changing any of these fields conflicts with the original request UID + for conflicting_req in [ + TransferRequest { + wtid: ShortHashCode::rand(), + ..req.clone() + }, + TransferRequest { + amount: amount(format!("{currency}:43")), + ..req.clone() + }, + TransferRequest { + exchange_base_url: url("https://other-exchange.taler/"), + ..req.clone() + }, + ] { + wire_gateway + .post("/transfer") + .json(conflicting_req) + .await + .assert_error(ErrorCode::BANK_TRANSFER_REQUEST_UID_REUSED); + } // Check wtid reuse wire_gateway .post("/transfer") @@ -471,7 +501,7 @@ pub async fn transfer_routine( .await .assert_error(ErrorCode::GENERIC_CURRENCY_MISMATCH); - // Base Base32 + // Invalid Base32 wire_gateway .post("/transfer") .json(json!(valid_req + { @@ -540,7 +570,7 @@ pub async fn transfer_routine( } // Malformed metadata - for metadata in ["bad_id", "bad id", "bad@id.com", &"A".repeat(41)] { + for metadata in ["", "bad_id", "bad id", "bad@id.com", &"A".repeat(41)] { wire_gateway .post("/transfer") .json(&json!(valid_req + { "metadata": metadata })) @@ -560,16 +590,35 @@ pub async fn transfer_routine( // Check transfer page { - for _ in 0..4 { + // Valid metadata boundaries and all supported punctuation. + for metadata in ["A", &"A".repeat(40), "test-1.2:metadata", "0"] { wire_gateway .post("/transfer") .json(&json!(valid_req + { "request_uid": HashCode::rand(), "wtid": ShortHashCode::rand(), + "metadata": metadata })) .await .assert_ok_json::<TransferResponse>(); } + for status in [ + TransferState::pending, + TransferState::transient_failure, + TransferState::permanent_failure, + TransferState::success, + ] { + if status != default_status { + wire_gateway + .get(format!("/transfers?status={}", status.as_ref())) + .await + .assert_no_content(); + } + } + wire_gateway + .get("/transfers?status=invalid") + .await + .assert_error(ErrorCode::GENERIC_PARAMETER_MALFORMED); { let list = wire_gateway .get("/transfers")