commit ac60a4467b5eaa8f8c431ee9c32b29a4bb290db2
parent 17b1149a1a781018491cecd000ce15a669ce9a67
Author: Florian Dold <florian@dold.me>
Date: Fri, 26 Jun 2026 01:50:24 +0200
harness: split up TOPS / short wire transfer subject tests, add merchant test
Diffstat:
6 files changed, 346 insertions(+), 2 deletions(-)
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
@@ -899,6 +899,11 @@ export interface NexusBankAccountInfo {
iban: string;
name: string;
bic: string;
+ /**
+ * QR IBAN of a QR virtual bank account linked to the configured
+ * bank account that can be used for QR BILL.
+ */
+ qrIban?: string;
}
export class LibeufinNexusService {
@@ -950,6 +955,9 @@ export class LibeufinNexusService {
config.setString("nexus-ebics", "iban", bi.iban);
config.setString("nexus-ebics", "bic", bi.bic);
config.setString("nexus-ebics", "name", bi.name);
+ if (bi.qrIban) {
+ config.setString("nexus-ebics", "qr_iban", bi.qrIban);
+ }
}
const cfgFilename = testDir + "/nexus.conf";
config.writeTo(cfgFilename, { excludeDefaults: true });
@@ -1016,6 +1024,10 @@ export class LibeufinNexusService {
get wireGatewayApiBaseUrl(): string {
return `http://localhost:${this.bc.httpPort}/taler-wire-gateway/`;
}
+
+ get preparedTransferApiBaseUrl(): string {
+ return `http://localhost:${this.bc.httpPort}/taler-prepared-transfer/`;
+ }
}
/**
diff --git a/packages/taler-harness/src/harness/tops.ts b/packages/taler-harness/src/harness/tops.ts
@@ -507,7 +507,7 @@ FALLBACK = freeze-investigate
`;
-const topsProvidersTestConf = `
+export const topsProvidersTestConf = `
[kyc-provider-postal-challenger]
LOGIC = oauth2
KYC_OAUTH2_VALIDITY = 2 years
diff --git a/packages/taler-harness/src/integrationtests/test-tops-merchant-kycauths.ts b/packages/taler-harness/src/integrationtests/test-tops-merchant-kycauths.ts
@@ -0,0 +1,165 @@
+/*
+ This file is part of GNU Taler
+ (C) 2025 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 {
+ IbanString,
+ j2s,
+ Logger,
+ MerchantAccountKycStatus,
+ Paytos,
+ TalerMerchantInstanceHttpClient,
+ TalerProtocolDuration,
+} from "@gnu-taler/taler-util";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import { startFakeChallenger } from "../harness/fake-challenger.js";
+import {
+ ExchangeService,
+ GlobalTestState,
+ LibeufinNexusService,
+ MerchantService,
+ NexusBankAccountInfo,
+ setupDb,
+} from "../harness/harness.js";
+import { topsKycRulesConf, topsProvidersTestConf } from "../harness/tops.js";
+
+const logger = new Logger("test-tops-merchant-kycauths.ts");
+
+/**
+ * Test short wire transfers in an exchange setup that simulates
+ * the Taler Operations CH exchange, using libeufin-nexus
+ * as the taler wire gateway API.
+ */
+export async function runTopsMerchantKycauthsTest(t: GlobalTestState) {
+ const db = await setupDb(t);
+
+ const challenger = await startFakeChallenger({
+ port: 6001,
+ addressType: "postal-ch",
+ });
+
+ let coinConfig: CoinConfig[];
+ coinConfig = defaultCoinConfig.map((x) => x("CHF"));
+
+ const bankAccountInfo: NexusBankAccountInfo = {
+ // Random IBAN generated via "libeufin-nexus testing iban gen --country CH"
+ iban: "CH7347363QVFHHFR8BWWB",
+ // PostFinance BIC
+ bic: "POFICHBEXXX",
+ name: "Harness Test Exchange",
+ qrIban: "CH1130000001166556117",
+ };
+
+ const nexus = await LibeufinNexusService.create(t, {
+ currency: "CHF",
+ database: db.connStr,
+ httpPort: 8085,
+ bankAccountInfo,
+ });
+
+ const exchangePayto = Paytos.toFullString(
+ Paytos.createIban(bankAccountInfo.qrIban as IbanString, undefined, {
+ "receiver-name": bankAccountInfo.name,
+ }),
+ );
+
+ await nexus.dbinit();
+
+ await nexus.start();
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "CHF",
+ httpPort: 8081,
+ database: db.connStr,
+ // FIXME: This is a terrible way to configure the exchange, should be moved into config.
+ extraProcEnv: {
+ EXCHANGE_AML_PROGRAM_TOPS_ENABLE_DEPOSITS_TOS_NAME: "v1",
+ EXCHANGE_AML_PROGRAM_TOPS_ENABLE_DEPOSITS_THRESHOLD: "CHF:0",
+ EXCHANGE_AML_PROGRAM_TOPS_POSTAL_CHECK_COUNTRY_REGEX: "ch|CH|Ch",
+ },
+ });
+
+ exchange.addBankAccount("nexusacct", {
+ accountPaytoUri: exchangePayto,
+ wireGatewayApiBaseUrl: nexus.wireGatewayApiBaseUrl,
+ wireGatewayAuth: {
+ type: "basic",
+ username: "exchange-test",
+ password: "exchange-test",
+ },
+ preparedTransferUrl: nexus.preparedTransferApiBaseUrl,
+ });
+
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.modifyConfig(async (config) => {
+ config.loadFromString(topsKycRulesConf);
+ config.loadFromString(topsProvidersTestConf);
+ });
+
+ await exchange.start();
+
+ const merchantIban = "CH4810303KL83V1QZ05T7";
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+
+ const merchantPayto = Paytos.toFullString(
+ Paytos.createIban(merchantIban as IbanString, undefined, {
+ "receiver-name": "Merchant Test",
+ }),
+ );
+
+ const { accessToken: merchantAdminAccessToken } =
+ await merchant.addInstanceWithWireAccount({
+ id: "admin",
+ name: "Default Instance",
+ paytoUris: [merchantPayto],
+ defaultWireTransferDelay: TalerProtocolDuration.fromSpec({ minutes: 1 }),
+ });
+
+ const merchantClient = new TalerMerchantInstanceHttpClient(
+ merchant.makeInstanceBaseUrl(),
+ );
+ // Do KYC auth transfer
+
+ const kycStatus = await merchantClient.getCurrentInstanceKycStatus(
+ merchantAdminAccessToken,
+ {
+ longpoll: {
+ type: "state-enter",
+ status: MerchantAccountKycStatus.KYC_WIRE_REQUIRED,
+ timeout: 10000,
+ },
+ },
+ );
+ t.assertTrue(kycStatus.type === "ok");
+ console.log(`kyc status: ${j2s(kycStatus)}`);
+
+ t.assertDeepEqual(kycStatus.body.kyc_data[0].status, "kyc-wire-required");
+}
+
+runTopsMerchantKycauthsTest.suites = ["tops", "libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-tops-nexus-basic.ts b/packages/taler-harness/src/integrationtests/test-tops-nexus-basic.ts
@@ -41,9 +41,12 @@ import {
const logger = new Logger("test-tops-nexus-swt.ts");
/**
- * Test short wire transfers in an exchange setup that simulates
+ * Test normal wire transfers in an exchange setup that simulates
* the Taler Operations CH exchange, using libeufin-nexus
* as the taler wire gateway API.
+ *
+ * In this test, the TOPS environment is simplified and does
+ * not configure any KYC checks.
*/
export async function runTopsNexusBasicTest(t: GlobalTestState) {
const db = await setupDb(t);
diff --git a/packages/taler-harness/src/integrationtests/test-tops-nexus-swt.ts b/packages/taler-harness/src/integrationtests/test-tops-nexus-swt.ts
@@ -0,0 +1,160 @@
+/*
+ This file is part of GNU Taler
+ (C) 2025 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 {
+ AmountString,
+ IbanString,
+ j2s,
+ Logger,
+ Paytos,
+ TalerProtocolDuration,
+ TransactionMajorState,
+ TransactionType,
+ WithdrawalType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import { createWalletDaemonWithClient } from "../harness/environments.js";
+import { startFakeChallenger } from "../harness/fake-challenger.js";
+import {
+ ExchangeService,
+ GlobalTestState,
+ LibeufinNexusService,
+ MerchantService,
+ NexusBankAccountInfo,
+ setupDb,
+} from "../harness/harness.js";
+import { topsKycRulesConf, topsProvidersTestConf } from "../harness/tops.js";
+
+const logger = new Logger("test-tops-nexus-swt.ts");
+
+/**
+ * Test short wire transfers in an exchange setup that simulates
+ * the Taler Operations CH exchange, using libeufin-nexus
+ * as the taler wire gateway API.
+ */
+export async function runTopsNexusSwtTest(t: GlobalTestState) {
+ const db = await setupDb(t);
+
+ const challenger = await startFakeChallenger({
+ port: 6001,
+ addressType: "postal-ch",
+ });
+
+ let coinConfig: CoinConfig[];
+ coinConfig = defaultCoinConfig.map((x) => x("CHF"));
+
+ const bankAccountInfo: NexusBankAccountInfo = {
+ // Random IBAN generated via "libeufin-nexus testing iban gen --country CH"
+ iban: "CH7347363QVFHHFR8BWWB",
+ // PostFinance BIC
+ bic: "POFICHBEXXX",
+ name: "Harness Test Exchange",
+ qrIban: "CH1130000001166556117",
+ };
+
+ const nexus = await LibeufinNexusService.create(t, {
+ currency: "CHF",
+ database: db.connStr,
+ httpPort: 8085,
+ bankAccountInfo,
+ });
+
+ const exchangePayto = Paytos.toFullString(
+ Paytos.createIban(bankAccountInfo.qrIban as IbanString, undefined, {
+ "receiver-name": bankAccountInfo.name,
+ }),
+ );
+
+ await nexus.dbinit();
+
+ await nexus.start();
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "CHF",
+ httpPort: 8081,
+ database: db.connStr,
+ // FIXME: This is a terrible way to configure the exchange, should be moved into config.
+ extraProcEnv: {
+ EXCHANGE_AML_PROGRAM_TOPS_ENABLE_DEPOSITS_TOS_NAME: "v1",
+ EXCHANGE_AML_PROGRAM_TOPS_ENABLE_DEPOSITS_THRESHOLD: "CHF:0",
+ EXCHANGE_AML_PROGRAM_TOPS_POSTAL_CHECK_COUNTRY_REGEX: "ch|CH|Ch",
+ },
+ });
+
+ exchange.addBankAccount("nexusacct", {
+ accountPaytoUri: exchangePayto,
+ wireGatewayApiBaseUrl: nexus.wireGatewayApiBaseUrl,
+ wireGatewayAuth: {
+ type: "basic",
+ username: "exchange-test",
+ password: "exchange-test",
+ },
+ preparedTransferUrl: nexus.preparedTransferApiBaseUrl,
+ });
+
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.modifyConfig(async (config) => {
+ config.loadFromString(topsKycRulesConf);
+ config.loadFromString(topsProvidersTestConf);
+ });
+
+ await exchange.start();
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "wallet",
+ });
+
+ const acceptRes = await walletClient.call(
+ WalletApiOperation.AcceptManualWithdrawal,
+ {
+ amount: "CHF:10" as AmountString,
+ exchangeBaseUrl: exchange.baseUrl,
+ },
+ );
+
+ const wtx = await walletClient.call(WalletApiOperation.GetTransactionById, {
+ transactionId: acceptRes.transactionId,
+ });
+
+ t.assertDeepEqual(wtx.type, TransactionType.Withdrawal);
+ t.assertDeepEqual(wtx.withdrawalDetails.type, WithdrawalType.ManualTransfer);
+
+ console.log(j2s(wtx.withdrawalDetails.exchangeCreditAccountDetails));
+ const transferOpt =
+ wtx.withdrawalDetails.exchangeCreditAccountDetails?.[0].transferOptions?.find(
+ (x) => x.type === "ch-qr-bill",
+ );
+ t.assertTrue(!!transferOpt);
+
+ await nexus.fakeIncoming({
+ creditPayto: transferOpt.paytoUri,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: acceptRes.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+}
+
+runTopsNexusSwtTest.suites = ["tops", "libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -173,8 +173,10 @@ import { runTopsAmlLegiTest } from "./test-tops-aml-legi.js";
import { runTopsAmlMeasuresTest } from "./test-tops-aml-measures.js";
import { runTopsAmlPdfTest } from "./test-tops-aml-pdf.js";
import { runTopsChallengerTwiceTest } from "./test-tops-challenger-twice.js";
+import { runTopsMerchantKycauthsTest } from "./test-tops-merchant-kycauths.js";
import { runTopsMerchantTosTest } from "./test-tops-merchant-tos.js";
import { runTopsNexusBasicTest } from "./test-tops-nexus-basic.js";
+import { runTopsNexusSwtTest } from "./test-tops-nexus-swt.js";
import { runTopsPeerTest } from "./test-tops-peer.js";
import { runTermOfServiceFormatTest } from "./test-tos-format.js";
import { runUtilMerchantClientTest } from "./test-util-merchant-client.js";
@@ -440,6 +442,8 @@ const allTests: TestMainFunction[] = [
runBalanceProspectiveTest,
runMerchantTemplatesTest,
runTopsNexusBasicTest,
+ runTopsNexusSwtTest,
+ runTopsMerchantKycauthsTest,
];
export interface TestRunSpec {