taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-harness/src/integrationtests/test-wallet-progress-token.ts | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
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;