taler-typescript-core

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

commit b64e2aab5222dd5f4a92a259773b9bc8d73d2a97
parent ce62ca1b77fcd168d0262b10752f556227c96a08
Author: Florian Dold <dold@taler.net>
Date:   Mon, 27 Jul 2026 21:45:32 +0200

harness: test that an exchange is listed only once in the contract terms

Diffstat:
Apackages/taler-harness/src/integrationtests/test-merchant-exchange-duplicate.ts | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
2 files changed, 192 insertions(+), 0 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-merchant-exchange-duplicate.ts b/packages/taler-harness/src/integrationtests/test-merchant-exchange-duplicate.ts @@ -0,0 +1,190 @@ +/* + 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 { + Duration, + j2s, + PaytoString, + succeedOrThrow, + TalerExchangeHttpClient, + TalerMerchantInstanceHttpClient, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { defaultCoinConfig } from "../harness/denomStructures.js"; +import { createWalletDaemonWithClient } from "../harness/environments.js"; +import { + ExchangeService, + GlobalTestState, + MerchantService, + setupDb, +} from "../harness/harness.js"; + +/** + * Test that an exchange which offers *multiple* bank accounts sharing the + * same wire method is listed only once in the contract terms. + * + * The merchant backend used to iterate over the accounts advertised in the + * exchange's /keys and add the exchange to the contract once per matching + * account, yielding duplicate (byte-identical) entries in "exchanges". + * + * See https://bugs.gnunet.org/view.php?id=11563 + */ +export async function runMerchantExchangeDuplicateTest(t: GlobalTestState) { + // Set up test environment + + const db = await setupDb(t); + + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "wallet", + }); + + const exchange1 = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + exchange1.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS"))); + + const paytoXtb1 = + "payto://x-taler-bank/localhost/exchange?receiver-name=exchange" as PaytoString; + + // Second account of the *same* exchange, using the *same* wire method. + const paytoXtb2 = + "payto://x-taler-bank/localhost/exchange-two?receiver-name=exchange-two" as PaytoString; + + await exchange1.addBankAccount("1", { + accountPaytoUri: paytoXtb1, + wireGatewayApiBaseUrl: "http://localhost:8082/", + wireGatewayAuth: { + type: "basic", + password: "test", + username: "test", + }, + }); + + await exchange1.addBankAccount("2", { + accountPaytoUri: paytoXtb2, + wireGatewayApiBaseUrl: "http://localhost:8082/", + wireGatewayAuth: { + type: "basic", + password: "test", + username: "test", + }, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + httpPort: 8083, + database: db.connStr, + }); + + merchant.addExchange(exchange1); + + await exchange1.start(); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + // Sanity check: the exchange really does advertise two accounts that + // share one wire method, otherwise this test would pass vacuously. + { + const exchangeClient = new TalerExchangeHttpClient(exchange1.baseUrl); + const keys = succeedOrThrow(await exchangeClient.getKeys()); + + console.log(`exchange advertises accounts: ${j2s(keys.accounts)}`); + + const xtbAccounts = keys.accounts.filter((x) => + x.payto_uri.startsWith("payto://x-taler-bank/"), + ); + + t.assertDeepEqual(xtbAccounts.length, 2); + } + + const { accessToken: adminAccessToken } = + await merchant.addInstanceWithWireAccount({ + id: "admin", + name: "Default Instance", + paytoUris: [paytoXtb1], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + const merchApi = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + const ordResp1 = succeedOrThrow( + await merchApi.createOrder(adminAccessToken, { + order: { + amount: "TESTKUDOS:5", + summary: "Test!", + }, + }), + ); + + console.log(j2s(ordResp1)); + + const ordDet1 = succeedOrThrow( + await merchApi.getOrderDetails(adminAccessToken, ordResp1.order_id), + ); + + t.assertDeepEqual(ordDet1.order_status, "unpaid"); + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUriV2, + { + talerPayUri: ordDet1.taler_pay_uri, + }, + ); + + // The order is only claimed once the download task has run. + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: preparePayResult.transactionId, + txState: { + major: TransactionMajorState.Dialog, + minor: TransactionMinorState.Proposed, + }, + }); + + const ordDet2 = succeedOrThrow( + await merchApi.getOrderDetails(adminAccessToken, ordResp1.order_id), + ); + + console.log(j2s(ordDet2)); + + t.assertDeepEqual(ordDet2.order_status, "claimed"); + + const exchanges = ordDet2.contract_terms.exchanges; + + // The exchange must be listed exactly once, even though it has two + // accounts with the wire method used by this order. + const numExch1 = exchanges.filter((x) => x.url === exchange1.baseUrl).length; + t.assertDeepEqual(numExch1, 1); + + // More generally, no exchange may be listed twice. + const urls = exchanges.map((x) => x.url); + t.assertDeepEqual(urls.length, new Set(urls).size); +} + +runMerchantExchangeDuplicateTest.suites = ["merchant"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -113,6 +113,7 @@ import { runMerchantBankBadWireTargetTest } from "./test-merchant-bank-bad-wire- import { runMerchantCategoriesTest } from "./test-merchant-categories.js"; import { runMerchantDepositLargeTest } from "./test-merchant-deposit-large.js"; import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion.js"; +import { runMerchantExchangeDuplicateTest } from "./test-merchant-exchange-duplicate.js"; import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete.js"; import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls.js"; import { runMerchantInstancesTest } from "./test-merchant-instances.js"; @@ -283,6 +284,7 @@ const allTests: TestMainFunction[] = [ runExchangeDepositTest, runExchangeEphemeralTest, runMerchantExchangeConfusionTest, + runMerchantExchangeDuplicateTest, runMerchantInstancesDeleteTest, runMerchantInstancesTest, runMerchantInstancesUrlsTest,