taler-typescript-core

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

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:
Apackages/taler-harness/src/integrationtests/test-kyc-merchant-ineligible-account.ts | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
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,