commit 54e25259f70a3270be2c62a8e48d30e8406b27cd
parent 0c769ec41737d064cec85440434976954a3dadcc
Author: Florian Dold <dold@taler.net>
Date: Wed, 4 Nov 2026 00:38:22 +0100
taler-harness: test demo withdrawal retries and cancellation
Issue: https://bugs.gnunet.org/view?id=11825
Diffstat:
1 file changed, 106 insertions(+), 2 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-progress-token.ts b/packages/taler-harness/src/integrationtests/test-wallet-progress-token.ts
@@ -333,9 +333,9 @@ export async function runWalletProgressTokenTest(t: GlobalTestState) {
file: "wallet-w-progress-stderr.log",
level: "WARNING",
message: "reserve long-poll failed while waiting for bank confirmation: {",
- maxOccurrences: 4,
+ maxOccurrences: 6,
justification:
- "The test injects HTTP 500 responses until the withdrawal has retried twice, then explicitly retries it once before restoring the network.",
+ "The transaction-wait and demo-cash cases inject HTTP 500 responses during bank confirmation polling.",
});
await walletClient.call(WalletApiOperation.ApplyDevExperiment, {
devExperimentUri: "taler://dev-experiment/start-tc?fake_500=1",
@@ -486,6 +486,110 @@ export async function runWalletProgressTokenTest(t: GlobalTestState) {
progressErrorsFor("tok-wait-retry").length,
errorCountAtDone,
);
+
+ for (const message of [
+ `unexpected status 500 from POST ${new URL("accounts", corebank.baseUrl).href}`,
+ "{",
+ ]) {
+ t.allowLog({
+ file: "wallet-w-progress-stderr.log",
+ level: "ERROR",
+ message,
+ maxOccurrences: 3,
+ justification:
+ "Demo cash deliberately receives two failed registration responses and one more before cancellation; the bank client logs each status and response body.",
+ });
+ }
+
+ // Demo cash uses the same token across account registration, withdrawal
+ // preparation, the transaction wait, and bank confirmation. Each endpoint
+ // fails twice. A lost confirmation response must be recovered by checking
+ // the bank status, since repeating confirmation can reject a duplicate reserve.
+ await walletClient.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-tc?fake_500_count=2",
+ });
+ const demoCash = await walletClient.call(
+ WalletApiOperation.WithdrawTestBalance,
+ {
+ amount: "TESTKUDOS:5",
+ corebankApiBaseUrl: corebank.baseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
+ progressToken: "tok-demo-cash",
+ },
+ );
+ await waitForNotified(
+ "demo cash errors and completion",
+ () =>
+ progressErrorsFor("tok-demo-cash").length >= 6 &&
+ phasesFor("tok-demo-cash").some(
+ (n) =>
+ n.type === NotificationType.RequestProgressPhase &&
+ n.phase === "done",
+ ),
+ );
+ t.assertTrue(
+ progressErrorsFor("tok-demo-cash").every(
+ (n) =>
+ n.type === NotificationType.RequestProgressError &&
+ n.operation === "withdrawTestBalance",
+ ),
+ );
+ await walletClient.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-tc",
+ });
+ t.assertTrue(
+ progressErrorsFor("tok-demo-cash").some(
+ (n) =>
+ n.type === NotificationType.RequestProgressError &&
+ "requestUrl" in n.error &&
+ typeof n.error.requestUrl === "string" &&
+ n.error.requestUrl.endsWith("/confirm"),
+ ),
+ "demo cash must report a failed confirmation response before recovering",
+ );
+ await walletClient.call(WalletApiOperation.WaitTransactionState, {
+ transactionId: demoCash.transactionId,
+ txState: "final",
+ timeout: { seconds: 30 },
+ });
+
+ await walletClient.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/start-tc?fake_500=1",
+ });
+ const demoCancelled = walletClient
+ .call(WalletApiOperation.WithdrawTestBalance, {
+ amount: "TESTKUDOS:5",
+ corebankApiBaseUrl: corebank.baseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
+ progressToken: "tok-demo-cancel",
+ })
+ .then(
+ () => undefined,
+ (e) => e,
+ );
+ await waitForNotified(
+ "demo cash retry error",
+ () => progressErrorsFor("tok-demo-cancel").length > 0,
+ );
+ await walletClient.call(WalletApiOperation.CancelProgressToken, {
+ operation: "withdrawTestBalance",
+ progressToken: "tok-demo-cancel",
+ });
+ const demoCancelOutcome = await Promise.race([
+ demoCancelled,
+ waitMs(2000).then(() => cancelTimeout),
+ ]);
+ t.assertTrue(demoCancelOutcome instanceof TalerError);
+ t.assertDeepEqual(
+ (demoCancelOutcome as TalerError).errorDetail.code,
+ TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED,
+ );
+ await walletClient.call(WalletApiOperation.ApplyDevExperiment, {
+ devExperimentUri: "taler://dev-experiment/stop-tc",
+ });
}
runWalletProgressTokenTest.suites = ["wallet"];
+
+// Includes real retry backoffs across the full demo-cash bank flow.
+runWalletProgressTokenTest.timeoutMs = 120_000;