taler-typescript-core

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

commit 119c423765650e10263ce8462e9fbbffd6506eb1
parent 4307c0a849e4b7f532161a5f8b7f544704ab2ff0
Author: Florian Dold <florian@dold.me>
Date:   Mon, 23 Feb 2026 19:55:09 +0100

harness: add for multiple instances with same bank account in merchant

Diffstat:
Mpackages/taler-harness/src/harness/harness.ts | 9+++++++++
Apackages/taler-harness/src/integrationtests/test-merchant-kyc-auth-multi.ts | 279+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-util/src/http-client/merchant.ts | 24++++++++++++++++++++----
4 files changed, 310 insertions(+), 4 deletions(-)

diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -2238,6 +2238,15 @@ export class MerchantService implements MerchantServiceInterface { ); } + async runExchangekeyupdateOnce() { + await runCommand( + this.globalState, + `merchant-${this.name}-exchangekeyupdate-once`, + "taler-merchant-exchangekeyupdate", + [...this.timetravelArgArr, "-LINFO", "-c", this.configFilename, "-t"], + ); + } + async runDonaukeyupdateOnce() { await runCommand( this.globalState, diff --git a/packages/taler-harness/src/integrationtests/test-merchant-kyc-auth-multi.ts b/packages/taler-harness/src/integrationtests/test-merchant-kyc-auth-multi.ts @@ -0,0 +1,279 @@ +/* + 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 { + Configuration, + encodeCrock, + getRandomBytes, + j2s, + KycStatusLongPollingReason, + Logger, + parsePaytoUriOrThrow, + succeedOrThrow, + TalerMerchantInstanceHttpClient, + TalerProtocolDuration, +} from "@gnu-taler/taler-util"; +import { + configureCommonKyc, + createKycTestkudosEnvironment, +} from "../harness/environments.js"; +import { + getTestHarnessPaytoForLabel, + GlobalTestState, +} 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"); + +/** + * Test for multiple merchant instances using the same + * bank account (with multiple public keys and thus KYC auth transfers). + */ +export async function runMerchantKycAuthMultiTest(t: GlobalTestState) { + // Set up test environment + + const { + bankClient, + exchangeBankAccount, + exchangeApi, + merchant, + bank, + exchange, + merchantAdminAccessToken, + wireGatewayApi, + } = await createKycTestkudosEnvironment(t, { adjustExchangeConfig }); + + const merchantPayto = getTestHarnessPaytoForLabel("merchant-default"); + + await bankClient.registerAccountExtended({ + name: "merchant-default", + password: encodeCrock(getRandomBytes(32)), + username: "merchant-default", + payto_uri: merchantPayto, + }); + + { + const merchantInstId = "minst1"; + const merchantInstPaytoUri = getTestHarnessPaytoForLabel(merchantInstId); + + const m1Res = await merchant.addInstanceWithWireAccount( + { + id: merchantInstId, + name: merchantInstId, + paytoUris: [merchantInstPaytoUri], + defaultWireTransferDelay: TalerProtocolDuration.fromSpec({ + minutes: 1, + }), + }, + { adminAccessToken: merchantAdminAccessToken }, + ); + + await bankClient.registerAccountExtended({ + name: merchantInstId, + password: encodeCrock(getRandomBytes(32)), + username: merchantInstId, + payto_uri: merchantInstPaytoUri, + }); + + await merchant.runExchangekeyupdateOnce(); + await merchant.runKyccheckOnce(); + + const merchantClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl("minst1"), + ); + { + const kycRes1 = succeedOrThrow( + await merchantClient.getCurrentInstanceKycStatus(m1Res.accessToken, {}), + ); + console.log(`kyc res: ${j2s(kycRes1)}`); + t.assertDeepEqual(kycRes1.kycRequired, true); + 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( + m1Res.accessToken, + { + reason: KycStatusLongPollingReason.AUTH_TRANSFER, + timeout: 30000, + }, + ); + logger.info(`kyc status after transfer: ${j2s(kycStatus)}`); + t.assertDeepEqual(kycStatus.case, "ok"); + t.assertTrue(kycStatus.body.kycRequired === true); + const myRow = kycStatus.body.kyc_data.find( + (x) => x.exchange_url === exchange.baseUrl, + ); + t.assertTrue(myRow != null); + t.assertDeepEqual(myRow.status, "ready"); + t.assertTrue(typeof myRow.access_token === "string"); + } + } + + t.logStep("fist-account-ready"); + + { + const merchantInstId = "minst2"; + const merchantInstPaytoUri = getTestHarnessPaytoForLabel(merchantInstId); + + const m1Res = await merchant.addInstanceWithWireAccount( + { + id: merchantInstId, + name: merchantInstId, + paytoUris: [merchantInstPaytoUri], + defaultWireTransferDelay: TalerProtocolDuration.fromSpec({ + minutes: 1, + }), + }, + { adminAccessToken: merchantAdminAccessToken }, + ); + + await bankClient.registerAccountExtended({ + name: merchantInstId, + password: encodeCrock(getRandomBytes(32)), + username: merchantInstId, + payto_uri: merchantInstPaytoUri, + }); + + await merchant.runExchangekeyupdateOnce(); + await merchant.runKyccheckOnce(); + + const merchantClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl("minst1"), + ); + { + const kycRes1 = succeedOrThrow( + await merchantClient.getCurrentInstanceKycStatus(m1Res.accessToken, {}), + ); + console.log(`kyc res: ${j2s(kycRes1)}`); + t.assertDeepEqual(kycRes1.kycRequired, true); + 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( + m1Res.accessToken, + { + reason: KycStatusLongPollingReason.AUTH_TRANSFER, + timeout: 30000, + }, + ); + logger.info(`kyc status after transfer: ${j2s(kycStatus)}`); + t.assertDeepEqual(kycStatus.case, "ok"); + t.assertTrue(kycStatus.body.kycRequired === true); + const myRow = kycStatus.body.kyc_data.find( + (x) => x.exchange_url === exchange.baseUrl, + ); + t.assertTrue(myRow != null); + t.assertDeepEqual(myRow.status, "ready"); + t.assertTrue(typeof myRow.access_token === "string"); + } + } + + t.logStep("second-account-ready"); +} + +runMerchantKycAuthMultiTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -112,6 +112,7 @@ import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confu import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete.js"; 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 { runMerchantRefundApiTest } from "./test-merchant-refund-api.js"; import { runMerchantReportsTest } from "./test-merchant-reports.js"; @@ -423,6 +424,7 @@ const allTests: TestMainFunction[] = [ runWalletRefreshRedenominateTest, runMerchantReportsTest, runExchangeMerchantKycAuthTest, + runMerchantKycAuthMultiTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -171,6 +171,16 @@ export enum TalerMerchantManagementCacheEviction { DELETE_INSTANCE, } +export type MerchantKycStatusResult = + | { + kycRequired: false | undefined; + } + | { + kycRequired: true; + kyc_data: TalerMerchantApi.MerchantAccountKycRedirect[]; + etag?: string; + }; + /** * Protocol version spoken with the core bank. * @@ -248,9 +258,9 @@ export class TalerMerchantInstanceHttpClient { | OperationFail<HttpStatusCode.NotFound> | OperationOk<TalerMerchantApi.LoginTokenSuccessResponse> | OperationAlternative< - HttpStatusCode.Accepted, - TalerMerchantApi.ChallengeResponse - > + HttpStatusCode.Accepted, + TalerMerchantApi.ChallengeResponse + > | OperationFail<HttpStatusCode.Unauthorized> > { const url = new URL(`private/token`, this.baseUrl); @@ -831,7 +841,13 @@ export class TalerMerchantInstanceHttpClient { async getCurrentInstanceKycStatus( token: AccessToken, params: TalerMerchantApi.GetKycStatusRequestParams = {}, - ) { + ): Promise< + | OperationOk<MerchantKycStatusResult> + | OperationFail<HttpStatusCode.Unauthorized> + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.ServiceUnavailable> + | OperationFail<HttpStatusCode.GatewayTimeout> + > { const url = new URL(`private/kyc`, this.baseUrl); if (params.wireHash) {