taler-typescript-core

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

commit 374a4e47e9b110f6b364efe92340dce502f93ba1
parent 7f966ae5de07fabd09f9cfcf9e0d6309a378063e
Author: Florian Dold <dold@taler.net>
Date:   Wed, 29 Jul 2026 15:59:29 +0200

harness: check that resuming the application does not skip the claim backoff

Issue: https://bugs.taler.net/n/11087

Diffstat:
Apackages/taler-harness/src/integrationtests/test-claim-retry-backoff.ts | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
2 files changed, 160 insertions(+), 0 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-claim-retry-backoff.ts b/packages/taler-harness/src/integrationtests/test-claim-retry-backoff.ts @@ -0,0 +1,158 @@ +/* + This file is part of GNU Taler + (C) 2026 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + NotificationType, + succeedOrThrow, + TalerMerchantInstanceHttpClient, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + createSimpleTestkudosEnvironmentV3, + createWalletDaemonWithClient, +} from "../harness/environments.js"; +import { delayMs, GlobalTestState } from "../harness/harness.js"; + +/** + * Check that the backoff of a failing claim survives being woken up. + * + * Claiming an order is the one request the wallet makes before the user has + * agreed to anything, so a merchant that answers it with an error must not be + * asked again right away. The backoff for that lives in the database, and a + * client that hints that the application resumed - which mobile wallets do + * whenever they come to the foreground - restarts every task, so it is the + * shortest path to retrying without any backoff at all. + */ +export async function runClaimRetryBackoffTest(t: GlobalTestState) { + const { merchant, merchantAdminAccessToken } = + await createSimpleTestkudosEnvironmentV3(t); + + // Each failed attempt is reported as a transition carrying the error, so the + // notifications the UI already listens to say how often the wallet tried. + const failedAttempts: { transactionId: string; when: number }[] = []; + + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "w-claim-backoff", + config: { + testing: { + devModeActive: true, + }, + }, + handleNotification(wn) { + if ( + wn.type === NotificationType.TransactionStateTransition && + wn.errorInfo != null + ) { + failedAttempts.push({ + transactionId: wn.transactionId, + when: Date.now(), + }); + } + }, + }); + + const merchantClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + const orderResp = succeedOrThrow( + await merchantClient.createOrder(merchantAdminAccessToken, { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }, + }), + ); + const orderStatus = succeedOrThrow( + await merchantClient.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), + ); + t.assertTrue(orderStatus.order_status === "unpaid"); + + await walletClient.call(WalletApiOperation.ApplyDevExperiment, { + devExperimentUri: "taler://dev-experiment/block-claim-response", + }); + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUriV2, + { + talerPayUri: orderStatus.taler_pay_uri, + }, + ); + + function claimAttempts(): number[] { + return failedAttempts + .filter((x) => x.transactionId === preparePayResult.transactionId) + .map((x) => x.when); + } + + // Let the wallet fail often enough for the delay between attempts to be a + // good few seconds, so that the assertion below has room to run inside it. + while (claimAttempts().length < 5) { + await delayMs(200); + } + + // The wallet just waited this long of its own accord, and the delay it uses + // grows, so nothing may happen before that much time has passed again. Read + // off what the wallet did rather than assuming a particular curve. + const seen = claimAttempts(); + const lastAttempt = seen[seen.length - 1]; + const lastDelay = lastAttempt - seen[seen.length - 2]; + const attemptsBefore = seen.length; + + // Every one of these used to produce another claim. + let hints = 0; + while (Date.now() < lastAttempt + lastDelay) { + await walletClient.call(WalletApiOperation.HintApplicationResumed, {}); + hints++; + await delayMs(100); + } + t.assertTrue(hints > 0); + t.assertDeepEqual(claimAttempts().length, attemptsBefore); + + // Backing off is not the same as giving up: the wallet gets there on its own + // once the delay it stored has passed. + while (claimAttempts().length === attemptsBefore) { + await delayMs(200); + } + + // An explicit retry is not a wakeup and still takes effect at once. + await walletClient.call(WalletApiOperation.ApplyDevExperiment, { + devExperimentUri: "taler://dev-experiment/block-claim-response?val=0", + }); + await walletClient.call(WalletApiOperation.RetryTransaction, { + transactionId: preparePayResult.transactionId, + }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: preparePayResult.transactionId, + txState: { + major: TransactionMajorState.Dialog, + minor: TransactionMinorState.Proposed, + }, + }); + + await t.shutdown(); +} + +runClaimRetryBackoffTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -42,6 +42,7 @@ import { runBalanceProspectiveTest } from "./test-balance-prospective.js"; import { runBankApiTest } from "./test-bank-api.js"; import { runBankWopTest } from "./test-bank-wop.js"; import { runClaimLoopTest } from "./test-claim-loop.js"; +import { runClaimRetryBackoffTest } from "./test-claim-retry-backoff.js"; import { runClauseSchnorrTest } from "./test-clause-schnorr.js"; import { runCoinselLegacy2024Test } from "./test-coinsel-legacy-2024.js"; import { runCurrencyScopeSeparationTest } from "./test-currency-scope-separation.js"; @@ -276,6 +277,7 @@ interface TestMainFunction { const allTests: TestMainFunction[] = [ runBankApiTest, runClaimLoopTest, + runClaimRetryBackoffTest, runClauseSchnorrTest, runDenomRevokedBalanceTest, runDenomUnofferedTest,