taler-typescript-core

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

commit e27c7c3219d67801ec42ed660ab104914201aed4
parent 8eb5f2d923e3ebb95f6965fdb3cac715a400d0a2
Author: Florian Dold <florian@dold.me>
Date:   Fri, 28 Nov 2025 19:43:58 +0100

harness: new test for v1 contracts repurchase detection

Diffstat:
Apackages/taler-harness/src/integrationtests/test-repurchase-v1.ts | 288+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
2 files changed, 290 insertions(+), 0 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-repurchase-v1.ts b/packages/taler-harness/src/integrationtests/test-repurchase-v1.ts @@ -0,0 +1,288 @@ +/* + This file is part of GNU Taler + (C) 2023 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 { + ConfirmPayResultType, + j2s, + PreparePayResultType, + succeedOrThrow, + TalerCorebankApiClient, + TalerMerchantInstanceHttpClient, + TransactionType, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + useSharedTestkudosEnvironment, + withdrawViaBankV3, +} from "../harness/environments.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; + +/** + * Repurchase detection with v1 contracts. + */ +export async function runRepurchaseV1Test(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bank, exchange, merchant, merchantAdminAccessToken } = + await useSharedTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + const bankClient = new TalerCorebankApiClient(bank.baseUrl); + + await withdrawViaBankV3(t, { + walletClient, + bankClient, + exchange, + amount: "TESTKUDOS:20", + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + const merchantClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + const orderOneResp = succeedOrThrow( + await merchantClient.createOrder(merchantAdminAccessToken, { + order: { + version: 1, + summary: "Buy me", + fulfillment_url: "https://example.com/test", + choices: [ + { + amount: "TESTKUDOS:5", + description: "foo", + }, + { + amount: "TESTKUDOS:2", + description: "bla", + }, + ], + }, + }), + ); + + let orderOneStatus = succeedOrThrow( + await merchantClient.getOrderDetails( + merchantAdminAccessToken, + orderOneResp.order_id, + { + sessionId: "session1", + }, + ), + ); + + t.assertTrue(orderOneStatus.order_status === "unpaid"); + + const preparePayOneResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderOneStatus.taler_pay_uri, + }, + ); + + t.assertTrue( + preparePayOneResult.status === PreparePayResultType.PaymentPossible, + ); + + const confirmPayResp = await walletClient.call( + WalletApiOperation.ConfirmPay, + { + transactionId: preparePayOneResult.transactionId, + choiceIndex: 0, + }, + ); + + t.assertTrue(confirmPayResp.type === ConfirmPayResultType.Done); + + const orderTwoResp = succeedOrThrow( + await merchantClient.createOrder(merchantAdminAccessToken, { + order: { + version: 1, + summary: "Buy me", + fulfillment_url: "https://example.com/test", + choices: [ + { + amount: "TESTKUDOS:5", + description: "foo", + }, + { + amount: "TESTKUDOS:2", + description: "bla", + }, + ], + }, + }), + ); + + let orderTwoStatus = succeedOrThrow( + await merchantClient.getOrderDetails( + merchantAdminAccessToken, + orderTwoResp.order_id, + { + sessionId: "session2", + }, + ), + ); + + t.assertTrue(orderTwoStatus.order_status === "unpaid"); + + const orderLongpollUrl = new URL( + `orders/${orderTwoResp.order_id}`, + merchant.makeInstanceBaseUrl(), + ); + if (orderTwoResp.token) { + orderLongpollUrl.searchParams.set("token", orderTwoResp.token); + } + orderLongpollUrl.searchParams.set("timeout_ms", "60000"); + orderLongpollUrl.searchParams.set("session_id", "session2"); + + const longpollPromise = harnessHttpLib.fetch(orderLongpollUrl.href); + + const preparePayTwoResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderTwoStatus.taler_pay_uri, + }, + ); + + // Repurchase should be detected + t.assertTrue( + preparePayTwoResult.status === PreparePayResultType.AlreadyConfirmed, + ); + + t.logStep("start-wait-longpoll-promise"); + await longpollPromise; + t.logStep("done-wait-longpoll-promise"); + + { + const resp = succeedOrThrow( + await merchantClient.getOrderDetails( + merchantAdminAccessToken, + orderTwoResp.order_id, + { + sessionId: "session2", + }, + ), + ); + console.log(resp); + } + + // Order three + + const orderThreeResp = succeedOrThrow( + await merchantClient.createOrder(merchantAdminAccessToken, { + order: { + version: 1, + summary: "Buy me", + fulfillment_url: "https://example.com/test", + choices: [ + { + amount: "TESTKUDOS:5", + description: "foo", + }, + { + amount: "TESTKUDOS:2", + description: "bla", + }, + ], + }, + }), + ); + + let orderThreeStatus = succeedOrThrow( + await merchantClient.getOrderDetails( + merchantAdminAccessToken, + orderThreeResp.order_id, + { + // Go back to session1 + sessionId: "session1", + }, + ), + ); + + t.assertTrue(orderThreeStatus.order_status === "unpaid"); + + const preparePayThreeResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderThreeStatus.taler_pay_uri, + }, + ); + + // Repurchase should be detected + t.assertTrue( + preparePayThreeResult.status === PreparePayResultType.AlreadyConfirmed, + ); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + { + const txnsNormal = await walletClient.call( + WalletApiOperation.GetTransactionsV2, + {}, + ); + + console.log(j2s(txnsNormal)); + + t.assertDeepEqual(txnsNormal.transactions.length, 2); + } + + // The list of all transactions should also include repurchases. + { + const txnsAll = await walletClient.call( + WalletApiOperation.GetTransactionsV2, + { + includeAll: true, + }, + ); + + console.log(j2s(txnsAll)); + + const numPayments = txnsAll.transactions.filter( + (x) => x.type === TransactionType.Payment, + ).length; + + t.assertDeepEqual(numPayments, 3); + } + + // Test that deleting the original transaction also deletes the repurchase transaction. + + await walletClient.call(WalletApiOperation.DeleteTransaction, { + transactionId: preparePayOneResult.transactionId, + }); + + { + const txnsAll = await walletClient.call( + WalletApiOperation.GetTransactionsV2, + { + includeAll: true, + }, + ); + + const numPayments = txnsAll.transactions.filter( + (x) => x.type === TransactionType.Payment, + ).length; + + t.assertDeepEqual(numPayments, 0); + } +} + +runRepurchaseV1Test.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -143,6 +143,7 @@ import { runRefundAutoTest } from "./test-refund-auto.js"; import { runRefundGoneTest } from "./test-refund-gone.js"; import { runRefundIncrementalTest } from "./test-refund-incremental.js"; import { runRefundTest } from "./test-refund.js"; +import { runRepurchaseV1Test } from "./test-repurchase-v1.js"; import { runRepurchaseTest } from "./test-repurchase.js"; import { runRevocationTest } from "./test-revocation.js"; import { runSimplePaymentTest } from "./test-simple-payment.js"; @@ -405,6 +406,7 @@ const allTests: TestMainFunction[] = [ runTopsAmlPdfTest, runMerchantWireTest, runWalletExchangeFeaturesTest, + runRepurchaseV1Test, ]; export interface TestRunSpec {