taler-typescript-core

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

commit 00522e511628cab0c8f07100f2da34136c89c206
parent da08ac723b8c079fe3058704adc20e916ddbf112
Author: Florian Dold <florian@dold.me>
Date:   Wed,  1 Apr 2026 16:45:39 +0200

harness: extended test for payto URI reuse across merchant instances

Diffstat:
Mpackages/taler-harness/src/harness/harness.ts | 17+++++++++++++++++
Apackages/taler-harness/src/integrationtests/test-merchant-payto-reuse.ts | 380+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/test-wire-metadata.ts | 18+++++-------------
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
4 files changed, 404 insertions(+), 13 deletions(-)

diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -749,6 +749,11 @@ export class FakebankService return new FakebankService(gc, bc, cfgFilename); } + async getAdminTok(): Promise<AccessToken> { + // fakebank does not support token auth. + return "secret-token:admin" as AccessToken; + } + static fromExistingConfig( gc: GlobalTestState, opts: { overridePath?: string }, @@ -988,6 +993,16 @@ export class LibeufinBankService return new LibeufinBankService(gc, bc, cfgFilename); } + async getAdminTok(): Promise<AccessToken> { + const bankClient = new TalerCoreBankHttpClient(this.corebankApiBaseUrl); + const bankAdminTok = succeedOrThrow( + await bankClient.createAccessToken("admin", harnessBankAdminCreds, { + scope: "readwrite", + }), + ).access_token; + return bankAdminTok; + } + static fromExistingConfig( gc: GlobalTestState, opts: { overridePath?: string }, @@ -1142,6 +1157,8 @@ export interface BankServiceHandle { stop(): Promise<void>; getAdminAuth(): { username: string; password: string }; + getAdminTok(): Promise<AccessToken>; + changeConfig(f: (config: Configuration) => void): void; } diff --git a/packages/taler-harness/src/integrationtests/test-merchant-payto-reuse.ts b/packages/taler-harness/src/integrationtests/test-merchant-payto-reuse.ts @@ -0,0 +1,380 @@ +/* + This file is part of GNU Taler + (C) 2024 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 { + AccessToken, + Configuration, + ConfirmPayResultType, + encodeCrock, + getRandomBytes, + j2s, + KycStatusLongPollingReason, + Logger, + parsePaytoUriOrThrow, + PreparePayResultType, + succeedOrThrow, + TalerMerchantInstanceHttpClient, + TalerProtocolDuration, + TalerWireGatewayHttpClient, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + configureCommonKyc, + createKycTestkudosEnvironmentFull, + withdrawViaBankV4, +} from "../harness/environments.js"; +import { + BankService, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + MerchantService, +} from "../harness/harness.js"; + +const myAmlConfig = ` +# Fallback measure on errors. +[kyc-measure-freeze-investigate] +CHECK_NAME = skip +PROGRAM = freeze-investigate +VOLUNTARY = NO +CONTEXT = {} + +[aml-program-freeze-investigate] +DESCRIPTION = "Fallback measure on errors that freezes the account and asks AML staff to investigate the system failure." +COMMAND = taler-exchange-helper-measure-freeze +ENABLED = YES +FALLBACK = freeze-investigate + +[aml-program-inform-investigate] +DESCRIPTION = "Measure that asks AML staff to investigate an account and informs the account owner about it." +COMMAND = taler-exchange-helper-measure-inform-investigate +ENABLED = YES +FALLBACK = freeze-investigate + +[kyc-check-form-gls-merchant-onboarding] +TYPE = FORM +FORM_NAME = gls-merchant-onboarding +DESCRIPTION = "GLS Merchant Onboarding" +DESCRIPTION_I18N = {} +OUTPUTS = +FALLBACK = freeze-investigate + +[kyc-measure-merchant-onboarding] +CHECK_NAME = form-gls-merchant-onboarding +PROGRAM = inform-investigate +CONTEXT = {} +VOLUNTARY = NO + +[kyc-rule-deposit-limit-zero] +OPERATION_TYPE = DEPOSIT +NEXT_MEASURES = merchant-onboarding +EXPOSED = YES +ENABLED = YES +THRESHOLD = TESTKUDOS:1 +TIMEFRAME = "1 days" +`; + +function adjustExchangeConfig(config: Configuration) { + configureCommonKyc(config); + config.loadFromString(myAmlConfig); +} + +const logger = new Logger("test-merchant-kyc-auth-multi.ts"); + +async function doAccountKycAuth( + t: GlobalTestState, + args: { + exchange: ExchangeService; + merchant: MerchantService; + bank: BankService; + merchantInstId: string; + merchantInstPaytoUri: string; + merchantAccessToken: AccessToken; + wireGatewayApi: TalerWireGatewayHttpClient; + }, +): Promise<void> { + const { + merchant, + exchange, + merchantInstId, + merchantAccessToken, + merchantInstPaytoUri, + bank, + wireGatewayApi, + } = args; + + const merchantClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(merchantInstId), + ); + { + const kycRes1 = succeedOrThrow( + await merchantClient.getCurrentInstanceKycStatus(merchantAccessToken, {}), + ); + console.log(`kyc res: ${j2s(kycRes1)}`); + const myRow = kycRes1.kyc_data.find( + (x) => x.exchange_url === exchange.baseUrl, + ); + t.assertTrue( + myRow?.payto_kycauths != null && myRow.payto_kycauths.length == 1, + ); + const authTxPayto = parsePaytoUriOrThrow(myRow.payto_kycauths[0]); + const authTxMessage = authTxPayto?.params["message"]; + t.assertTrue(typeof authTxMessage === "string"); + t.assertTrue(authTxMessage.startsWith("KYC:")); + const accountPub = authTxMessage.substring(4); + logger.info(`merchant account pub: ${accountPub}`); + await wireGatewayApi.addKycAuth({ + auth: bank.getAdminAuth(), + body: { + amount: "TESTKUDOS:0.1", + debit_account: merchantInstPaytoUri, + account_pub: accountPub, + }, + }); + } + + // Wait for auth transfer to be registered by the exchange + { + const kycStatus = await merchantClient.getCurrentInstanceKycStatus( + merchantAccessToken, + { + reason: KycStatusLongPollingReason.AUTH_TRANSFER, + timeout: 30000, + }, + ); + logger.info(`kyc status after transfer: ${j2s(kycStatus)}`); + t.assertDeepEqual(kycStatus.case, "ok"); + const myRow = kycStatus.body.kyc_data.find( + (x) => + x.exchange_url === exchange.baseUrl && + x.payto_uri === merchantInstPaytoUri, + ); + t.assertTrue(myRow != null); + t.assertDeepEqual(myRow.status, "ready"); + t.assertTrue(typeof myRow.access_token === "string"); + } +} + +/** + * Test for multiple merchant instances using the same + * bank account (with multiple public keys and thus KYC auth transfers). + */ +export async function runMerchantPaytoReuseTest(t: GlobalTestState) { + // Set up test environment + + const { + bankClient, + merchant, + bank, + exchange, + merchantAdminAccessToken, + wireGatewayApi, + walletClient, + } = await createKycTestkudosEnvironmentFull(t, { adjustExchangeConfig }); + + const wres = await withdrawViaBankV4(t, { + walletClient, + bank, + amount: "TESTKUDOS:30", + exchange, + bankAdminTok: await bank.getAdminTok(), + }); + + await wres.withdrawalFinishedCond; + + const merchantInstPaytoUri = getTestHarnessPaytoForLabel("mymerchant"); + + await bankClient.registerAccountExtended({ + name: "mymerchant", + password: encodeCrock(getRandomBytes(32)), + username: "mymerchant", + payto_uri: merchantInstPaytoUri, + }); + + const merchantInstId2 = "minst2"; + const merchantInstId1 = "minst1"; + + let minst1AccessToken: AccessToken; + + { + const m1Res = await merchant.addInstanceWithWireAccount( + { + id: merchantInstId1, + name: merchantInstId1, + paytoUris: [merchantInstPaytoUri], + defaultWireTransferDelay: TalerProtocolDuration.fromSpec({ + minutes: 1, + }), + }, + { adminAccessToken: merchantAdminAccessToken }, + ); + + minst1AccessToken = m1Res.accessToken; + + await merchant.runExchangekeyupdateOnce(); + + t.logStep("start first-account-kyc-auth"); + + await doAccountKycAuth(t, { + exchange, + merchant, + bank, + merchantInstId: merchantInstId1, + merchantAccessToken: minst1AccessToken, + merchantInstPaytoUri, + wireGatewayApi, + }); + } + + t.logStep("first-account-ready"); + + let minst2AccessToken: AccessToken; + + { + const m2Res = await merchant.addInstanceWithWireAccount( + { + id: merchantInstId2, + name: merchantInstId2, + paytoUris: [merchantInstPaytoUri], + defaultWireTransferDelay: TalerProtocolDuration.fromSpec({ + minutes: 1, + }), + }, + { adminAccessToken: merchantAdminAccessToken }, + ); + + await merchant.runKyccheckOnce(); + + minst2AccessToken = m2Res.accessToken; + + t.logStep("start second-account-kyc-auth"); + + await doAccountKycAuth(t, { + exchange, + merchant, + bank, + merchantInstId: merchantInstId2, + merchantAccessToken: m2Res.accessToken, + merchantInstPaytoUri, + wireGatewayApi, + }); + } + + t.logStep("second-account-ready"); + + { + const m1Client = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(merchantInstId1), + ); + + const orderResp = succeedOrThrow( + await m1Client.createOrder(minst1AccessToken, { + order: { + amount: "TESTKUDOS:1", + summary: "test1", + }, + }), + ); + + let orderStatus = succeedOrThrow( + await m1Client.getOrderDetails(minst1AccessToken, orderResp.order_id), + ); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderStatus.taler_pay_uri, + }, + ); + + t.assertDeepEqual( + preparePayResult.status, + PreparePayResultType.PaymentPossible, + ); + + const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: preparePayResult.transactionId, + }); + + t.assertDeepEqual(r2.type, ConfirmPayResultType.Done); + + // Check if payment was successful. + + orderStatus = succeedOrThrow( + await m1Client.getOrderDetails(minst1AccessToken, orderResp.order_id), + ); + + t.assertDeepEqual(orderStatus.order_status, "paid"); + } + + { + const m2Client = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(merchantInstId2), + ); + + const orderResp = succeedOrThrow( + await m2Client.createOrder(minst2AccessToken, { + order: { + amount: "TESTKUDOS:1", + summary: "test2", + }, + }), + ); + + let orderStatus = succeedOrThrow( + await m2Client.getOrderDetails(minst2AccessToken, orderResp.order_id), + ); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderStatus.taler_pay_uri, + }, + ); + + t.assertDeepEqual( + preparePayResult.status, + PreparePayResultType.PaymentPossible, + ); + + const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: preparePayResult.transactionId, + }); + + t.assertDeepEqual(r2.type, ConfirmPayResultType.Done); + + // Check if payment was successful. + + orderStatus = succeedOrThrow( + await m2Client.getOrderDetails(minst2AccessToken, orderResp.order_id), + ); + + t.assertDeepEqual(orderStatus.order_status, "paid"); + } +} + +runMerchantPaytoReuseTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-wire-metadata.ts b/packages/taler-harness/src/integrationtests/test-wire-metadata.ts @@ -43,7 +43,6 @@ import { ExchangeService, getTestHarnessPaytoForLabel, GlobalTestState, - harnessBankAdminCreds, HarnessExchangeBankAccount, LibeufinBankService, MerchantService, @@ -99,11 +98,7 @@ export async function runWireMetadataTest(t: GlobalTestState) { await bank.start(); const bankClient = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl); - const bankAdminTok = succeedOrThrow( - await bankClient.createAccessToken("admin", harnessBankAdminCreds, { - scope: "readwrite", - }), - ).access_token; + const bankAdminTok = await bank.getAdminTok(); await bankClient.createAccount(bankAdminTok, { name: receiverName, @@ -194,13 +189,10 @@ export async function runWireMetadataTest(t: GlobalTestState) { }), ); - const { walletClient, walletService } = await createWalletDaemonWithClient( - t, - { - name: "wallet", - persistent: true, - }, - ); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "wallet", + persistent: true, + }); const wres = await withdrawViaBankV4(t, { walletClient, diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -114,6 +114,7 @@ import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls.js" import { runMerchantInstancesTest } from "./test-merchant-instances.js"; import { runMerchantKycAuthMultiTest } from "./test-merchant-kyc-auth-multi.js"; import { runMerchantLongpollingTest } from "./test-merchant-longpolling.js"; +import { runMerchantPaytoReuseTest } from "./test-merchant-payto-reuse.js"; import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js"; import { runMerchantReportsTest } from "./test-merchant-reports.js"; import { runMerchantSelfProvisionActivationAndLoginTest } from "./test-merchant-self-provision-activation-and-login.js"; @@ -430,6 +431,7 @@ const allTests: TestMainFunction[] = [ runWalletWithdrawalRedenominateTest, runMerchantDepositLargeTest, runWireMetadataTest, + runMerchantPaytoReuseTest, ]; export interface TestRunSpec {