commit 17b1149a1a781018491cecd000ce15a669ce9a67
parent 56e1d716b1dc8dba4cacee2f41b174e4c49dcd50
Author: Florian Dold <florian@dold.me>
Date: Fri, 26 Jun 2026 01:11:28 +0200
harness: implement basic test with libeufin-nexus and fake-incoming
We eventually want to migrate away from libeufin-bank to libeufin-nexus
in the tops-* integration tests.
Diffstat:
5 files changed, 242 insertions(+), 9 deletions(-)
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
@@ -891,15 +891,26 @@ export interface NexusConfig {
currency: string;
/** DB connection string */
database: string;
+ httpPort: number;
+ bankAccountInfo?: NexusBankAccountInfo;
+}
+
+export interface NexusBankAccountInfo {
+ iban: string;
+ name: string;
+ bic: string;
}
export class LibeufinNexusService {
+ async fakeIncoming(args: { creditPayto: string }): Promise<void> {
+ await sh(
+ this.gc,
+ "fake-incoming",
+ `libeufin-nexus testing fake-incoming -c "${this.configFile}" --credit-payto="${args.creditPayto}"`,
+ );
+ }
/**
- * Create a new fakebank service handle.
- *
- * First generates the configuration for the fakebank and
- * then creates a fakebank handle, but doesn't start the fakebank
- * service yet.
+ * Create a libeufin nexus service handle.
*/
static async create(
gc: GlobalTestState,
@@ -908,13 +919,45 @@ export class LibeufinNexusService {
const config = new Configuration();
const testDir = gc.testDir;
setTalerPaths(config, testDir + "/talerhome");
+ // Still used for currency conversion?
config.setString("libeufin-nexus", "currency", bc.currency);
+ config.setString("nexus-ebics", "currency", bc.currency);
config.setString("libeufin-nexusdb-postgres", "config", bc.database);
+ config.setString("nexus-httpd", "port", `${bc.httpPort}`);
+ config.setString("nexus-httpd-wire-gateway-api", "enabled", "yes");
+
+ config.setString("nexus-httpd-wire-gateway-api", "auth_method", "basic");
+ config.setString(
+ "nexus-httpd-wire-gateway-api",
+ "username",
+ "exchange-test",
+ );
+ config.setString(
+ "nexus-httpd-wire-gateway-api",
+ "password",
+ "exchange-test",
+ );
+
+ // We might want to switch to bearer in the future!
+ // config.setString("nexus-httpd-wire-gateway-api", "auth_method", "bearer");
+ // config.setString(
+ // "nexus-httpd-wire-gateway-api",
+ // "token",
+ // "secret-token:test",
+ // );
+ if (bc.bankAccountInfo) {
+ const bi = bc.bankAccountInfo;
+ config.setString("nexus-ebics", "iban", bi.iban);
+ config.setString("nexus-ebics", "bic", bi.bic);
+ config.setString("nexus-ebics", "name", bi.name);
+ }
const cfgFilename = testDir + "/nexus.conf";
config.writeTo(cfgFilename, { excludeDefaults: true });
return new LibeufinNexusService(gc, bc, cfgFilename);
}
+ proc: ProcessWrapper | undefined;
+
constructor(
private gc: GlobalTestState,
private bc: NexusConfig,
@@ -928,6 +971,51 @@ export class LibeufinNexusService {
`libeufin-nexus dbinit -c "${this.configFile}"`,
);
}
+
+ async start(): Promise<void> {
+ logger.info("starting fakebank");
+ if (this.proc) {
+ logger.info("fakebank already running, not starting again");
+ return;
+ }
+ this.proc = this.gc.spawnService(
+ "libeufin-nexus",
+ ["serve", "-c", this.configFile],
+ "nexus",
+ );
+ await this.pingUntilAvailable();
+ // Check version
+ {
+ const bankClient = new TalerWireGatewayHttpClient(
+ this.baseUrl + "taler-wire-gateway/",
+ );
+ // This would fail/throw if the version doesn't match.
+ const resp = await bankClient.getConfig();
+ this.gc.assertTrue(resp.type === "ok");
+ }
+ }
+
+ async pingUntilAvailable(): Promise<void> {
+ const url = `http://localhost:${this.bc.httpPort}/taler-wire-gateway/config`;
+ await pingProc(this.proc, url, "bank");
+ }
+
+ async stop(): Promise<void> {
+ const nexusProc = this.proc;
+ if (nexusProc) {
+ nexusProc.proc.kill("SIGTERM");
+ await nexusProc.wait();
+ this.proc = undefined;
+ }
+ }
+
+ get baseUrl(): string {
+ return `http://localhost:${this.bc.httpPort}/`;
+ }
+
+ get wireGatewayApiBaseUrl(): string {
+ return `http://localhost:${this.bc.httpPort}/taler-wire-gateway/`;
+ }
}
/**
@@ -1483,7 +1571,6 @@ export class ExchangeService implements ExchangeServiceInterface {
setTalerPaths(config, `${testDir}/talerhome-exchange-${e.name}`, e.name);
config.setString("exchange", "currency", e.currency);
// Required by the exchange but not really used yet.
- config.setString("exchange", "aml_threshold", `${e.currency}:1000000`);
config.setString(
"exchange",
"currency_round_unit",
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-conversion.ts b/packages/taler-harness/src/integrationtests/test-libeufin-conversion.ts
@@ -72,6 +72,7 @@ export async function runLibeufinConversionTest(t: GlobalTestState) {
const nexus = await LibeufinNexusService.create(t, {
currency: "FOO",
database: db.connStr,
+ httpPort: 8085,
});
await nexus.dbinit();
diff --git a/packages/taler-harness/src/integrationtests/test-tops-nexus-basic.ts b/packages/taler-harness/src/integrationtests/test-tops-nexus-basic.ts
@@ -0,0 +1,143 @@
+/*
+ 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,
+ 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 {
+ ExchangeService,
+ GlobalTestState,
+ LibeufinNexusService,
+ NexusBankAccountInfo,
+ setupDb,
+} from "../harness/harness.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 runTopsNexusBasicTest(t: GlobalTestState) {
+ const db = await setupDb(t);
+
+ 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",
+ };
+
+ const nexus = await LibeufinNexusService.create(t, {
+ currency: "CHF",
+ database: db.connStr,
+ httpPort: 8085,
+ bankAccountInfo,
+ });
+
+ const payto = Paytos.toFullString(
+ Paytos.createIban(bankAccountInfo.iban 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: payto,
+ wireGatewayApiBaseUrl: nexus.wireGatewayApiBaseUrl,
+ wireGatewayAuth: {
+ type: "basic",
+ username: "exchange-test",
+ password: "exchange-test",
+ },
+ });
+
+ exchange.addCoinConfigList(coinConfig);
+
+ 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?.[0];
+ t.assertDeepEqual(transferOpt?.type, "payto");
+
+ await nexus.fakeIncoming({
+ creditPayto: transferOpt.paytoUri,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: acceptRes.transactionId,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+}
+
+runTopsNexusBasicTest.suites = ["tops", "libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -174,6 +174,7 @@ 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 { runTopsMerchantTosTest } from "./test-tops-merchant-tos.js";
+import { runTopsNexusBasicTest } from "./test-tops-nexus-basic.js";
import { runTopsPeerTest } from "./test-tops-peer.js";
import { runTermOfServiceFormatTest } from "./test-tos-format.js";
import { runUtilMerchantClientTest } from "./test-util-merchant-client.js";
@@ -438,6 +439,7 @@ const allTests: TestMainFunction[] = [
runMerchantTokenfamiliesTest,
runBalanceProspectiveTest,
runMerchantTemplatesTest,
+ runTopsNexusBasicTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/src/types-taler-wire-gateway.ts b/packages/taler-util/src/types-taler-wire-gateway.ts
@@ -16,6 +16,7 @@
SPDX-License-Identifier: AGPL-3.0-or-later
*/
+import { codecForAmountString } from "./amounts.js";
import {
Codec,
buildCodecForObject,
@@ -30,11 +31,10 @@ import {
codecOptional,
} from "./codec.js";
import {
- codecForEddsaSignature,
EddsaSignature,
TalerWireGatewayApi,
+ codecForEddsaSignature,
} from "./index.js";
-import { codecForAmountString } from "./amounts.js";
import { PaytoString, codecForPaytoString } from "./payto.js";
import { codecForTimestamp } from "./time.js";
import {
@@ -377,7 +377,7 @@ export const codecForWireConfigResponse =
(): Codec<TalerWireGatewayApi.WireConfig> =>
buildCodecForObject<TalerWireGatewayApi.WireConfig>()
.property("currency", codecForString())
- .property("implementation", codecForString())
+ .property("implementation", codecOptional(codecForString()))
.property("name", codecForConstString("taler-wire-gateway"))
.property("support_account_check", codecForBoolean())
.property("version", codecForString())