commit 91ff47a5fc84ee85c8c54eab7e03b649d9608ef8
parent b73126f1a2ff05f433a8b2558195043fa4c5e580
Author: Antoine A <>
Date: Thu, 8 Oct 2026 17:48:18 +0200
common: improve test routine
Diffstat:
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")