taler-typescript-core

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

commit 0394aa0cd076a17e5c1cf983bc9a710b787bfc7e
parent a033009111114db0394e675ddb783ebb70eba557
Author: Florian Dold <dold@taler.net>
Date:   Wed, 29 Jul 2026 12:47:01 +0200

harness: check that a deleted order settles the payment transaction

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

Diffstat:
Apackages/taler-harness/src/integrationtests/test-payment-order-gone.ts | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
2 files changed, 201 insertions(+), 0 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-payment-order-gone.ts b/packages/taler-harness/src/integrationtests/test-payment-order-gone.ts @@ -0,0 +1,199 @@ +/* + 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 { + AmountString, + ConfirmPayResultType, + succeedOrThrow, + TalerMerchantInstanceHttpClient, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, +} from "../harness/environments.js"; +import { GlobalTestState } from "../harness/harness.js"; + +/** + * Once the merchant has deleted an order, it answers every request about it + * with a 404. The wallet must take that as final and stop asking: a wallet + * that keeps polling for its old orders puts load on the merchant backend + * that grows with the number of wallets, and that no merchant can shed. + */ +export async function runPaymentOrderGoneTest(t: GlobalTestState) { + const { + walletClient, + bankClient, + exchange, + merchant, + merchantAdminAccessToken, + } = await createSimpleTestkudosEnvironmentV3(t, undefined, { + // The merchant keeps a paid order around for its legal preservation + // period and refuses to delete it before that. Cut the period short, so + // that the test can produce the order the merchant no longer has. + additionalMerchantConfig(m) { + m.modifyConfig(async (cfg) => { + cfg.setString("merchant", "LEGAL_PRESERVATION", "0 s"); + }); + }, + }); + + const merchantClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + await withdrawViaBankV3(t, { + walletClient, + bankClient, + exchange, + amount: "TESTKUDOS:20", + }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + async function createOrder(amount: AmountString): Promise<{ + orderId: string; + talerPayUri: string; + }> { + const orderResp = succeedOrThrow( + await merchantClient.createOrder(merchantAdminAccessToken, { + order: { + summary: "Buy me!", + amount, + fulfillment_url: "taler://fulfillment-success/thx", + }, + }), + ); + const orderStatus = succeedOrThrow( + await merchantClient.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), + ); + t.assertTrue(orderStatus.order_status === "unpaid"); + return { + orderId: orderResp.order_id, + talerPayUri: orderStatus.taler_pay_uri, + }; + } + + t.logStep("setup-done"); + + // Case 1: the order is deleted after it was paid, and the wallet is then + // asked to check for a refund. The payment stays valid, so the transaction + // goes back to being done instead of querying the merchant forever. + { + const order = await createOrder("TESTKUDOS:5" as AmountString); + + const prep = await walletClient.call( + WalletApiOperation.PreparePayForUriV2, + { talerPayUri: order.talerPayUri }, + ); + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: prep.transactionId, + txState: { + major: TransactionMajorState.Dialog, + minor: TransactionMinorState.Proposed, + }, + }); + const confirmResp = await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: prep.transactionId, + choiceIndex: 0, + }); + t.assertTrue(confirmResp.type === ConfirmPayResultType.Done); + + succeedOrThrow( + await merchantClient.deleteOrder( + merchantAdminAccessToken, + order.orderId, + true, + ), + ); + + t.logStep("paid-order-deleted"); + + await walletClient.call(WalletApiOperation.StartRefundQuery, { + transactionId: prep.transactionId, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: prep.transactionId, + txState: { + major: TransactionMajorState.Done, + }, + }); + + const tx = await walletClient.call(WalletApiOperation.GetTransactionById, { + transactionId: prep.transactionId, + }); + t.assertTrue(tx.type === "payment"); + t.assertTrue(tx.refundQueryActive === false); + + t.logStep("refund-query-given-up"); + } + + // Case 2: the order is deleted while a shared proposal is waiting for + // another wallet to pay it. Nobody can pay a deleted order, so the wallet + // must stop watching it. + { + const order = await createOrder("TESTKUDOS:5" as AmountString); + + const prep = await walletClient.call( + WalletApiOperation.PreparePayForUriV2, + { talerPayUri: order.talerPayUri }, + ); + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: prep.transactionId, + txState: { + major: TransactionMajorState.Dialog, + minor: TransactionMinorState.Proposed, + }, + }); + + succeedOrThrow( + await merchantClient.deleteOrder( + merchantAdminAccessToken, + order.orderId, + false, + ), + ); + + t.logStep("shared-order-deleted"); + + await walletClient.call(WalletApiOperation.SharePayment, { + merchantBaseUrl: merchant.makeInstanceBaseUrl(), + orderId: order.orderId, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: prep.transactionId, + txState: { + major: TransactionMajorState.Aborted, + }, + }); + + t.logStep("shared-proposal-given-up"); + } + + // No task may be left polling the merchant for either order. + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); +} + +runPaymentOrderGoneTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -150,6 +150,7 @@ import { runPaymentExpiredTest } from "./test-payment-expired.js"; import { runPaymentForgettableTest } from "./test-payment-forgettable.js"; import { runPaymentIdempotencyTest } from "./test-payment-idempotency.js"; import { runPaymentMultipleTest } from "./test-payment-multiple.js"; +import { runPaymentOrderGoneTest } from "./test-payment-order-gone.js"; import { runPaymentShareIdempotencyTest } from "./test-payment-share-idempotency.js"; import { runPaymentShareTest } from "./test-payment-share.js"; import { runPaymentTemplateTest } from "./test-payment-template.js"; @@ -305,6 +306,7 @@ const allTests: TestMainFunction[] = [ runPaymentForgettableTest, runPaymentIdempotencyTest, runPaymentMultipleTest, + runPaymentOrderGoneTest, runPaymentTest, runPaymentShareTest, runPaymentShareIdempotencyTest,