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:
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,