taler-typescript-core

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

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:
Mpackages/taler-harness/src/harness/harness.ts | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mpackages/taler-harness/src/integrationtests/test-libeufin-conversion.ts | 1+
Apackages/taler-harness/src/integrationtests/test-tops-nexus-basic.ts | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-util/src/types-taler-wire-gateway.ts | 6+++---
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())