commit 38fe6df9acc5cd88f740591292c8f5189d7f1362
parent c2e11ad42fef02ae9e65d298807eba7a48b184af
Author: Florian Dold <dold@taler.net>
Date: Wed, 2 Sep 2026 16:17:10 +0200
taler-harness: test unsupported merchant account KYC status
Diffstat:
2 files changed, 155 insertions(+), 0 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-ineligible-account.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-ineligible-account.ts
@@ -0,0 +1,153 @@
+/*
+ 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/>
+ */
+
+/**
+ * A merchant bank account that the exchange will not pay to must be reported
+ * as "unsupported-account", not as "exchange-unreachable".
+ *
+ * Regression test for the case where taler-merchant-kyccheck learns the
+ * exchange keys *after* it discovered the accounts -- which is what happens on
+ * a first-ever start, where taler-merchant-exchangekeyupdate still has to
+ * download /keys. That path used to start inquiries only for eligible
+ * accounts and silently skip the ineligible ones, so no merchant_kyc row was
+ * ever written for them and GET /private/kyc fell back to its
+ * "exchange-unreachable" placeholder forever -- with a hardcoded
+ * "no_keys": true and "exchange_http_status": 0, which made it look like the
+ * exchange was down when it was in fact perfectly reachable.
+ *
+ * @author Christian Grothoff
+ */
+
+/**
+ * Imports.
+ */
+import {
+ codecForAccountKycRedirects,
+ j2s,
+ Logger,
+ MerchantAccountKycStatus,
+ PaytoString,
+} from "@gnu-taler/taler-util";
+import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import { createKycTestkudosEnvironmentFull } from "../harness/environments.js";
+import {
+ delayMs,
+ GlobalTestState,
+ harnessHttpLib,
+ runCommand,
+} from "../harness/harness.js";
+
+const logger = new Logger(`test-kyc-merchant-ineligible-account.ts`);
+
+const STATUS_TIMEOUT_MS = 30000;
+
+/**
+ * An IBAN account. The test exchange only pays to "x-taler-bank"
+ * accounts, so this one is not eligible and the KYC checker must record
+ * that fact rather than stay silent about it.
+ */
+const IBAN_PAYTO =
+ "payto://iban/CH1000768300161323105?receiver-name=HERNANI%20MARQUES%20MADEIRA" as PaytoString;
+
+export async function runKycMerchantIneligibleAccountTest(t: GlobalTestState) {
+ const { merchant, merchantAdminAccessToken, commonDb } =
+ await createKycTestkudosEnvironmentFull(t, {
+ adjustExchangeConfig(config) {
+ config.setString("exchange", "enable_kyc", "yes");
+ },
+ });
+
+ // An instance whose bank account the exchange will not accept.
+ const { accessToken } = await merchant.addInstanceWithWireAccount(
+ {
+ id: "ibaninst",
+ name: "Instance with an IBAN account",
+ paytoUris: [IBAN_PAYTO],
+ },
+ { adminAccessToken: merchantAdminAccessToken },
+ );
+
+ const baseUrl = merchant.makeInstanceBaseUrl("ibaninst");
+ const headers = { Authorization: `Bearer ${accessToken}` };
+
+ // Force the ordering that triggers the bug: take the merchant down,
+ // forget the exchange keys, and bring it back up. taler-merchant-kyccheck
+ // then starts with no keys in the database, discovers the accounts first,
+ // and only afterwards learns about the exchange -- which is the code path
+ // that used to drop ineligible accounts on the floor.
+ logger.info(`restarting merchant without cached exchange keys`);
+ await merchant.stop();
+ // Forget the exchange keys *and* every KYC status recorded so far, so
+ // that the only thing that can re-create a merchant_kyc row after the
+ // restart is the "exchange became known after the accounts" path.
+ await runCommand(t, "forget-exchange-keys", "psql", [
+ commonDb.connStr,
+ "-v",
+ "ON_ERROR_STOP=1",
+ "-c",
+ "DELETE FROM merchant.merchant_exchange_keys;",
+ "-c",
+ `DO $$
+ DECLARE r RECORD;
+ BEGIN
+ FOR r IN SELECT nspname FROM pg_namespace
+ WHERE nspname LIKE 'merchant\\_instance\\_%'
+ LOOP
+ EXECUTE format('DELETE FROM %I.merchant_kyc', r.nspname);
+ END LOOP;
+ END $$;`,
+ ]);
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ const deadline = Date.now() + STATUS_TIMEOUT_MS;
+ let lastStatus: string | undefined = undefined;
+
+ while (Date.now() < deadline) {
+ const resp = await harnessHttpLib.fetch(
+ new URL("private/kyc", baseUrl).href,
+ { headers },
+ );
+ if (resp.status === 200) {
+ const parsed = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForAccountKycRedirects(),
+ );
+ t.assertDeepEqual(parsed.kyc_data.length, 1);
+ lastStatus = parsed.kyc_data[0].status;
+ if (lastStatus !== MerchantAccountKycStatus.EXCHANGE_UNREACHABLE) {
+ logger.info(`got KYC status: ${j2s(parsed)}`);
+ // The exchange will not pay to this account; the merchant must
+ // say so instead of blaming reachability.
+ t.assertDeepEqual(
+ lastStatus,
+ MerchantAccountKycStatus.UNSUPPORTED_ACCOUNT,
+ );
+ return;
+ }
+ logger.info(`still "exchange-unreachable", waiting ...`);
+ }
+ await delayMs(500);
+ }
+
+ throw Error(
+ "BUG REPRODUCED: an account the exchange does not support is reported " +
+ `as "exchange-unreachable" forever (last status: ${lastStatus}); ` +
+ "taler-merchant-kyccheck never wrote a merchant_kyc row for it",
+ );
+}
+
+runKycMerchantIneligibleAccountTest.suites = ["merchant", "kyc"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -98,6 +98,7 @@ import { runKycMerchantAggregateTest } from "./test-kyc-merchant-aggregate.js";
import { runKycMerchantDepositFormTest } from "./test-kyc-merchant-deposit-form.js";
import { runKycMerchantDepositRewriteTest } from "./test-kyc-merchant-deposit-rewrite.js";
import { runKycMerchantDepositTest } from "./test-kyc-merchant-deposit.js";
+import { runKycMerchantIneligibleAccountTest } from "./test-kyc-merchant-ineligible-account.js";
import { runKycMerchantWalletReuseTest } from "./test-kyc-merchant-wallet-reuse.js";
import { runKycNewMeasureTest } from "./test-kyc-new-measure.js";
import { runKycNewMeasuresProgTest } from "./test-kyc-new-measures-prog.js";
@@ -431,6 +432,7 @@ const allTests: TestMainFunction[] = [
runKycMerchantDepositTest,
runKycMerchantDepositRewriteTest,
runKycMerchantAggregateTest,
+ runKycMerchantIneligibleAccountTest,
runAccountRestrictionsTest,
runRepurchaseTest,
runWalletTransactionsTest,