taler-typescript-core

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

commit d8ed968c6704cacf49f850d4c1d92d7562589435
parent 6a30be60a21b92df9feef813ea4043f5c18bfd09
Author: Florian Dold <dold@taler.net>
Date:   Mon, 17 Aug 2026 22:35:03 +0200

taler-harness: consolidate integration test scenarios

Diffstat:
Apackages/taler-harness/src/integrationtests/kyc-dynamic-measure-helper.ts | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-harness/src/integrationtests/kyc-form-withdrawal-helper.ts | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-harness/src/integrationtests/kyc-threshold-withdrawal-helper.ts | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-harness/src/integrationtests/merchant-kyc-auth-helper.ts | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/test-kyc-form-compression.ts | 174+++----------------------------------------------------------------------------
Dpackages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts | 193-------------------------------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-kyc-merchant-deposit-form.ts | 59+++--------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-kyc-merchant-deposit-rewrite.ts | 29++---------------------------
Mpackages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts | 29++---------------------------
Mpackages/taler-harness/src/integrationtests/test-kyc-new-measures-prog.ts | 320++++++++-----------------------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts | 280+++++--------------------------------------------------------------------------
Dpackages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts | 178-------------------------------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-kyc-withdrawal-verboten.ts | 149+++----------------------------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-acctsel.ts | 73+++++++++++++++----------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-kyc-auth-multi.ts | 120+++++--------------------------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-payto-reuse.ts | 125++++---------------------------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-refund-api.ts | 279++++++++++++-------------------------------------------------------------------
Dpackages/taler-harness/src/integrationtests/test-merchant-self-provision-activation-two-bank-account.ts | 222-------------------------------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts | 200++++++++++++++++++++++++++++++++++---------------------------------------------
Dpackages/taler-harness/src/integrationtests/test-merchant-self-provision-casing.ts | 221-------------------------------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts | 700++++++++++++++-----------------------------------------------------------------
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 8--------
22 files changed, 979 insertions(+), 3016 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/kyc-dynamic-measure-helper.ts b/packages/taler-harness/src/integrationtests/kyc-dynamic-measure-helper.ts @@ -0,0 +1,181 @@ +/* + This file is part of GNU Taler + (C) 2024, 2026 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. +*/ + +import { + codecForAny, + codecForKycProcessClientInformation, + codecOptional, + Configuration, + decodeCrock, + encodeCrock, + KycProcessClientInformation, + signAmlQuery, + TalerProtocolTimestamp, + TransactionIdStr, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + configureCommonKyc, + createKycTestkudosEnvironmentFull, + postAmlDecision, + withdrawViaBankV3, +} from "../harness/environments.js"; +import { GlobalTestState, harnessHttpLib, waitMs } from "../harness/harness.js"; + +function configureDynamicMeasureExchange( + config: Configuration, + amlProgramName: string, +): void { + configureCommonKyc(config); + config.setString("KYC-RULE-R1", "operation_type", "withdraw"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "no"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M2"); + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + config.setString("KYC-MEASURE-M2", "check_name", "C2"); + config.setString("KYC-MEASURE-M2", "context", "{}"); + config.setString("KYC-MEASURE-M2", "program", "P2"); + config.setString("KYC-MEASURE-M3", "check_name", "SKIP"); + config.setString("KYC-MEASURE-M3", "context", "{}"); + config.setString("KYC-MEASURE-M3", "program", "P1"); + config.setString( + "AML-PROGRAM-P1", + "command", + `taler-harness aml-program run-program --name ${amlProgramName}`, + ); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "remove all rules"); + config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); + config.setString("AML-PROGRAM-P2", "command", "/bin/true"); + config.setString("AML-PROGRAM-P2", "enabled", "true"); + config.setString("AML-PROGRAM-P2", "description", "does nothing"); + config.setString("AML-PROGRAM-P2", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P2", "fallback", "FREEZE"); + config.setString("KYC-CHECK-C1", "type", "FORM"); + config.setString("KYC-CHECK-C1", "form_name", "myform"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "description_i18n", "{}"); + config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate"); + config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); + config.setString("KYC-CHECK-C2", "type", "FORM"); + config.setString("KYC-CHECK-C2", "form_name", "dynamicform"); + config.setString("KYC-CHECK-C2", "description", "my check info!"); + config.setString("KYC-CHECK-C2", "description_i18n", "{}"); + config.setString("KYC-CHECK-C2", "outputs", "what_the_officer_asked"); + config.setString("KYC-CHECK-C2", "fallback", "FREEZE"); +} + +/** Exercise M3/SKIP and inspect the requirements produced by its AML program. */ +export async function runDynamicMeasureScenario( + t: GlobalTestState, + args: { + amlProgramName: string; + assertRequirements: (info: KycProcessClientInformation) => void; + }, +): Promise<void> { + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironmentFull(t, { + adjustExchangeConfig: (config) => + configureDynamicMeasureExchange(config, args.amlProgramName), + }); + const wres = await withdrawViaBankV3(t, { + amount: "TESTKUDOS:20", + bankClient, + exchange, + walletClient, + }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: wres.transactionId as TransactionIdStr, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }, + }); + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { transactionId: wres.transactionId }, + ); + t.assertTrue(!!txDetails.kycAccessToken); + t.assertTrue(!!txDetails.kycPaytoHash); + const accessToken = txDetails.kycAccessToken; + + const initialInfoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href, + ); + await readResponseJsonOrThrow( + initialInfoResp, + codecOptional(codecForKycProcessClientInformation()), + ); + t.assertDeepEqual(initialInfoResp.status, 200); + + const sig = signAmlQuery(decodeCrock(amlKeypair.priv)); + const decisionHeaders = { + "Taler-AML-Officer-Signature": encodeCrock(sig), + }; + const decisionsUrl = new URL( + `aml/${amlKeypair.pub}/decisions`, + exchange.baseUrl, + ).href; + const emptyDecisionsResp = await harnessHttpLib.fetch(decisionsUrl, { + headers: decisionHeaders, + }); + t.assertDeepEqual(emptyDecisionsResp.status, 204); + + await postAmlDecision(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: txDetails.kycPaytoHash, + newMeasures: "M3", + properties: { form: { name: "string" } }, + newRules: { + expiration_time: TalerProtocolTimestamp.now(), + custom_measures: {}, + rules: [], + }, + }); + + const populatedDecisionsResp = await harnessHttpLib.fetch(decisionsUrl, { + headers: decisionHeaders, + }); + await readResponseJsonOrThrow(populatedDecisionsResp, codecForAny()); + t.assertDeepEqual(populatedDecisionsResp.status, 200); + + const deadline = Date.now() + 30_000; + while (true) { + t.assertTrue(Date.now() < deadline, "timed out waiting for AML program"); + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href, + ); + if (infoResp.status === 202 || infoResp.status === 204) { + await waitMs(250); + continue; + } + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecOptional(codecForKycProcessClientInformation()), + ); + t.assertDeepEqual(infoResp.status, 200); + if (!clientInfo || clientInfo.requirements.length === 0) { + await waitMs(250); + continue; + } + args.assertRequirements(clientInfo); + return; + } +} diff --git a/packages/taler-harness/src/integrationtests/kyc-form-withdrawal-helper.ts b/packages/taler-harness/src/integrationtests/kyc-form-withdrawal-helper.ts @@ -0,0 +1,137 @@ +/* + This file is part of GNU Taler + (C) 2020, 2026 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. +*/ + +import { + codecForAny, + codecForKycProcessClientInformation, + Configuration, + decodeCrock, + encodeCrock, + signAmlQuery, + TransactionIdStr, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { + expectSuccessResponseOrThrow, + readResponseJsonOrThrow, +} from "@gnu-taler/taler-util/http"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + configureCommonKyc, + createKycTestkudosEnvironmentFull, + withdrawViaBankV3, +} from "../harness/environments.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; + +function adjustExchangeConfig(config: Configuration): void { + configureCommonKyc(config); + config.setString("KYC-RULE-R1", "operation_type", "withdraw"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1 M2"); + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + config.setString("KYC-MEASURE-M2", "check_name", "C2"); + config.setString("KYC-MEASURE-M2", "context", "{}"); + config.setString("KYC-MEASURE-M2", "program", "NONE"); + config.setString( + "AML-PROGRAM-P1", + "command", + "taler-exchange-helper-measure-test-form", + ); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString( + "AML-PROGRAM-P1", + "description", + "test for FULL_NAME and DATE_OF_BIRTH", + ); + config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); + config.setString("KYC-CHECK-C1", "type", "FORM"); + config.setString("KYC-CHECK-C1", "form_name", "full_name_and_birthdate"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "description_i18n", "{}"); + config.setString("KYC-CHECK-C1", "outputs", "FULL_NAME DATE_OF_BIRTH"); + config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); + config.setString("KYC-CHECK-C2", "type", "INFO"); + config.setString("KYC-CHECK-C2", "description", "my check info!"); + config.setString("KYC-CHECK-C2", "description_i18n", "{}"); + config.setString("KYC-CHECK-C2", "fallback", "FREEZE"); +} + +export async function runKycFormWithdrawalScenario( + t: GlobalTestState, + upload: { body: object; compress?: "deflate" }, +): Promise<void> { + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironmentFull(t, { adjustExchangeConfig }); + const wres = await withdrawViaBankV3(t, { + amount: "TESTKUDOS:20", + bankClient, + exchange, + walletClient, + }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: wres.transactionId as TransactionIdStr, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }, + }); + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { transactionId: wres.transactionId }, + ); + t.assertTrue(!!txDetails.kycAccessToken); + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href, + ); + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecForKycProcessClientInformation(), + ); + const kycId = clientInfo.requirements.find((x) => x.id != null)?.id; + t.assertTrue(!!kycId); + const uploadResp = await harnessHttpLib.fetch( + new URL(`kyc-upload/${kycId}`, exchange.baseUrl).href, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + compress: upload.compress, + body: upload.body, + }, + ); + await expectSuccessResponseOrThrow(uploadResp); + t.assertDeepEqual(uploadResp.status, 204); + + const updatedInfoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href, + ); + await readResponseJsonOrThrow( + updatedInfoResp, + codecForKycProcessClientInformation(), + ); + + const sig = signAmlQuery(decodeCrock(amlKeypair.priv)); + const decisionsResp = await harnessHttpLib.fetch( + new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, + { headers: { "Taler-AML-Officer-Signature": encodeCrock(sig) } }, + ); + await readResponseJsonOrThrow(decisionsResp, codecForAny()); + t.assertDeepEqual(decisionsResp.status, 200); + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: wres.transactionId as TransactionIdStr, + txState: { major: TransactionMajorState.Done }, + }); +} diff --git a/packages/taler-harness/src/integrationtests/kyc-threshold-withdrawal-helper.ts b/packages/taler-harness/src/integrationtests/kyc-threshold-withdrawal-helper.ts @@ -0,0 +1,128 @@ +/* + This file is part of GNU Taler + (C) 2020, 2026 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. +*/ + +import { + Configuration, + NotificationType, + TransactionMajorState, + TransactionMinorState, + TransactionType, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + configureCommonKyc, + createKycTestkudosEnvironmentFull, + postAmlDecisionNoRules, +} from "../harness/environments.js"; +import { GlobalTestState } from "../harness/harness.js"; + +function adjustExchangeConfig(config: Configuration): void { + configureCommonKyc(config); + + config.setString("KYC-RULE-R1", "operation_type", "withdraw"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-RULE-R2", "operation_type", "withdraw"); + config.setString("KYC-RULE-R2", "enabled", "yes"); + config.setString("KYC-RULE-R2", "exposed", "yes"); + config.setString("KYC-RULE-R2", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R2", "threshold", "TESTKUDOS:300"); + config.setString("KYC-RULE-R2", "timeframe", "1d"); + config.setString("KYC-RULE-R2", "next_measures", "verboten"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "NONE"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); +} + +/** Run the common successful withdrawal that establishes an AML account. */ +export async function runKycThresholdWithdrawalScenario(t: GlobalTestState) { + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironmentFull(t, { adjustExchangeConfig }); + + const user = await bankClient.createRandomBankUser(); + bankClient.setAuth({ username: user.username, password: user.password }); + const wop = await bankClient.createWithdrawalOperation( + user.username, + "TESTKUDOS:20", + ); + + const withdrawalUrlInfo = await walletClient.client.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { talerWithdrawUri: wop.taler_withdraw_uri }, + ); + const withdrawalAmountInfo = await walletClient.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + amount: withdrawalUrlInfo.amount!, + exchangeBaseUrl: withdrawalUrlInfo.possibleExchanges[0].exchangeBaseUrl, + }, + ); + t.assertTrue(!!withdrawalAmountInfo.kycHardLimit); + t.assertAmountEquals(withdrawalAmountInfo.kycHardLimit, "TESTKUDOS:300"); + + const acceptResp = await walletClient.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + }, + ); + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: acceptResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.BankConfirmTransfer, + }, + }); + await bankClient.confirmWithdrawalOperation(user.username, { + withdrawalOperationId: wop.withdrawal_id, + }); + + await walletClient.waitForNotificationCond((x) => + x.type === NotificationType.TransactionStateTransition && + x.transactionId === acceptResp.transactionId && + x.newTxState.major === TransactionMajorState.Pending && + x.newTxState.minor === TransactionMinorState.KycRequired + ? x + : false, + ); + + const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, { + transactionId: acceptResp.transactionId, + }); + t.assertDeepEqual(txDet.type, TransactionType.Withdrawal); + const kycPaytoHash = txDet.kycPaytoHash; + t.assertTrue(!!kycPaytoHash); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: kycPaytoHash, + }); + await walletClient.waitForNotificationCond((x) => + x.type === NotificationType.TransactionStateTransition && + x.transactionId === acceptResp.transactionId && + x.newTxState.major === TransactionMajorState.Done + ? x + : false, + ); + + return { walletClient, bankClient, exchange, amlKeypair, user, kycPaytoHash }; +} diff --git a/packages/taler-harness/src/integrationtests/merchant-kyc-auth-helper.ts b/packages/taler-harness/src/integrationtests/merchant-kyc-auth-helper.ts @@ -0,0 +1,190 @@ +/* + This file is part of GNU Taler + (C) 2024, 2026 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. +*/ + +import { + AccessToken, + Configuration, + MerchantAccountKycStatus, + Paytos, + Result, + succeedOrThrow, + TalerMerchantInstanceHttpClient, + TalerWireGatewayHttpClient, +} from "@gnu-taler/taler-util"; +import { configureCommonKyc } from "../harness/environments.js"; +import { + BankService, + ExchangeService, + GlobalTestState, + MerchantService, +} from "../harness/harness.js"; + +const merchantOnboardingConfig = ` +[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 +`; + +export function configureMerchantDepositKyc( + config: Configuration, + threshold: string, + includeInformInvestigateSuccessor = false, +): void { + configureCommonKyc(config); + config.loadFromString(merchantOnboardingConfig); + config.setString("KYC-RULE-DEPOSIT-LIMIT", "operation_type", "deposit"); + config.setString( + "KYC-RULE-DEPOSIT-LIMIT", + "next_measures", + "merchant-onboarding", + ); + config.setString("KYC-RULE-DEPOSIT-LIMIT", "exposed", "yes"); + config.setString("KYC-RULE-DEPOSIT-LIMIT", "enabled", "yes"); + config.setString("KYC-RULE-DEPOSIT-LIMIT", "threshold", threshold); + config.setString("KYC-RULE-DEPOSIT-LIMIT", "timeframe", "1 days"); + if (includeInformInvestigateSuccessor) { + config.setString( + "KYC-MEASURE-INFORM-INVESTIGATE", + "check_name", + "SKIP", + ); + config.setString( + "KYC-MEASURE-INFORM-INVESTIGATE", + "program", + "NONE", + ); + config.setString( + "KYC-MEASURE-INFORM-INVESTIGATE", + "voluntary", + "no", + ); + config.setString( + "KYC-MEASURE-INFORM-INVESTIGATE", + "context", + "{}", + ); + } +} + +export function configureMerchantInfoDepositKyc(config: Configuration): void { + configureCommonKyc(config); + config.setString("KYC-RULE-R1", "operation_type", "deposit"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:0"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + config.setString("KYC-MEASURE-FM", "check_name", "SKIP"); + config.setString("KYC-MEASURE-FM", "context", "{}"); + config.setString("KYC-MEASURE-FM", "program", "P1"); + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); +} + +export async function doAccountKycAuth( + t: GlobalTestState, + args: { + exchange: ExchangeService; + merchant: MerchantService; + bank: BankService; + merchantInstId: string; + merchantInstPaytoUri: string; + merchantAccessToken: AccessToken; + wireGatewayApi: TalerWireGatewayHttpClient; + }, +): Promise<void> { + const merchantClient = new TalerMerchantInstanceHttpClient( + args.merchant.makeInstanceBaseUrl(args.merchantInstId), + ); + const initialStatus = succeedOrThrow( + await merchantClient.getCurrentInstanceKycStatus( + args.merchantAccessToken, + {}, + ), + ); + const initialRow = initialStatus.kyc_data.find( + (x) => x.exchange_url === args.exchange.baseUrl, + ); + t.assertTrue( + initialRow?.payto_kycauths != null && + initialRow.payto_kycauths.length === 1, + ); + const authPayto = Result.unpack( + Paytos.fromString(initialRow.payto_kycauths[0]), + ); + const authMessage = authPayto.params["message"]; + t.assertTrue(typeof authMessage === "string"); + t.assertTrue(authMessage.startsWith("KYC:")); + await args.wireGatewayApi.addKycAuth({ + auth: args.bank.getAdminAuth(), + body: { + amount: "TESTKUDOS:0.1", + debit_account: args.merchantInstPaytoUri, + account_pub: authMessage.substring(4), + }, + }); + await args.exchange.runWirewatchOnce(); + + const finalStatus = await merchantClient.getCurrentInstanceKycStatus( + args.merchantAccessToken, + { + longpoll: { + type: "state-exit", + status: MerchantAccountKycStatus.KYC_WIRE_REQUIRED, + timeout: 30_000, + }, + }, + ); + t.assertDeepEqual(finalStatus.case, "ok"); + const finalRow = finalStatus.body.kyc_data.find( + (x) => + x.exchange_url === args.exchange.baseUrl && + x.payto_uri === args.merchantInstPaytoUri, + ); + t.assertTrue(finalRow != null); + t.assertDeepEqual(finalRow.status, "ready"); + t.assertTrue(typeof finalRow.access_token === "string"); +} diff --git a/packages/taler-harness/src/integrationtests/test-kyc-form-compression.ts b/packages/taler-harness/src/integrationtests/test-kyc-form-compression.ts @@ -17,136 +17,17 @@ /** * Imports. */ -import { - codecForAny, - codecForKycProcessClientInformation, - Configuration, - decodeCrock, - encodeCrock, - j2s, - signAmlQuery, - TransactionIdStr, - TransactionMajorState, - TransactionMinorState, -} from "@gnu-taler/taler-util"; -import { - expectSuccessResponseOrThrow, - readResponseJsonOrThrow, -} from "@gnu-taler/taler-util/http"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - configureCommonKyc, - createKycTestkudosEnvironmentFull, - withdrawViaBankV3, -} from "../harness/environments.js"; -import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; - -function adjustExchangeConfig(config: Configuration) { - configureCommonKyc(config); - - config.setString("KYC-RULE-R1", "operation_type", "withdraw"); - config.setString("KYC-RULE-R1", "enabled", "yes"); - config.setString("KYC-RULE-R1", "exposed", "yes"); - config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); - config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); - config.setString("KYC-RULE-R1", "timeframe", "1d"); - config.setString("KYC-RULE-R1", "next_measures", "M1 M2"); - - config.setString("KYC-MEASURE-M1", "check_name", "C1"); - config.setString("KYC-MEASURE-M1", "context", "{}"); - config.setString("KYC-MEASURE-M1", "program", "P1"); - - config.setString("KYC-MEASURE-M2", "check_name", "C2"); - config.setString("KYC-MEASURE-M2", "context", "{}"); - config.setString("KYC-MEASURE-M2", "program", "NONE"); - - config.setString( - "AML-PROGRAM-P1", - "command", - "taler-exchange-helper-measure-test-form", - ); - config.setString("AML-PROGRAM-P1", "enabled", "true"); - config.setString( - "AML-PROGRAM-P1", - "description", - "test for FULL_NAME and DATE_OF_BIRTH", - ); - config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); - config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C1", "type", "FORM"); - config.setString("KYC-CHECK-C1", "form_name", "full_name_and_birthdate"); - config.setString("KYC-CHECK-C1", "description", "my check!"); - config.setString("KYC-CHECK-C1", "description_i18n", "{}"); - config.setString("KYC-CHECK-C1", "outputs", "FULL_NAME DATE_OF_BIRTH"); - config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C2", "type", "INFO"); - config.setString("KYC-CHECK-C2", "description", "my check info!"); - config.setString("KYC-CHECK-C2", "description_i18n", "{}"); - config.setString("KYC-CHECK-C2", "fallback", "FREEZE"); -} +import { GlobalTestState } from "../harness/harness.js"; +import { runKycFormWithdrawalScenario } from "./kyc-form-withdrawal-helper.js"; +/** Submit a large deflate-compressed KYC form and finish the withdrawal. */ export async function runKycFormCompressionTest(t: GlobalTestState) { - // Set up test environment - - const { walletClient, bankClient, exchange, amlKeypair } = - await createKycTestkudosEnvironmentFull(t, { adjustExchangeConfig }); - - // Withdraw digital cash into the wallet. - - const wres = await withdrawViaBankV3(t, { - amount: "TESTKUDOS:20", - bankClient, - exchange, - walletClient, - }); - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: wres.transactionId as TransactionIdStr, - txState: { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.KycRequired, - }, - }); - - const txDetails = await walletClient.call( - WalletApiOperation.GetTransactionById, - { - transactionId: wres.transactionId, - }, - ); - - console.log(j2s(txDetails)); - const accessToken = txDetails.kycAccessToken; - t.assertTrue(!!accessToken); - - const infoResp = await harnessHttpLib.fetch( - new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href, - ); - - const clientInfo = await readResponseJsonOrThrow( - infoResp, - codecForKycProcessClientInformation(), - ); - - console.log(j2s(clientInfo)); - - const kycId = clientInfo.requirements.find((x) => x.id != null)?.id; - t.assertTrue(!!kycId); - const CONTENTS = "JVBERi0xLjcKJcOkw7zDtsOfCjIgMCBvYmoKPDwvTGVuZ3RoIDMgMCBSL0ZpbHRlci9GbGF0ZURlY29kZT4+CnN0cmVhbQp4nC3NvQrCMBgF0P17ijs7JDdJ06RQCv0THDqIAedirehQMBR8fUF8gMOhMviIbvP+XOfbjm7q5Q2CijbCV17Z4BELo2JpkO9yPWCTcepFX/Z5W+a81LWe+tMANk03/DWI/JAuiS9VRAhOVbFCWqCPBsYirTUNLR0dC3qWDCwY6VixbdJLxiTnX3PGF2mOJVkKZW5kc3RyZWFtCmVuZG9iagoKMyAwIG9iagoxNDQKZW5kb2JqCgo3IDAgb2JqCjw8L0xlbmd0aCA4IDAgUi9GaWx0ZXIvRmxhdGVEZWNvZGUvTGVuZ3RoMSA5MzQ4Pj4Kc3RyZWFtCnic5Vl7cBt3nf98dyW/ZFta11blqrZ+7kZujCS/FKdJEzeqbclOnMRKbLdS2sRaS2tLxZZUSUlIOa4GWgguuYbSoxR6NNwA0+lkruumgFtKY44WjoFCOXpMn0c4ysHMNddQwuOgsW/2p7VjhxTmbu6/+zne/b7f318ipZA7qKISMxARiE8r2WuqLDUAvgdQTfxQgXUP1W0B6Awg/PNEdnL6M1+99TxgegIofWJy6sjEJ/9rrAWoTAJlm5Kqkpjv2OgFak8A2JhMqsqexSOlQO1LANYlpwvvOyH+1A3UvgPAO5WJK1dVtpcBdesBOKaV92XtpoAA1AUAsLQyrf7+c99MAHUxwJLPZvKFBI4uAexFnZ/Nqdmdnxl/DmDnAfE4AAJBP5UAlei4IJrMJaVl5RX4/3nMx1CHAXM3rMjy55ojnkQ9HgSW3tSxi8/FnUt/+L+Moqz4+jS+hCdwDC9jv8EIIYwUDuLlNeLfwA+R4lAY+/AoZt/F7EnMY58hF8O9eiaXPWE8gFP49hovYUzj/TiGL+Nl6sB3AMrgbSrDB/Ec5vE2dWDX5UwJ1QAmODixivoqPivcgx3CGwAe1DlCm2DDs3iIDgBUQAjHVjLe+idGP4oPABhGEofw0SLJ3P3OKyhf+jXuxQewAx/CjZhapfE0PSxWAOIIHsaX8A1Oa1tmlg6ItwlfEYQLnwTwCUziE1DoZUA4Jt74LhX6Hx9xFFXUIrpRfjmusAHWxT8InUvnxXWowOjSuWXa0uDSr0VlMW0aM11t7jZ998/5KPmEaRoVwNLPF9+/mDDvNn8JX8YjQKD/ln3RyOjI8N494aHdu3YO7tg+0B8K9vX23BjYdkP31i3Xb9503caujva2Vp93/bXN7nXyNU0uR61ks1ZXWSrKy0pLzCZRIHiZRrGgJrqZFFLkoKwM+Lws6Ej2+bxBORTTmMK0UEwzNcsDA5wkKxqLMa1Z0ZiyihzTAgrTJi6RDBQlAyuSZGNbsVV3ITPt+T6ZzdO+PRGZacf65CjTznJ4F4dNzRyp6pOjTU0+L+NR6dGyoBY6lJwNxvp8XpqzVPTKvWqFz4u5Ckuv3GvxeaGtl7NztP4G4oCwPnj9nICyKt2tJrqDSkIL74kE+5xNTVGfd7tWLfdxFnq5Sa2kVyvlJllKDx33sDnvwuzH520Yj3kqE3JCuTWiiUrU550Vg7OzH9Ukj9Yi92ktd7zh8HmDquaV+4KaR7c6uHfFz+BFl6SZ3TaZzf4GGsXks2+upSgGpcRt+w10UBN6NdobadKPMySHYrOzIZmFZmOzyvzSzLjMbPLsXGXlbDYYYxrCEY2U+aWn7nFqoY9HNVssSddHjdRDewe1K/bcEtEEd4glFU10a6J7m9y0ydkkrciE340NrbRXK9Er3NSkl+Ge+QDGfd4mbWZPpIgzjDsfR6DNE9WEmM5ZWObUjeqcmWXOinpMbvJ5B4cjs5rJvT0hB1Na4B5FmxnXmHKb3hjZplX/1tkkz9ZIbHNblMsyTXRvT6SYZm7WSnSt1QqaqVlXmbVxpPq3xddZ56xmapZq2GaZbW7T7QTlYMz4cyjp0GbGmc+rDXiKgzAS0QJ9LKgFFKNjwbn2tqAcVGIaxVJ9vJlam5zVauWele7qYQVTwxGuYqhptb0aYnFDS2sL8r1iwdlYXzEE3Za8J/Ik/Etn5jYw5yk/NiDapwvbeyOa2BycjSQmNFfMmdBYbIJFnE1aIKqREpUjalQfO9mmtZxx8uGI8lkZiQwOy4N79kU2GYEUGbo5kzt4iRk54iya0cxurcxdxiKCU4xqJrdNM7tZSDO55Z6tmsmtlbrLtFK3TSspUvXB7dnKIuTEsrTWckZrYUG1z5DT8TVGzfo49Q4sWyvRUY1ivQPOpmhT8fi8gmZyM8OxZnaX6UUdWGaJbqaZ3GWa4O4d4CS9lg596FlEVuWonGRaIBzRc9PLw6tsFIPX3OjVyBpsVbF8Xg1NgyMriF5MLeRxri6u1s/xFXTgEvb2ZTabLZMHh2d147JhEJrg3q5BH+HAJsnJ7wJ9oeWQIjMbCxUXenYuENCXOXm9bkTenpiVhyNbufTg3sgHnHfovmowSIMjPT7vnICeOZmO7pkL0NHhfZEnbQA7OhJ5XCChN9YTnVtHR/dEnmRAgFMFnaoTdYTpiG5pb+RxoYzLO58MADOca+IEjsfnCZxWtkwjxOeFIs1WdNTMHQUgID5vKnICy9ImxOfLirQZTuNnDnrJAhXmQFmgPFApVAnOOdJJj5sDZU8RUE44VUlV5JybEXr3cvI8zcyVB5xFiRmUU6AY4dHRi65H90VOVaKKnPwZjUZ79OPzBh1JeVD/ayXIEvqg/FU0ORuL6ssGuya4NcFNGsk3QBPkG+ZIKKnUKmS1R7PIPTp9m07fVqSX6PRSuUcjO/m82owm9IY10ifglkiTbNPYVd9xztrO6p2KenzeWdvPfSBsAcz3mLvhxGOBdIXdLpXX1wuS0HB1uSMWtZZvKx8qFy1ieTnMNkuZaBbHolVmsd4uCRDGorWo2dBA6xqotoFMDXT9sw2UaKC+Zfx8A73B0aLQmQYSXmigEw2UbaBwA+3fv3//2P7b9ZPTzwGdgG2dHgl+v9+xzV8EpJrNjraxA/s9Ug1t3iz5iz8d7f6u66Rru5rqSqUm431lXVPXdVKTdGXdlpMnhfqTwuGTguPkyQu/PHnhYye7nKbPXdXVddWFXwlW/f3HcWdXl1PouvBdZxcEhJfeFEPic6jD1TgW2FdPZL2qrM5a19BYj3DUWu+qFyrF+vrKmhp7OFpjqzTviVbaFxpJa6QTjXS8kWYaKdtIsUYKNxIa6YZwIwUaqb2RWCPZGukcl5tppDXZ7i+m7IFjm0eqwdpEjTyprraR/J0br6urJvmaZmnDRj+T6uiakrqmDc1k6r5zcuP97e1fvOnV737/NKUWH0hm6L5b6eWa2QfDNZZNrtY3yfzbtxcn9tJDj3zh1IP6p7EWQLCZj6Ecfx/Imi0V5SXhaDlgFs3hqFj3ooWetdATFvqChe630F0WKlgoYaF1Fqq1kMlCm89zieMWErIWilkobKGAhRYspFnoBEdtFoKFznH0uIVWixlJ377S97E1xeDlkODnBaHNHe3uYlev7WqizOcX60+coFCo3uerNwsOHwTsXnpTfFx8DhWw46nAByWzBWZc6SirDkfLbEJtOCrYmYPgoDMOCjuo3UE2B53j6AsOWnCQ5qATDjruoBkHZR0Uc1DAQUWVLQ9zUpiT2jnVxhmr9U9wzaJazEFGYkaTL0mtmHYut9JxY6A72snWdE1z14aN/k576YZm+ZqSulq7v3Oj+PjiwIsvvfTaj1954q8/8uGDhz941wy9uigt/uo/3/ndr1/6x6fO/Ozrz4J/wt69+BzdiR/BBm/AUQKYLBapRjT9XbRafOSWkuoXaihWQ/v3o83v4X75eHW0u2tLSrs2dm1ovtbwTnc+fJ/W6O7r6woMdXzsyffs2pStZVfIgY2bb+V+hKU3zc+IJ1GLXwcaK8zWWnNtnV0oq6gaEKqqaq0V5lJzOCqVWqstlvml3wc+XFE1YBEJJvuInfrstM5ONjuZ7HTeTk/Y6YSd7rfTXXYqcG47F/gCJybsNGIn2GnzeTu9wZGAnYR2OzFuA3aasVPMTmHOKNJfsNNpbnXGTlk7jXHiZSfO6Mvta9qFbX6/3++R/Mt3j9/R5l/dJrNsIZn4beMkv7MImZy7f/nzHYtfy9Dph37ys5F/+5fP0ESyVpi6cL94R73P57xwt6BeeED4YL3PV4fl/fu0+BxqsSfgk0pLqbKyzl4iQbJJQrVZEoVam60qHLVZSysrKsPRiroxO7l4lsXZKa4HHHq0Ywf2S349wprNncXbQr72mpLlu/DarqYrbyC/8GnP9Z0f6/z8Ys/hw1RTvvX5reJzi2mn/UKPvkkiq/cd7LyVf0TEjqU3xV+Yj+EKNGAmMFRrsqC+3mayNbqusIWjV9RZK8NRK0qvDkdLbfWAIFy5JyrY4aL+sIsCLmp3EXMRXLTgohlOKQIxTjdKbixGMRXJSGbtShSns0Rm0oYaf+eVzd16rfWsSF+M66RmmQk/uv2BxTtfeXEqU/I56iss/n7RNXPX7fuiucV3Qvvop78jurLp7vMO3x+erPfR88987VrhFxLP8dWln9Hz5m5YYKXSwFdRVVVSWWmTqkRrNVWK1WJALP/sLeIVP5boWxKFJNoo0Vck+pJEzRLZJSrSvyLRFyS6X6IPSURZiWIShSXqk2iDROskskkEiTafk+gNiV6UaEGiJ5Y1ZiRDYW"; - const uploadResp = await harnessHttpLib.fetch( - new URL(`kyc-upload/${kycId}`, exchange.baseUrl).href, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - compress: "deflate", - body: { + await runKycFormWithdrawalScenario(t, { + compress: "deflate", + body: { FULL_NAME: "Alice Abc", DATE_OF_BIRTH: "2000-01-01", STUFF: { @@ -207,49 +88,6 @@ export async function runKycFormCompressionTest(t: GlobalTestState) { }, ], }, - }, - }, - ); - - await expectSuccessResponseOrThrow(uploadResp); - - t.assertDeepEqual(uploadResp.status, 204); - - { - // Do a GET on kyc-info here as this reproduces a bug in the - // exchange. - const infoResp = await harnessHttpLib.fetch( - new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href, - ); - - await readResponseJsonOrThrow( - infoResp, - codecForKycProcessClientInformation(), - ); - } - - const sig = signAmlQuery(decodeCrock(amlKeypair.priv)); - - const decisionsResp = await harnessHttpLib.fetch( - new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, - { - headers: { - "Taler-AML-Officer-Signature": encodeCrock(sig), - }, - }, - ); - - const decisions = await readResponseJsonOrThrow(decisionsResp, codecForAny()); - console.log(j2s(decisions)); - - t.assertDeepEqual(decisionsResp.status, 200); - - // KYC should pass now - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: wres.transactionId as TransactionIdStr, - txState: { - major: TransactionMajorState.Done, }, }); } diff --git a/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts @@ -1,193 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 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 { - codecForAny, - codecForKycProcessClientInformation, - Configuration, - decodeCrock, - encodeCrock, - j2s, - signAmlQuery, - TransactionIdStr, - TransactionMajorState, - TransactionMinorState, -} from "@gnu-taler/taler-util"; -import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - configureCommonKyc, - createKycTestkudosEnvironmentFull, - withdrawViaBankV3, -} from "../harness/environments.js"; -import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; - -function adjustExchangeConfig(config: Configuration) { - configureCommonKyc(config); - - config.setString("KYC-RULE-R1", "operation_type", "withdraw"); - config.setString("KYC-RULE-R1", "enabled", "yes"); - config.setString("KYC-RULE-R1", "exposed", "yes"); - config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); - config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); - config.setString("KYC-RULE-R1", "timeframe", "1d"); - config.setString("KYC-RULE-R1", "next_measures", "M1 M2"); - - config.setString("KYC-MEASURE-M1", "check_name", "C1"); - config.setString("KYC-MEASURE-M1", "context", "{}"); - config.setString("KYC-MEASURE-M1", "program", "P1"); - - config.setString("KYC-MEASURE-M2", "check_name", "C2"); - config.setString("KYC-MEASURE-M2", "context", "{}"); - config.setString("KYC-MEASURE-M2", "program", "NONE"); - - config.setString( - "AML-PROGRAM-P1", - "command", - "taler-exchange-helper-measure-test-form", - ); - config.setString("AML-PROGRAM-P1", "enabled", "true"); - config.setString( - "AML-PROGRAM-P1", - "description", - "test for FULL_NAME and DATE_OF_BIRTH", - ); - config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); - config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C1", "type", "FORM"); - config.setString("KYC-CHECK-C1", "form_name", "full_name_and_birthdate"); - config.setString("KYC-CHECK-C1", "description", "my check!"); - config.setString("KYC-CHECK-C1", "description_i18n", "{}"); - config.setString("KYC-CHECK-C1", "outputs", "FULL_NAME DATE_OF_BIRTH"); - config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C2", "type", "INFO"); - config.setString("KYC-CHECK-C2", "description", "my check info!"); - config.setString("KYC-CHECK-C2", "description_i18n", "{}"); - config.setString("KYC-CHECK-C2", "fallback", "FREEZE"); -} - -export async function runKycFormWithdrawalTest(t: GlobalTestState) { - // Set up test environment - - const { walletClient, bankClient, exchange, amlKeypair } = - await createKycTestkudosEnvironmentFull(t, { adjustExchangeConfig }); - - // Withdraw digital cash into the wallet. - - const wres = await withdrawViaBankV3(t, { - amount: "TESTKUDOS:20", - bankClient, - exchange, - walletClient, - }); - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: wres.transactionId as TransactionIdStr, - txState: { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.KycRequired, - }, - }); - - const txDetails = await walletClient.call( - WalletApiOperation.GetTransactionById, - { - transactionId: wres.transactionId, - }, - ); - - console.log(j2s(txDetails)); - const accessToken = txDetails.kycAccessToken; - t.assertTrue(!!accessToken); - - const infoResp = await harnessHttpLib.fetch( - new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href, - ); - - const clientInfo = await readResponseJsonOrThrow( - infoResp, - codecForKycProcessClientInformation(), - ); - - console.log(j2s(clientInfo)); - - const kycId = clientInfo.requirements.find((x) => x.id != null)?.id; - t.assertTrue(!!kycId); - - const uploadResp = await harnessHttpLib.fetch( - new URL(`kyc-upload/${kycId}`, exchange.baseUrl).href, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: { - FULL_NAME: "Alice Abc", - DATE_OF_BIRTH: "2000-01-01", - FORM_ID: "full_name_and_birthdate", - }, - }, - ); - - console.log("resp status", uploadResp.status); - - t.assertDeepEqual(uploadResp.status, 204); - - { - // Do a GET on kyc-info here as this reproduces a bug in the - // exchange. - const infoResp = await harnessHttpLib.fetch( - new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href, - ); - - await readResponseJsonOrThrow( - infoResp, - codecForKycProcessClientInformation(), - ); - } - - const sig = signAmlQuery(decodeCrock(amlKeypair.priv)); - - const decisionsResp = await harnessHttpLib.fetch( - new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, - { - headers: { - "Taler-AML-Officer-Signature": encodeCrock(sig), - }, - }, - ); - - const decisions = await readResponseJsonOrThrow(decisionsResp, codecForAny()); - console.log(j2s(decisions)); - - t.assertDeepEqual(decisionsResp.status, 200); - - // KYC should pass now - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: wres.transactionId as TransactionIdStr, - txState: { - major: TransactionMajorState.Done, - }, - }); -} - -runKycFormWithdrawalTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit-form.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit-form.ts @@ -33,71 +33,18 @@ import { readResponseJsonOrThrow, readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; -import { - configureCommonKyc, - createKycTestkudosEnvironmentFull, -} from "../harness/environments.js"; +import { createKycTestkudosEnvironmentFull } from "../harness/environments.js"; import { delayMs, GlobalTestState, harnessHttpLib, } from "../harness/harness.js"; +import { configureMerchantDepositKyc } from "./merchant-kyc-auth-helper.js"; const logger = new Logger(`test-kyc-merchant-deposit.ts`); -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 - -# Successor produced by taler-exchange-helper-measure-inform-investigate. -[kyc-measure-inform-investigate] -CHECK_NAME = SKIP -PROGRAM = NONE -VOLUNTARY = NO -CONTEXT = {} - -[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:0 -TIMEFRAME = "1 days" -`; - function adjustExchangeConfig(config: Configuration) { - configureCommonKyc(config); - config.loadFromString(myAmlConfig); + configureMerchantDepositKyc(config, "TESTKUDOS:0", true); } export async function runKycMerchantDepositFormTest(t: GlobalTestState) { diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit-rewrite.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit-rewrite.ts @@ -28,41 +28,16 @@ import { succeedOrThrow, } from "@gnu-taler/taler-util"; import { - configureCommonKyc, createKycTestkudosEnvironmentFull, postAmlDecisionNoRules, } from "../harness/environments.js"; import { delayMs, GlobalTestState } from "../harness/harness.js"; +import { configureMerchantInfoDepositKyc } from "./merchant-kyc-auth-helper.js"; const logger = new Logger(`test-kyc-merchant-deposit-rewrite.ts`); function adjustExchangeConfig(config: Configuration) { - configureCommonKyc(config); - - config.setString("KYC-RULE-R1", "operation_type", "deposit"); - config.setString("KYC-RULE-R1", "enabled", "yes"); - config.setString("KYC-RULE-R1", "exposed", "yes"); - config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); - config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:0"); - config.setString("KYC-RULE-R1", "timeframe", "1d"); - config.setString("KYC-RULE-R1", "next_measures", "M1"); - - config.setString("KYC-MEASURE-M1", "check_name", "C1"); - config.setString("KYC-MEASURE-M1", "context", "{}"); - config.setString("KYC-MEASURE-M1", "program", "P1"); - - config.setString("KYC-MEASURE-FM", "check_name", "SKIP"); - config.setString("KYC-MEASURE-FM", "context", "{}"); - config.setString("KYC-MEASURE-FM", "program", "P1"); - - config.setString("AML-PROGRAM-P1", "command", "/bin/true"); - config.setString("AML-PROGRAM-P1", "enabled", "true"); - config.setString("AML-PROGRAM-P1", "description", "this does nothing"); - config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C1", "type", "INFO"); - config.setString("KYC-CHECK-C1", "description", "my check!"); - config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); + configureMerchantInfoDepositKyc(config); } function adjustMerchantConfig(config: Configuration) { diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts @@ -36,7 +36,6 @@ import { readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; import { - configureCommonKyc, createKycTestkudosEnvironmentFull, postAmlDecisionNoRules, } from "../harness/environments.js"; @@ -45,36 +44,12 @@ import { GlobalTestState, harnessHttpLib, } from "../harness/harness.js"; +import { configureMerchantInfoDepositKyc } from "./merchant-kyc-auth-helper.js"; const logger = new Logger(`test-kyc-merchant-deposit.ts`); function adjustExchangeConfig(config: Configuration) { - configureCommonKyc(config); - - config.setString("KYC-RULE-R1", "operation_type", "deposit"); - config.setString("KYC-RULE-R1", "enabled", "yes"); - config.setString("KYC-RULE-R1", "exposed", "yes"); - config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); - config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:0"); - config.setString("KYC-RULE-R1", "timeframe", "1d"); - config.setString("KYC-RULE-R1", "next_measures", "M1"); - - config.setString("KYC-MEASURE-M1", "check_name", "C1"); - config.setString("KYC-MEASURE-M1", "context", "{}"); - config.setString("KYC-MEASURE-M1", "program", "P1"); - - config.setString("KYC-MEASURE-FM", "check_name", "SKIP"); - config.setString("KYC-MEASURE-FM", "context", "{}"); - config.setString("KYC-MEASURE-FM", "program", "P1"); - - config.setString("AML-PROGRAM-P1", "command", "/bin/true"); - config.setString("AML-PROGRAM-P1", "enabled", "true"); - config.setString("AML-PROGRAM-P1", "description", "this does nothing"); - config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C1", "type", "INFO"); - config.setString("KYC-CHECK-C1", "description", "my check!"); - config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); + configureMerchantInfoDepositKyc(config); } export async function runKycMerchantDepositTest(t: GlobalTestState) { diff --git a/packages/taler-harness/src/integrationtests/test-kyc-new-measures-prog.ts b/packages/taler-harness/src/integrationtests/test-kyc-new-measures-prog.ts @@ -1,61 +1,24 @@ /* This file is part of GNU Taler - (C) 2024 Taler Systems S.A. + (C) 2024, 2026 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 { - codecForAny, - codecForKycProcessClientInformation, - codecOptional, - Configuration, - decodeCrock, - encodeCrock, - j2s, - signAmlQuery, - TalerKycAml, - TalerProtocolTimestamp, - TransactionIdStr, - TransactionMajorState, - TransactionMinorState, -} from "@gnu-taler/taler-util"; -import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - configureCommonKyc, - createKycTestkudosEnvironmentFull, - postAmlDecision, - withdrawViaBankV3, -} from "../harness/environments.js"; -import { GlobalTestState, harnessHttpLib, waitMs } from "../harness/harness.js"; +import { TalerKycAml, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; +import { GlobalTestState, waitMs } from "../harness/harness.js"; +import { runDynamicMeasureScenario } from "./kyc-dynamic-measure-helper.js"; export const AML_PROGRAM_TEST_KYC_NEW_MEASURES_PROG: TalerKycAml.AmlProgramDefinition = { name: "test-kyc-new-measures-prog", - logic: async (_input, config) => { - // Artificially delay the AML program. + logic: async (input, config) => { await waitMs(500); - const outcome: TalerKycAml.AmlOutcome = { + return { to_investigate: false, - // pushing to info into properties for testing purposes - properties: { - "this comes": "from the program", - input: _input as any, - config, - }, + properties: { "this comes": "from the program", input, config }, events: [], new_measures: "ask_more_info ask_basic_info", new_rules: { @@ -63,267 +26,46 @@ export const AML_PROGRAM_TEST_KYC_NEW_MEASURES_PROG: TalerKycAml.AmlProgramDefin rules: [], custom_measures: { ask_basic_info: { - context: { - // this is the context info that the KYC-SPA will see - infotype: "basic", - }, + context: { infotype: "basic" }, check_name: "C2", prog_name: "P2", }, ask_more_info: { - context: { - // this is the context info that the KYC-SPA will see - WAT: "REALLY?", - }, + context: { WAT: "REALLY?" }, check_name: "C2", prog_name: "P2", }, }, }, }; - return outcome; }, requiredAttributes: [], requiredInputs: [], requiredContext: [], }; -function adjustExchangeConfig(config: Configuration) { - configureCommonKyc(config); - - config.setString("KYC-RULE-R1", "operation_type", "withdraw"); - config.setString("KYC-RULE-R1", "enabled", "yes"); - config.setString("KYC-RULE-R1", "exposed", "yes"); - config.setString("KYC-RULE-R1", "is_and_combinator", "no"); - config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); - config.setString("KYC-RULE-R1", "timeframe", "1d"); - config.setString("KYC-RULE-R1", "next_measures", "M2"); - - config.setString("KYC-MEASURE-M1", "check_name", "C1"); - config.setString("KYC-MEASURE-M1", "context", "{}"); - config.setString("KYC-MEASURE-M1", "program", "P1"); - - config.setString("KYC-MEASURE-M2", "check_name", "C2"); - config.setString("KYC-MEASURE-M2", "context", "{}"); - config.setString("KYC-MEASURE-M2", "program", "P2"); - - config.setString("KYC-MEASURE-M3", "check_name", "SKIP"); - config.setString("KYC-MEASURE-M3", "context", "{}"); - config.setString("KYC-MEASURE-M3", "program", "P1"); - - config.setString( - "AML-PROGRAM-P1", - "command", - `taler-harness aml-program run-program --name ${AML_PROGRAM_TEST_KYC_NEW_MEASURES_PROG.name}`, - ); - config.setString("AML-PROGRAM-P1", "enabled", "true"); - config.setString("AML-PROGRAM-P1", "description", "remove all rules"); - config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); - config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); - - config.setString("AML-PROGRAM-P2", "command", "/bin/true"); - config.setString("AML-PROGRAM-P2", "enabled", "true"); - config.setString("AML-PROGRAM-P2", "description", "does nothing"); - config.setString("AML-PROGRAM-P2", "description_i18n", "{}"); - config.setString("AML-PROGRAM-P2", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C1", "type", "FORM"); - config.setString("KYC-CHECK-C1", "form_name", "myform"); - config.setString("KYC-CHECK-C1", "description", "my check!"); - config.setString("KYC-CHECK-C1", "description_i18n", "{}"); - config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate"); - config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C2", "type", "FORM"); - config.setString("KYC-CHECK-C2", "form_name", "dynamicform"); - config.setString("KYC-CHECK-C2", "description", "my check info!"); - config.setString("KYC-CHECK-C2", "description_i18n", "{}"); - config.setString("KYC-CHECK-C1", "outputs", "what_the_officer_asked"); - config.setString("KYC-CHECK-C2", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C3", "type", "INFO"); - config.setString("KYC-CHECK-C3", "description", "this is info c3"); - config.setString("KYC-CHECK-C3", "description_i18n", "{}"); - config.setString("KYC-CHECK-C3", "fallback", "FREEZE"); -} - -/** - * Test the usage of new_measures as the return - * value of an AML measure program. - */ +/** Verify that all measures returned by an AML program become requirements. */ export async function runKycNewMeasuresProgTest(t: GlobalTestState) { - // Set up test environment - - const { walletClient, bankClient, exchange, amlKeypair } = - await createKycTestkudosEnvironmentFull(t, { adjustExchangeConfig }); - - // Withdraw digital cash into the wallet. - let kycPaytoHash: string | undefined; - let accessToken: string | undefined; - let firstTransaction: string | undefined; - - { - // step 1) Withdraw to trigger AML - const wres = await withdrawViaBankV3(t, { - amount: "TESTKUDOS:20", - bankClient, - exchange, - walletClient, - }); - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: wres.transactionId as TransactionIdStr, - txState: { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.KycRequired, - }, - }); - - const txDetails = await walletClient.call( - WalletApiOperation.GetTransactionById, - { - transactionId: wres.transactionId, - }, - ); - - accessToken = txDetails.kycAccessToken; - kycPaytoHash = txDetails.kycPaytoHash; - firstTransaction = wres.transactionId; - } - - t.assertTrue(!!accessToken); - - { - // step 2) Check KYC info - const infoResp = await harnessHttpLib.fetch( - new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href, - ); - - const clientInfo = await readResponseJsonOrThrow( - infoResp, - codecOptional(codecForKycProcessClientInformation()), - ); - - console.log(j2s(clientInfo)); - t.assertDeepEqual(infoResp.status, 200); - } - - const sig = signAmlQuery(decodeCrock(amlKeypair.priv)); - { - // step 3) Apply Measure 3 with SKIP check - const decisionsResp = await harnessHttpLib.fetch( - new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, - { - headers: { - "Taler-AML-Officer-Signature": encodeCrock(sig), - }, - }, - ); - - console.log(decisionsResp.status); - t.assertDeepEqual(decisionsResp.status, 204); - - t.assertTrue(!!kycPaytoHash); - - await postAmlDecision(t, { - amlPriv: amlKeypair.priv, - amlPub: amlKeypair.pub, - exchangeBaseUrl: exchange.baseUrl, - paytoHash: kycPaytoHash, - // Immediately run M3 - newMeasures: "M3", - properties: { - form: { name: "string" }, - }, - newRules: { - expiration_time: TalerProtocolTimestamp.now(), - custom_measures: {}, - rules: [ - // No rules! - ], - }, - }); - } - - { - // step 4) Check KYC info, it should have the result - // of running program p1 - const decisionsResp = await harnessHttpLib.fetch( - new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, - { - headers: { - "Taler-AML-Officer-Signature": encodeCrock(sig), - }, - }, - ); - - const decisions = await readResponseJsonOrThrow( - decisionsResp, - codecForAny(), - ); - console.log(j2s(decisions)); - - t.assertDeepEqual(decisionsResp.status, 200); - } - - // Make sure that there can be another decision - await waitMs(2000); - - // Wait for the KYC program to run - const deadline = Date.now() + 30_000; - while (true) { - t.assertTrue(Date.now() < deadline, "timed out waiting for new measures"); - const infoResp = await harnessHttpLib.fetch( - new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href, - ); - - console.log(`kyc-info status: ${infoResp.status}`); - if (infoResp.status == 202) { - await waitMs(1000); - continue; - } - // KYC program still busy. - // In the future, this should long-poll. - if (infoResp.status == 204) { - await waitMs(1000); - continue; - } - - const respJson = await infoResp.json(); - console.log(j2s(respJson)); - - t.assertDeepEqual(infoResp.status, 200); - - const clientInfo = await readResponseJsonOrThrow( - infoResp, - codecOptional(codecForKycProcessClientInformation()), - ); - - if (clientInfo?.requirements.length == 0) { - console.log("requirements empty, waiting ..."); - await waitMs(1000); - continue; - } - - console.log(j2s(clientInfo)); - - // Both custom measures returned by the AML program must survive. A test - // of only the first requirement would miss one being silently dropped. - t.assertDeepEqual(clientInfo?.requirements.length, 2); - for (const requirement of clientInfo?.requirements ?? []) { - t.assertDeepEqual(requirement.form, "dynamicform"); - t.assertTrue(!!requirement.id); - } - const contexts = (clientInfo?.requirements ?? []) - .map((requirement) => JSON.stringify(requirement.context)) - .sort(); - t.assertDeepEqual( - contexts, - [JSON.stringify({ WAT: "REALLY?" }), JSON.stringify({ infotype: "basic" })].sort(), - ); - - break; - } + await runDynamicMeasureScenario(t, { + amlProgramName: AML_PROGRAM_TEST_KYC_NEW_MEASURES_PROG.name, + assertRequirements(info) { + t.assertDeepEqual(info.requirements.length, 2); + for (const requirement of info.requirements) { + t.assertDeepEqual(requirement.form, "dynamicform"); + t.assertTrue(!!requirement.id); + } + const contexts = info.requirements + .map((requirement) => JSON.stringify(requirement.context)) + .sort(); + t.assertDeepEqual( + contexts, + [ + JSON.stringify({ WAT: "REALLY?" }), + JSON.stringify({ infotype: "basic" }), + ].sort(), + ); + }, + }); } runKycNewMeasuresProgTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts b/packages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts @@ -1,61 +1,24 @@ /* This file is part of GNU Taler - (C) 2024 Taler Systems S.A. + (C) 2024, 2026 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 { - codecForAny, - codecForKycProcessClientInformation, - codecOptional, - Configuration, - decodeCrock, - encodeCrock, - j2s, - signAmlQuery, - TalerKycAml, - TalerProtocolTimestamp, - TransactionIdStr, - TransactionMajorState, - TransactionMinorState, -} from "@gnu-taler/taler-util"; -import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - configureCommonKyc, - createKycTestkudosEnvironmentFull, - postAmlDecision, - withdrawViaBankV3, -} from "../harness/environments.js"; -import { GlobalTestState, harnessHttpLib, waitMs } from "../harness/harness.js"; +import { TalerKycAml, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; +import { GlobalTestState, waitMs } from "../harness/harness.js"; +import { runDynamicMeasureScenario } from "./kyc-dynamic-measure-helper.js"; export const AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT: TalerKycAml.AmlProgramDefinition = { name: "from-attr-to-context", - logic: async (_input, config) => { - // Artificially delay the AML program. + logic: async (input, config) => { await waitMs(500); - const outcome: TalerKycAml.AmlOutcome = { + return { to_investigate: false, - // pushing to info into properties for testing purposes - properties: { - "this comes": "from the program", - input: _input as any, - config, - }, + properties: { "this comes": "from the program", input, config }, events: [], new_rules: { expiration_time: TalerProtocolTimestamp.zero(), @@ -63,237 +26,28 @@ export const AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT: TalerKycAml.AmlProgramDefin successor_measure: "ask_more_info", custom_measures: { ask_more_info: { - context: { - // this is the context info that the KYC-SPA will see - WAT: "REALLY?", - }, + context: { WAT: "REALLY?" }, check_name: "C2", prog_name: "P2", }, }, }, }; - return outcome; }, requiredAttributes: [], requiredInputs: [], requiredContext: [], }; -function adjustExchangeConfig(config: Configuration) { - configureCommonKyc(config); - - config.setString("KYC-RULE-R1", "operation_type", "withdraw"); - config.setString("KYC-RULE-R1", "enabled", "yes"); - config.setString("KYC-RULE-R1", "exposed", "yes"); - config.setString("KYC-RULE-R1", "is_and_combinator", "no"); - config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); - config.setString("KYC-RULE-R1", "timeframe", "1d"); - config.setString("KYC-RULE-R1", "next_measures", "M2"); - - config.setString("KYC-MEASURE-M1", "check_name", "C1"); - config.setString("KYC-MEASURE-M1", "context", "{}"); - config.setString("KYC-MEASURE-M1", "program", "P1"); - - config.setString("KYC-MEASURE-M2", "check_name", "C2"); - config.setString("KYC-MEASURE-M2", "context", "{}"); - config.setString("KYC-MEASURE-M2", "program", "P2"); - - config.setString("KYC-MEASURE-M3", "check_name", "SKIP"); - config.setString("KYC-MEASURE-M3", "context", "{}"); - config.setString("KYC-MEASURE-M3", "program", "P1"); - - config.setString( - "AML-PROGRAM-P1", - "command", - "taler-harness aml-program run-program --name from-attr-to-context", - ); - config.setString("AML-PROGRAM-P1", "enabled", "true"); - config.setString("AML-PROGRAM-P1", "description", "remove all rules"); - config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); - config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); - - config.setString("AML-PROGRAM-P2", "command", "/bin/true"); - config.setString("AML-PROGRAM-P2", "enabled", "true"); - config.setString("AML-PROGRAM-P2", "description", "does nothing"); - config.setString("AML-PROGRAM-P2", "description_i18n", "{}"); - config.setString("AML-PROGRAM-P2", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C1", "type", "FORM"); - config.setString("KYC-CHECK-C1", "form_name", "myform"); - config.setString("KYC-CHECK-C1", "description", "my check!"); - config.setString("KYC-CHECK-C1", "description_i18n", "{}"); - config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate"); - config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C2", "type", "FORM"); - config.setString("KYC-CHECK-C2", "form_name", "dynamicform"); - config.setString("KYC-CHECK-C2", "description", "my check info!"); - config.setString("KYC-CHECK-C2", "description_i18n", "{}"); - config.setString("KYC-CHECK-C1", "outputs", "what_the_officer_asked"); - config.setString("KYC-CHECK-C2", "fallback", "FREEZE"); - - config.setString("KYC-CHECK-C3", "type", "INFO"); - config.setString("KYC-CHECK-C3", "description", "this is info c3"); - config.setString("KYC-CHECK-C3", "description_i18n", "{}"); - config.setString("KYC-CHECK-C3", "fallback", "FREEZE"); -} - +/** Verify that an expired rule can produce a successor measure via SKIP. */ export async function runKycSkipExpirationTest(t: GlobalTestState) { - // Set up test environment - - const { walletClient, bankClient, exchange, amlKeypair } = - await createKycTestkudosEnvironmentFull(t, { adjustExchangeConfig }); - - // Withdraw digital cash into the wallet. - let kycPaytoHash: string | undefined; - let accessToken: string | undefined; - let firstTransaction: string | undefined; - - { - // step 1) Withdraw to trigger AML - const wres = await withdrawViaBankV3(t, { - amount: "TESTKUDOS:20", - bankClient, - exchange, - walletClient, - }); - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: wres.transactionId as TransactionIdStr, - txState: { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.KycRequired, - }, - }); - - const txDetails = await walletClient.call( - WalletApiOperation.GetTransactionById, - { - transactionId: wres.transactionId, - }, - ); - - accessToken = txDetails.kycAccessToken; - kycPaytoHash = txDetails.kycPaytoHash; - firstTransaction = wres.transactionId; - } - - t.assertTrue(!!accessToken); - - { - // step 2) Check KYC info - const infoResp = await harnessHttpLib.fetch( - new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href, - ); - - const clientInfo = await readResponseJsonOrThrow( - infoResp, - codecOptional(codecForKycProcessClientInformation()), - ); - - console.log(j2s(clientInfo)); - t.assertDeepEqual(infoResp.status, 200); - } - - const sig = signAmlQuery(decodeCrock(amlKeypair.priv)); - { - // step 3) Apply Measure 3 with SKIP check - const decisionsResp = await harnessHttpLib.fetch( - new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, - { - headers: { - "Taler-AML-Officer-Signature": encodeCrock(sig), - }, - }, - ); - - console.log(decisionsResp.status); - t.assertDeepEqual(decisionsResp.status, 204); - - t.assertTrue(!!kycPaytoHash); - - await postAmlDecision(t, { - amlPriv: amlKeypair.priv, - amlPub: amlKeypair.pub, - exchangeBaseUrl: exchange.baseUrl, - paytoHash: kycPaytoHash, - newMeasures: "M3", - properties: { - form: { name: "string" }, - }, - newRules: { - expiration_time: TalerProtocolTimestamp.now(), - custom_measures: {}, - rules: [ - // No rules! - ], - }, - }); - } - - { - // step 4) Check KYC info, it should have the result - // of running program p1 - const decisionsResp = await harnessHttpLib.fetch( - new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, - { - headers: { - "Taler-AML-Officer-Signature": encodeCrock(sig), - }, - }, - ); - - const decisions = await readResponseJsonOrThrow( - decisionsResp, - codecForAny(), - ); - console.log(j2s(decisions)); - - t.assertDeepEqual(decisionsResp.status, 200); - } - - // Make sure that there can be another decision - await waitMs(2000); - - // Wait for the KYC program to run - while (true) { - const infoResp = await harnessHttpLib.fetch( - new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href, - ); - - console.log(`kyc-info status: ${infoResp.status}`); - if (infoResp.status == 202) { - await waitMs(1000); - continue; - } - // KYC program still busy. - // In the future, this should long-poll. - if (infoResp.status == 204) { - await waitMs(1000); - continue; - } - - const respJson = await infoResp.json(); - console.log(j2s(respJson)); - - t.assertDeepEqual(infoResp.status, 200); - - const clientInfo = await readResponseJsonOrThrow( - infoResp, - codecOptional(codecForKycProcessClientInformation()), - ); - - console.log(j2s(clientInfo)); - - // Finally here we must see the officer defined form - t.assertDeepEqual(clientInfo?.requirements[0].context, { - // this is fixed by the aml program - WAT: "REALLY?", - }); - - break; - } + await runDynamicMeasureScenario(t, { + amlProgramName: AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT.name, + assertRequirements(info) { + t.assertDeepEqual(info.requirements.length, 1); + t.assertDeepEqual(info.requirements[0].context, { WAT: "REALLY?" }); + }, + }); } runKycSkipExpirationTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts @@ -1,178 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 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, - NotificationType, - TransactionMajorState, - TransactionMinorState, - TransactionType, -} from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - configureCommonKyc, - createKycTestkudosEnvironmentFull, - postAmlDecisionNoRules, -} from "../harness/environments.js"; -import { GlobalTestState } from "../harness/harness.js"; - -function adjustExchangeConfig(config: Configuration): void { - configureCommonKyc(config); - - config.setString("KYC-RULE-R1", "operation_type", "withdraw"); - config.setString("KYC-RULE-R1", "enabled", "yes"); - config.setString("KYC-RULE-R1", "exposed", "yes"); - config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); - config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); - config.setString("KYC-RULE-R1", "timeframe", "1d"); - config.setString("KYC-RULE-R1", "next_measures", "M1"); - - config.setString("KYC-RULE-R2", "operation_type", "withdraw"); - config.setString("KYC-RULE-R2", "enabled", "yes"); - config.setString("KYC-RULE-R2", "exposed", "yes"); - config.setString("KYC-RULE-R2", "is_and_combinator", "yes"); - config.setString("KYC-RULE-R2", "threshold", "TESTKUDOS:300"); - config.setString("KYC-RULE-R2", "timeframe", "1d"); - config.setString("KYC-RULE-R2", "next_measures", "verboten"); - - config.setString("KYC-MEASURE-M1", "check_name", "C1"); - config.setString("KYC-MEASURE-M1", "context", "{}"); - config.setString("KYC-MEASURE-M1", "program", "NONE"); - - config.setString("KYC-CHECK-C1", "type", "INFO"); - config.setString("KYC-CHECK-C1", "description", "my check!"); - config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); -} - -export async function runKycThresholdWithdrawalTest(t: GlobalTestState) { - // Set up test environment - - const { walletClient, bankClient, exchange, amlKeypair } = - await createKycTestkudosEnvironmentFull(t, { - adjustExchangeConfig, - }); - - // Withdraw digital cash into the wallet. - - const amount = "TESTKUDOS:20"; - const user = await bankClient.createRandomBankUser(); - bankClient.setAuth({ - username: user.username, - password: user.password, - }); - - const wop = await bankClient.createWithdrawalOperation(user.username, amount); - - // Hand it to the wallet - - const withdrawalUrlInfo = await walletClient.client.call( - WalletApiOperation.GetWithdrawalDetailsForUri, - { - talerWithdrawUri: wop.taler_withdraw_uri, - }, - ); - - const withdrawalAmountInfo = await walletClient.call( - WalletApiOperation.GetWithdrawalDetailsForAmount, - { - amount: withdrawalUrlInfo.amount!, - exchangeBaseUrl: withdrawalUrlInfo.possibleExchanges[0].exchangeBaseUrl, - }, - ); - - t.assertTrue(!!withdrawalAmountInfo.kycHardLimit); - t.assertAmountEquals(withdrawalAmountInfo.kycHardLimit, "TESTKUDOS:300"); - - // Withdraw - - const acceptResp = await walletClient.client.call( - WalletApiOperation.AcceptBankIntegratedWithdrawal, - { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }, - ); - - const withdrawalTxId = acceptResp.transactionId; - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: acceptResp.transactionId, - txState: { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.BankConfirmTransfer, - }, - }); - - // Confirm it - - await bankClient.confirmWithdrawalOperation(user.username, { - withdrawalOperationId: wop.withdrawal_id, - }); - - t.logStep("waiting for pending(kyc-required)"); - - const kycNotificationCond = walletClient.waitForNotificationCond((x) => { - if ( - x.type === NotificationType.TransactionStateTransition && - x.transactionId === withdrawalTxId && - x.newTxState.major === TransactionMajorState.Pending && - x.newTxState.minor === TransactionMinorState.KycRequired - ) { - return x; - } - return false; - }); - - await kycNotificationCond; - - const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, { - transactionId: withdrawalTxId, - }); - - t.assertDeepEqual(txDet.type, TransactionType.Withdrawal); - - const kycPaytoHash = txDet.kycPaytoHash; - t.assertTrue(!!kycPaytoHash); - - t.logStep("posting aml decision"); - - await postAmlDecisionNoRules(t, { - amlPriv: amlKeypair.priv, - amlPub: amlKeypair.pub, - exchangeBaseUrl: exchange.baseUrl, - paytoHash: kycPaytoHash, - }); - - t.logStep("waiting for withdrawal to be done"); - - const doneNotificationCond = walletClient.waitForNotificationCond((x) => { - if ( - x.type === NotificationType.TransactionStateTransition && - x.transactionId === withdrawalTxId && - x.newTxState.major === TransactionMajorState.Done - ) { - return x; - } - return false; - }); - - await doneNotificationCond; -} - -runKycThresholdWithdrawalTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-withdrawal-verboten.ts b/packages/taler-harness/src/integrationtests/test-kyc-withdrawal-verboten.ts @@ -24,156 +24,15 @@ import { TalerProtocolTimestamp, TransactionMajorState, TransactionMinorState, - TransactionType, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - configureCommonKyc, - createKycTestkudosEnvironmentFull, - postAmlDecision, - postAmlDecisionNoRules, -} from "../harness/environments.js"; +import { postAmlDecision } from "../harness/environments.js"; import { GlobalTestState } from "../harness/harness.js"; +import { runKycThresholdWithdrawalScenario } from "./kyc-threshold-withdrawal-helper.js"; export async function runKycWithdrawalVerbotenTest(t: GlobalTestState) { - // Set up test environment - - const { walletClient, bankClient, exchange, amlKeypair } = - await createKycTestkudosEnvironmentFull(t, { - adjustExchangeConfig(config) { - configureCommonKyc(config); - - config.setString("KYC-RULE-R1", "operation_type", "withdraw"); - config.setString("KYC-RULE-R1", "enabled", "yes"); - config.setString("KYC-RULE-R1", "exposed", "yes"); - config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); - config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); - config.setString("KYC-RULE-R1", "timeframe", "1d"); - config.setString("KYC-RULE-R1", "next_measures", "M1"); - - config.setString("KYC-RULE-R2", "operation_type", "withdraw"); - config.setString("KYC-RULE-R2", "enabled", "yes"); - config.setString("KYC-RULE-R2", "exposed", "yes"); - config.setString("KYC-RULE-R2", "is_and_combinator", "yes"); - config.setString("KYC-RULE-R2", "threshold", "TESTKUDOS:300"); - config.setString("KYC-RULE-R2", "timeframe", "1d"); - config.setString("KYC-RULE-R2", "next_measures", "verboten"); - - config.setString("KYC-MEASURE-M1", "check_name", "C1"); - config.setString("KYC-MEASURE-M1", "context", "{}"); - config.setString("KYC-MEASURE-M1", "program", "NONE"); - - config.setString("KYC-CHECK-C1", "type", "INFO"); - config.setString("KYC-CHECK-C1", "description", "my check!"); - config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); - }, - }); - - // Withdraw digital cash into the wallet. - - const amount = "TESTKUDOS:20"; - const user = await bankClient.createRandomBankUser(); - bankClient.setAuth({ - username: user.username, - password: user.password, - }); - - const wop = await bankClient.createWithdrawalOperation(user.username, amount); - - // Hand it to the wallet - - const withdrawalUrlInfo = await walletClient.client.call( - WalletApiOperation.GetWithdrawalDetailsForUri, - { - talerWithdrawUri: wop.taler_withdraw_uri, - }, - ); - - const withdrawalAmountInfo = await walletClient.call( - WalletApiOperation.GetWithdrawalDetailsForAmount, - { - amount: withdrawalUrlInfo.amount!, - exchangeBaseUrl: withdrawalUrlInfo.possibleExchanges[0].exchangeBaseUrl, - }, - ); - - t.assertTrue(!!withdrawalAmountInfo.kycHardLimit); - t.assertAmountEquals(withdrawalAmountInfo.kycHardLimit, "TESTKUDOS:300"); - - // Withdraw - - const acceptResp = await walletClient.client.call( - WalletApiOperation.AcceptBankIntegratedWithdrawal, - { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }, - ); - - const withdrawalTxId = acceptResp.transactionId; - - // Confirm it - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: acceptResp.transactionId, - txState: { - major: TransactionMajorState.Pending, - minor: TransactionMinorState.BankConfirmTransfer, - }, - }); - - await bankClient.confirmWithdrawalOperation(user.username, { - withdrawalOperationId: wop.withdrawal_id, - }); - - t.logStep("waiting for pending(kyc-required)"); - - const kycNotificationCond = walletClient.waitForNotificationCond((x) => { - if ( - x.type === NotificationType.TransactionStateTransition && - x.transactionId === withdrawalTxId && - x.newTxState.major === TransactionMajorState.Pending && - x.newTxState.minor === TransactionMinorState.KycRequired - ) { - return x; - } - return false; - }); - - await kycNotificationCond; - - const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, { - transactionId: withdrawalTxId, - }); - - t.assertDeepEqual(txDet.type, TransactionType.Withdrawal); - - const kycPaytoHash = txDet.kycPaytoHash; - t.assertTrue(!!kycPaytoHash); - - t.logStep("posting aml decision"); - - await postAmlDecisionNoRules(t, { - amlPriv: amlKeypair.priv, - amlPub: amlKeypair.pub, - exchangeBaseUrl: exchange.baseUrl, - paytoHash: kycPaytoHash, - }); - - t.logStep("waiting for withdrawal to be done"); - - const doneNotificationCond = walletClient.waitForNotificationCond((x) => { - if ( - x.type === NotificationType.TransactionStateTransition && - x.transactionId === withdrawalTxId && - x.newTxState.major === TransactionMajorState.Done - ) { - return x; - } - return false; - }); - - await doneNotificationCond; + const { walletClient, bankClient, exchange, amlKeypair, user, kycPaytoHash } = + await runKycThresholdWithdrawalScenario(t); // Now that the first withdrawal has succeeded, we freeze the account. diff --git a/packages/taler-harness/src/integrationtests/test-merchant-acctsel.ts b/packages/taler-harness/src/integrationtests/test-merchant-acctsel.ts @@ -118,64 +118,21 @@ export async function runMerchantAcctselTest(t: GlobalTestState) { merchant.makeInstanceBaseUrl(), ); - { - const ordResp1 = succeedOrThrow( - await merchApi.createOrder(adminAccessToken, { - payment_target: "iban", - order: { - amount: "TESTKUDOS:5", - summary: "Test!", - }, - }), - ); - - console.log(j2s(ordResp1)); - - const ordDet1 = succeedOrThrow( - await merchApi.getOrderDetails(adminAccessToken, ordResp1.order_id), - ); - - t.assertDeepEqual(ordDet1.order_status, "unpaid"); - - const preparePayResult = await walletClient.call( - WalletApiOperation.PreparePayForUriV2, - { - talerPayUri: ordDet1.taler_pay_uri, - }, - ); - - // The order is only claimed once the download task has run. - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: preparePayResult.transactionId, - txState: { - major: TransactionMajorState.Dialog, - minor: TransactionMinorState.Proposed, - }, - }); - - const ordDet2 = succeedOrThrow( - await merchApi.getOrderDetails(adminAccessToken, ordResp1.order_id), - ); - - console.log(j2s(ordDet2)); - - t.assertDeepEqual(ordDet2.order_status, "claimed"); - - const numExch1 = ordDet2.contract_terms.exchanges.filter( - (x) => x.url === exchange1.baseUrl, - ).length; - const numExch2 = ordDet2.contract_terms.exchanges.filter( - (x) => x.url === exchange2.baseUrl, - ).length; - - t.assertDeepEqual(numExch1, 0); - t.assertDeepEqual(numExch2, 1); - } - - { + for (const scenario of [ + { + paymentTarget: "iban", + expectedExchange1: 0, + expectedExchange2: 1, + }, + { + paymentTarget: "x-taler-bank", + expectedExchange1: 1, + expectedExchange2: 0, + }, + ]) { const ordResp1 = succeedOrThrow( await merchApi.createOrder(adminAccessToken, { - payment_target: "x-taler-bank", + payment_target: scenario.paymentTarget, order: { amount: "TESTKUDOS:5", summary: "Test!", @@ -222,8 +179,8 @@ export async function runMerchantAcctselTest(t: GlobalTestState) { (x) => x.url === exchange2.baseUrl, ).length; - t.assertDeepEqual(numExch1, 1); - t.assertDeepEqual(numExch2, 0); + t.assertDeepEqual(numExch1, scenario.expectedExchange1); + t.assertDeepEqual(numExch2, scenario.expectedExchange2); } } 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 @@ -25,17 +25,12 @@ import { j2s, Logger, MerchantAccountKycStatus, - Paytos, - Result, succeedOrThrow, TalerMerchantInstanceHttpClient, TalerProtocolDuration, TalerWireGatewayHttpClient, } from "@gnu-taler/taler-util"; -import { - configureCommonKyc, - createKycTestkudosEnvironmentFull, -} from "../harness/environments.js"; +import { createKycTestkudosEnvironmentFull } from "../harness/environments.js"; import { BankService, ExchangeService, @@ -43,53 +38,13 @@ import { 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" -`; +import { + configureMerchantDepositKyc, + doAccountKycAuth as runAccountKycAuth, +} from "./merchant-kyc-auth-helper.js"; function adjustExchangeConfig(config: Configuration) { - configureCommonKyc(config); - config.loadFromString(myAmlConfig); + configureMerchantDepositKyc(config, "TESTKUDOS:1"); } const logger = new Logger("test-merchant-kyc-auth-multi.ts"); @@ -106,68 +61,7 @@ async function doAccountKycAuth( 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 = Result.unpack(Paytos.fromString(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, - { - longpoll: { - type: "state-exit", - timeout: 30000, - status: MerchantAccountKycStatus.KYC_WIRE_REQUIRED, - }, - }, - ); - 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"); - } + await runAccountKycAuth(t, args); } /** diff --git a/packages/taler-harness/src/integrationtests/test-merchant-payto-reuse.ts b/packages/taler-harness/src/integrationtests/test-merchant-payto-reuse.ts @@ -23,11 +23,6 @@ import { ConfirmPayResultType, encodeCrock, getRandomBytes, - j2s, - Logger, - MerchantAccountKycStatus, - Paytos, - Result, succeedOrThrow, TalerMerchantInstanceHttpClient, TalerProtocolDuration, @@ -37,7 +32,6 @@ import { } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { - configureCommonKyc, createKycTestkudosEnvironmentFull, withdrawViaBankV4, } from "../harness/environments.js"; @@ -48,57 +42,15 @@ import { 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-three] -OPERATION_TYPE = DEPOSIT -NEXT_MEASURES = merchant-onboarding -EXPOSED = YES -ENABLED = YES -THRESHOLD = TESTKUDOS:3 -TIMEFRAME = "1 days" -`; +import { + configureMerchantDepositKyc, + doAccountKycAuth as runAccountKycAuth, +} from "./merchant-kyc-auth-helper.js"; function adjustExchangeConfig(config: Configuration) { - configureCommonKyc(config); - config.loadFromString(myAmlConfig); + configureMerchantDepositKyc(config, "TESTKUDOS:3"); } -const logger = new Logger("test-merchant-kyc-auth-multi.ts"); - async function doAccountKycAuth( t: GlobalTestState, args: { @@ -111,72 +63,7 @@ async function doAccountKycAuth( 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 = Result.unpack( - Paytos.fromString(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, - }, - }); - await exchange.runWirewatchOnce(); - } - - // Wait for auth transfer to be registered by the exchange - { - const kycStatus = await merchantClient.getCurrentInstanceKycStatus( - merchantAccessToken, - { - longpoll: { - type: "state-exit", - status: MerchantAccountKycStatus.KYC_WIRE_REQUIRED, - 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"); - } + await runAccountKycAuth(t, args); } /** diff --git a/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts @@ -1,22 +1,12 @@ /* This file is part of GNU Taler - (C) 2020 Taler Systems S.A. + (C) 2020, 2026 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, Duration, @@ -33,63 +23,52 @@ import { withdrawViaBankV3, } from "../harness/environments.js"; import { - ExchangeServiceInterface, GlobalTestState, harnessHttpLib, MerchantServiceInterface, WalletClient, } from "../harness/harness.js"; -async function testRefundApiWithFulfillmentUrl( +async function testRefundApi( t: GlobalTestState, env: { merchant: MerchantServiceInterface; walletClient: WalletClient; merchantAdminAccessToken: AccessToken; - exchange: ExchangeServiceInterface; }, + fulfillment: + | { fulfillment_url: string } + | { fulfillment_message: string }, ): Promise<void> { const { walletClient, merchant, merchantAdminAccessToken } = env; - const merchantClient = new TalerMerchantInstanceHttpClient( merchant.makeInstanceBaseUrl(), ); - - // Set up order. const orderResp = succeedOrThrow( await merchantClient.createOrder(merchantAdminAccessToken, { order: { summary: "Buy me!", amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/fulfillment", + ...fulfillment, }, refund_delay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 5 }), ), }), ); - let orderStatus = succeedOrThrow( await merchantClient.getOrderDetails( merchantAdminAccessToken, orderResp.order_id, ), ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - + t.assertDeepEqual(orderStatus.order_status, "unpaid"); const talerPayUri = orderStatus.taler_pay_uri; - const orderId = orderResp.order_id; - - // Make wallet pay for the order let preparePayResult = await walletClient.call( WalletApiOperation.PreparePayForUriV2, - { - talerPayUri, - }, + { talerPayUri }, ); - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { transactionId: preparePayResult.transactionId, txState: { @@ -97,237 +76,68 @@ async function testRefundApiWithFulfillmentUrl( minor: TransactionMinorState.Proposed, }, }); - await walletClient.call(WalletApiOperation.ConfirmPay, { transactionId: preparePayResult.transactionId, choiceIndex: 0, }); - - // Check if payment was successful. - orderStatus = succeedOrThrow( await merchantClient.getOrderDetails( merchantAdminAccessToken, orderResp.order_id, ), ); - - t.assertTrue(orderStatus.order_status === "paid"); + t.assertDeepEqual(orderStatus.order_status, "paid"); preparePayResult = await walletClient.call( WalletApiOperation.PreparePayForUriV2, - { - talerPayUri, - }, + { talerPayUri }, ); - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { transactionId: preparePayResult.transactionId, - txState: { - major: TransactionMajorState.Done, - }, + txState: { major: TransactionMajorState.Done }, }); - const payTx = await walletClient.call(WalletApiOperation.GetTransactionById, { transactionId: preparePayResult.transactionId, }); - - t.assertTrue(payTx.type === TransactionType.Payment); + t.assertDeepEqual(payTx.type, TransactionType.Payment); t.assertTrue(payTx.info != null); - await merchantClient.addRefund(merchantAdminAccessToken, orderResp.order_id, { - refund: "TESTKUDOS:5", - reason: "foo", - }); - - orderStatus = succeedOrThrow( - await merchantClient.getOrderDetails( - merchantAdminAccessToken, - orderResp.order_id, - ), - ); - - t.assertTrue(orderStatus.order_status === "paid"); - - t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5"); - - // Now test what the merchant gives as a response for various requests to the - // public order status URL! - - let publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - publicOrderStatusUrl.searchParams.set( - "h_contract", - payTx.info.contractTermsHash, - ); - - let publicOrderStatusResp = await harnessHttpLib.fetch( - publicOrderStatusUrl.href, - ); - const respData = await publicOrderStatusResp.json(); - t.assertTrue(publicOrderStatusResp.status === 200); - t.assertAmountEquals(respData.refund_amount, "TESTKUDOS:5"); - - publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - console.log(`requesting order status via '${publicOrderStatusUrl.href}'`); - publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href); - console.log(publicOrderStatusResp.status); - console.log(await publicOrderStatusResp.json()); - // We didn't give any authentication, so we should get a fulfillment URL back - t.assertTrue(publicOrderStatusResp.status === 403); -} - -async function testRefundApiWithFulfillmentMessage( - t: GlobalTestState, - env: { - merchant: MerchantServiceInterface; - walletClient: WalletClient; - merchantAdminAccessToken: AccessToken; - exchange: ExchangeServiceInterface; - }, -): Promise<void> { - const { walletClient, merchant, merchantAdminAccessToken } = env; - - const merchantClient = new TalerMerchantInstanceHttpClient( - merchant.makeInstanceBaseUrl(), - ); - - // Set up order. - const orderResp = succeedOrThrow( - await merchantClient.createOrder(merchantAdminAccessToken, { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_message: "Thank you for buying foobar", - }, - refund_delay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 5 }), - ), - }), - ); - - let orderStatus = succeedOrThrow( - await merchantClient.getOrderDetails( - merchantAdminAccessToken, - orderResp.order_id, - ), - ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUri = orderStatus.taler_pay_uri; - const orderId = orderResp.order_id; - - // Make wallet pay for the order - - let preparePayResult = await walletClient.call( - WalletApiOperation.PreparePayForUriV2, - { - talerPayUri, - }, + await merchantClient.addRefund( + merchantAdminAccessToken, + orderResp.order_id, + { refund: "TESTKUDOS:5", reason: "foo" }, ); - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: preparePayResult.transactionId, - txState: { - major: TransactionMajorState.Dialog, - minor: TransactionMinorState.Proposed, - }, - }); - - await walletClient.call(WalletApiOperation.ConfirmPay, { - transactionId: preparePayResult.transactionId, - choiceIndex: 0, - }); - - // Check if payment was successful. - orderStatus = succeedOrThrow( await merchantClient.getOrderDetails( merchantAdminAccessToken, orderResp.order_id, ), ); - - t.assertTrue(orderStatus.order_status === "paid"); - - preparePayResult = await walletClient.call( - WalletApiOperation.PreparePayForUriV2, - { - talerPayUri, - }, - ); - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: preparePayResult.transactionId, - txState: { - major: TransactionMajorState.Done, - }, - }); - - const payTx = await walletClient.call(WalletApiOperation.GetTransactionById, { - transactionId: preparePayResult.transactionId, - }); - - t.assertTrue(payTx.type === TransactionType.Payment); - t.assertTrue(payTx.info != null); - - await merchantClient.addRefund(merchantAdminAccessToken, orderId, { - refund: "TESTKUDOS:5", - reason: "foo", - }); - - orderStatus = succeedOrThrow( - await merchantClient.getOrderDetails(merchantAdminAccessToken, orderId), - ); - - t.assertTrue(orderStatus.order_status === "paid"); - + t.assertDeepEqual(orderStatus.order_status, "paid"); t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5"); - // Now test what the merchant gives as a response for various requests to the - // public order status URL! - - let publicOrderStatusUrl = new URL( - `orders/${orderId}`, + const authenticatedUrl = new URL( + `orders/${orderResp.order_id}`, merchant.makeInstanceBaseUrl(), ); - publicOrderStatusUrl.searchParams.set( - "h_contract", - payTx.info.contractTermsHash, - ); + authenticatedUrl.searchParams.set("h_contract", payTx.info.contractTermsHash); + const authenticatedResp = await harnessHttpLib.fetch(authenticatedUrl.href); + const authenticatedBody = await authenticatedResp.json(); + t.assertDeepEqual(authenticatedResp.status, 200); + t.assertAmountEquals(authenticatedBody.refund_amount, "TESTKUDOS:5"); - let publicOrderStatusResp = await harnessHttpLib.fetch( - publicOrderStatusUrl.href, + const unauthenticatedResp = await harnessHttpLib.fetch( + new URL( + `orders/${orderResp.order_id}`, + merchant.makeInstanceBaseUrl(), + ).href, ); - let respData = await publicOrderStatusResp.json(); - console.log(respData); - t.assertTrue(publicOrderStatusResp.status === 200); - t.assertAmountEquals(respData.refund_amount, "TESTKUDOS:5"); - - publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - - publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl.href); - respData = await publicOrderStatusResp.json(); - console.log(respData); - // We didn't give any authentication, so we should get a fulfillment URL back - t.assertTrue(publicOrderStatusResp.status === 403); + await unauthenticatedResp.json(); + t.assertDeepEqual(unauthenticatedResp.status, 403); } -/** - * Test case for the refund API of the merchant backend. - */ +/** Test refund status for both fulfillment URL and message orders. */ export async function runMerchantRefundApiTest(t: GlobalTestState) { - // Set up test environment - const { walletClient, bankClient, @@ -335,9 +145,6 @@ export async function runMerchantRefundApiTest(t: GlobalTestState) { merchant, merchantAdminAccessToken, } = await createSimpleTestkudosEnvironmentV3(t); - - // Withdraw digital cash into the wallet. - const wres = await withdrawViaBankV3(t, { walletClient, bankClient, @@ -346,19 +153,13 @@ export async function runMerchantRefundApiTest(t: GlobalTestState) { }); await wres.withdrawalFinishedCond; - await testRefundApiWithFulfillmentUrl(t, { - walletClient, - exchange, - merchant, - merchantAdminAccessToken, - }); - - await testRefundApiWithFulfillmentMessage(t, { - walletClient, - exchange, - merchant, - merchantAdminAccessToken, - }); + const env = { walletClient, merchant, merchantAdminAccessToken }; + for (const fulfillment of [ + { fulfillment_url: "https://example.com/fulfillment" }, + { fulfillment_message: "Thank you for buying foobar" }, + ]) { + await testRefundApi(t, env, fulfillment); + } } runMerchantRefundApiTest.suites = ["merchant"]; diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation-two-bank-account.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation-two-bank-account.ts @@ -1,222 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 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 { - alternativeOrThrow, - Duration, - HttpStatusCode, - LoginTokenScope, - MerchantAuthMethod, - succeedOrThrow, - TalerMerchantInstanceHttpClient, - TalerMerchantManagementHttpClient, -} from "@gnu-taler/taler-util"; -import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; -import { - getTestHarnessPaytoForLabel, - GlobalTestState, -} from "../harness/harness.js"; -import { - configureTestMerchantMfa, - doChallenge, - makeMfaConfigEmailSms, - solveMFA, -} from "../harness/tan-helper.js"; - -/** - * Activate a self-provisioned instance with email/SMS MFA and verify that two - * distinct bank accounts survive the MFA-protected account-add flow. - */ -export async function runMerchantSelfProvisionActivationTwoBankAccountsTest( - t: GlobalTestState, -) { - // Set up test environment - - const instanceInfo = { - id: "self-instance", - name: "My instance", - auth: { - method: MerchantAuthMethod.TOKEN, - password: "123", - }, - default_pay_delay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ days: 14 }), - ), - default_wire_transfer_delay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ days: 14 }), - ), - jurisdiction: {}, - address: {}, - email: "some@taler.net", - phone_number: "+1111", - use_stefan: false, - }; - - const mfaConfig = makeMfaConfigEmailSms( - t, - instanceInfo.email, - instanceInfo.phone_number, - ); - - const { merchant, merchantAdminAccessToken } = - await createSimpleTestkudosEnvironmentV3(t, undefined, { - additionalMerchantConfig(m) { - m.modifyConfig(async (cfg) => { - cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); - configureTestMerchantMfa(cfg, mfaConfig); - }); - }, - }); - - const merchantClient = new TalerMerchantManagementHttpClient( - merchant.makeInstanceBaseUrl(), - ); - - { - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } - - const signupStart = alternativeOrThrow( - await merchantClient.createInstanceSelfProvision(instanceInfo), - HttpStatusCode.Accepted, - ); - - // creation requires 2fa - t.assertDeepEqual(signupStart.challenges.length, 2); - t.assertDeepEqual(signupStart.combi_and, true); - - const firstChallenge = signupStart.challenges[0]; - const secondChallenge = signupStart.challenges[1]; - - { - // new instance is pending, then is not listed - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } - - await doChallenge( - t, - merchantClient, - firstChallenge.challenge_id, - instanceInfo.email, - mfaConfig.email.path, - ); - - await doChallenge( - t, - merchantClient, - secondChallenge.challenge_id, - instanceInfo.phone_number, - mfaConfig.sms.path, - ); - - const completeSignup = await merchantClient.createInstanceSelfProvision( - instanceInfo, - { - challengeIds: [firstChallenge.challenge_id, secondChallenge.challenge_id], - }, - ); - - t.assertDeepEqual(completeSignup.type, "ok"); - - const instanceApi = new TalerMerchantInstanceHttpClient( - merchantClient.getSubInstanceAPI(instanceInfo.id), - merchantClient.httpLib, - ); - - { - // new instance is completed, now it should be visible - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 3); - } - - const loginChallenge = alternativeOrThrow( - await instanceApi.createAccessToken( - instanceInfo.id, - instanceInfo.auth.password, - { - scope: LoginTokenScope.All, - }, - ), - HttpStatusCode.Accepted, - ); - - await solveMFA(t, merchantClient, loginChallenge, mfaConfig); - - const { access_token: token } = succeedOrThrow( - await instanceApi.createAccessToken( - instanceInfo.id, - instanceInfo.auth.password, - { - scope: LoginTokenScope.All, - }, - { - challengeIds: loginChallenge.challenges.map((c) => c.challenge_id), - }, - ), - ); - - const bankAccount = succeedOrThrow( - await instanceApi.addBankAccount(token, { - payto_uri: getTestHarnessPaytoForLabel("account1"), - }), - ); - - const secondBankAccountChallenge = alternativeOrThrow( - await instanceApi.addBankAccount(token, { - payto_uri: getTestHarnessPaytoForLabel("account2"), - }), - HttpStatusCode.Accepted, - ); - - await solveMFA(t, instanceApi, secondBankAccountChallenge, mfaConfig); - - const secondBankAccount = succeedOrThrow( - await instanceApi.addBankAccount( - token, - { - payto_uri: getTestHarnessPaytoForLabel("account2"), - }, - { - challengeIds: secondBankAccountChallenge.challenges.map( - (c) => c.challenge_id, - ), - }, - ), - ); - - t.assertTrue(bankAccount.h_wire !== secondBankAccount.h_wire); - const accounts = succeedOrThrow(await instanceApi.listBankAccounts(token)); - const accountHashes = accounts.accounts.map((account) => account.h_wire); - t.assertDeepEqual(accounts.accounts.length, 2); - t.assertTrue(accountHashes.includes(bankAccount.h_wire)); - t.assertTrue(accountHashes.includes(secondBankAccount.h_wire)); -} - -runMerchantSelfProvisionActivationTwoBankAccountsTest.suites = [ - "merchant", - "self-provision", -]; diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts @@ -1,28 +1,16 @@ /* This file is part of GNU Taler - (C) 2021 Taler Systems S.A. + (C) 2021, 2026 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 { alternativeOrThrow, Duration, HttpStatusCode, - j2s, - Logger, LoginTokenScope, MerchantAuthMethod, succeedOrThrow, @@ -31,32 +19,28 @@ import { TanChannel, } from "@gnu-taler/taler-util"; import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; -import { GlobalTestState } from "../harness/harness.js"; +import { + getTestHarnessPaytoForLabel, + GlobalTestState, +} from "../harness/harness.js"; import { configureTestMerchantMfa, + doChallenge, makeMfaConfigEmailSms, solveMFA, - wait2FaCode, } from "../harness/tan-helper.js"; -export const logger = new Logger("test-merchant-self-provision-activation.ts"); - /** - * Activate a self-provisioned instance with email/SMS MFA and verify its - * authenticated contact details. + * Cover self-provision activation, instance-ID casing, contact validation and + * the MFA-protected addition of a second bank account in one environment. */ export async function runMerchantSelfProvisionActivationTest( t: GlobalTestState, ) { - // Set up test environment - const instanceInfo = { - id: "my-awesome-instance", + id: "MYUPPERCASEINSTANCE", name: "My awesome instance", - auth: { - method: MerchantAuthMethod.TOKEN, - password: "123", - }, + auth: { method: MerchantAuthMethod.TOKEN, password: "123" }, default_pay_delay: Duration.toTalerProtocolDuration( Duration.fromSpec({ days: 14 }), ), @@ -69,13 +53,11 @@ export async function runMerchantSelfProvisionActivationTest( phone_number: "+1111", use_stefan: false, }; - const mfaConfig = makeMfaConfigEmailSms( t, instanceInfo.email, instanceInfo.phone_number, ); - const { merchant, merchantAdminAccessToken } = await createSimpleTestkudosEnvironmentV3(t, undefined, { additionalMerchantConfig(m) { @@ -85,134 +67,126 @@ export async function runMerchantSelfProvisionActivationTest( }); }, }); - const merchantClient = new TalerMerchantManagementHttpClient( merchant.makeInstanceBaseUrl(), ); + const initialInstances = succeedOrThrow( + await merchantClient.listInstances(merchantAdminAccessToken), + ); + t.assertDeepEqual(initialInstances.instances.length, 2); - { - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } + await t.assertThrowsTalerErrorAsync(async () => { + await merchantClient.createInstanceSelfProvision({ + ...instanceInfo, + id: "löl", + }); + }); const signupStart = alternativeOrThrow( await merchantClient.createInstanceSelfProvision(instanceInfo), HttpStatusCode.Accepted, ); - - // creation requires 2fa t.assertDeepEqual(signupStart.challenges.length, 2); t.assertDeepEqual(signupStart.combi_and, true); + const [emailChallenge, smsChallenge] = signupStart.challenges; + t.assertDeepEqual(emailChallenge.tan_channel, TanChannel.EMAIL); + t.assertDeepEqual(smsChallenge.tan_channel, TanChannel.SMS); + const pendingInstances = succeedOrThrow( + await merchantClient.listInstances(merchantAdminAccessToken), + ); + t.assertDeepEqual(pendingInstances.instances.length, 2); - const firstChallenge = signupStart.challenges[0]; - const secondChallenge = signupStart.challenges[1]; - - // FIXME: check the order - // always first emails since is cheaper - t.assertTrue(firstChallenge.tan_channel === TanChannel.EMAIL); - t.assertTrue(secondChallenge.tan_channel === TanChannel.SMS); - - { - // new instance is pending, then is not listed - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } - - { - succeedOrThrow( - await merchantClient.sendChallenge(firstChallenge.challenge_id), - ); - const res = await wait2FaCode(mfaConfig.email.path); - console.log(`MFA message: ${res.message}`); - const loginMatch = res.message.match(/^Login: (.*)$/m); - const login = loginMatch?.[1]; - console.log(`login account: ${login}`); - t.assertDeepEqual(login, instanceInfo.id); - t.assertDeepEqual(res.address, instanceInfo.email); - succeedOrThrow( - await merchantClient.confirmChallenge(firstChallenge.challenge_id, { - tan: res.code, - }), - ); - } - - { - succeedOrThrow( - await merchantClient.sendChallenge(secondChallenge.challenge_id), - ); - const res = await wait2FaCode(mfaConfig.sms.path); - t.assertDeepEqual(res.address, instanceInfo.phone_number); - succeedOrThrow( - await merchantClient.confirmChallenge(secondChallenge.challenge_id, { - tan: res.code, - }), - ); - } - + await doChallenge( + t, + merchantClient, + emailChallenge.challenge_id, + instanceInfo.email, + mfaConfig.email.path, + ); + await doChallenge( + t, + merchantClient, + smsChallenge.challenge_id, + instanceInfo.phone_number, + mfaConfig.sms.path, + ); const completeSignup = await merchantClient.createInstanceSelfProvision( instanceInfo, { - challengeIds: [firstChallenge.challenge_id, secondChallenge.challenge_id], + challengeIds: [ + emailChallenge.challenge_id, + smsChallenge.challenge_id, + ], }, ); - - logger.info(`signup response:`); - logger.info(j2s(await completeSignup.response.json())); - t.assertDeepEqual(completeSignup.type, "ok"); const instanceApi = new TalerMerchantInstanceHttpClient( merchantClient.getSubInstanceAPI(instanceInfo.id), merchantClient.httpLib, ); - - { - // new instance is completed, now it should be visible - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 3); - } + const completedInstances = succeedOrThrow( + await merchantClient.listInstances(merchantAdminAccessToken), + ); + t.assertDeepEqual(completedInstances.instances.length, 3); const loginChallenge = alternativeOrThrow( await instanceApi.createAccessToken( instanceInfo.id, instanceInfo.auth.password, - { - scope: LoginTokenScope.All, - }, + { scope: LoginTokenScope.All }, ), HttpStatusCode.Accepted, ); - await solveMFA(t, merchantClient, loginChallenge, mfaConfig); - const { access_token: token } = succeedOrThrow( await instanceApi.createAccessToken( instanceInfo.id, instanceInfo.auth.password, - { - scope: LoginTokenScope.All, - }, + { scope: LoginTokenScope.All }, { challengeIds: loginChallenge.challenges.map((c) => c.challenge_id), }, ), ); - const det = succeedOrThrow( + const details = succeedOrThrow( await instanceApi.getCurrentInstanceDetails(token), ); - - // check that the instance has the new email - t.assertDeepEqual(det.email, instanceInfo.email); - t.assertDeepEqual(det.email_validated, true); - t.assertDeepEqual(det.phone_number, instanceInfo.phone_number); - t.assertDeepEqual(det.phone_validated, true); + t.assertDeepEqual(details.email, instanceInfo.email); + t.assertDeepEqual(details.email_validated, true); + t.assertDeepEqual(details.phone_number, instanceInfo.phone_number); + t.assertDeepEqual(details.phone_validated, true); + + const firstAccount = succeedOrThrow( + await instanceApi.addBankAccount(token, { + payto_uri: getTestHarnessPaytoForLabel("account1"), + }), + ); + const secondAccountChallenge = alternativeOrThrow( + await instanceApi.addBankAccount(token, { + payto_uri: getTestHarnessPaytoForLabel("account2"), + }), + HttpStatusCode.Accepted, + ); + await solveMFA(t, instanceApi, secondAccountChallenge, mfaConfig); + const secondAccount = succeedOrThrow( + await instanceApi.addBankAccount( + token, + { payto_uri: getTestHarnessPaytoForLabel("account2") }, + { + challengeIds: secondAccountChallenge.challenges.map( + (c) => c.challenge_id, + ), + }, + ), + ); + t.assertTrue(firstAccount.h_wire !== secondAccount.h_wire); + const accounts = succeedOrThrow(await instanceApi.listBankAccounts(token)); + const accountHashes = accounts.accounts.map((account) => account.h_wire); + t.assertDeepEqual(accounts.accounts.length, 2); + t.assertTrue(accountHashes.includes(firstAccount.h_wire)); + t.assertTrue(accountHashes.includes(secondAccount.h_wire)); } runMerchantSelfProvisionActivationTest.suites = ["merchant", "self-provision"]; diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-casing.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-casing.ts @@ -1,221 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 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 { - alternativeOrThrow, - Duration, - HttpStatusCode, - Logger, - LoginTokenScope, - MerchantAuthMethod, - succeedOrThrow, - TalerMerchantInstanceHttpClient, - TalerMerchantManagementHttpClient, - TanChannel, -} from "@gnu-taler/taler-util"; -import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; -import { GlobalTestState } from "../harness/harness.js"; -import { - configureTestMerchantMfa, - makeMfaConfigEmailSms, - solveMFA, - wait2FaCode, -} from "../harness/tan-helper.js"; - -export const logger = new Logger("test-merchant-self-provision-activation.ts"); - -/** - * Regression test for bad case normalization in the - * self onboarding. - */ -export async function runMerchantSelfProvisionCasingTest(t: GlobalTestState) { - // Set up test environment - - const instanceInfo = { - id: "MYUPPERCASEINSTANCE", - name: "My awesome instance", - auth: { - method: MerchantAuthMethod.TOKEN, - password: "123", - }, - default_pay_delay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ days: 14 }), - ), - default_wire_transfer_delay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ days: 14 }), - ), - jurisdiction: {}, - address: {}, - email: "some@taler.net", - phone_number: "+1111", - use_stefan: false, - }; - - const mfaConfig = makeMfaConfigEmailSms( - t, - instanceInfo.email, - instanceInfo.phone_number, - ); - - const { merchant, merchantAdminAccessToken } = - await createSimpleTestkudosEnvironmentV3(t, undefined, { - additionalMerchantConfig(m) { - m.modifyConfig(async (cfg) => { - cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); - configureTestMerchantMfa(cfg, mfaConfig); - }); - }, - }); - - const merchantClient = new TalerMerchantManagementHttpClient( - merchant.makeInstanceBaseUrl(), - ); - - { - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } - - { - // Special characters in instance name should not be allowed - await t.assertThrowsTalerErrorAsync(async () => { - await merchantClient.createInstanceSelfProvision({ - ...instanceInfo, - id: "löl", - }); - }); - } - - const signupStart = alternativeOrThrow( - await merchantClient.createInstanceSelfProvision(instanceInfo), - HttpStatusCode.Accepted, - ); - - // creation requires 2fa - t.assertDeepEqual(signupStart.challenges.length, 2); - t.assertDeepEqual(signupStart.combi_and, true); - - const firstChallenge = signupStart.challenges[0]; - const secondChallenge = signupStart.challenges[1]; - - // FIXME: check the order - // always first emails since is cheaper - t.assertTrue(firstChallenge.tan_channel === TanChannel.EMAIL); - t.assertTrue(secondChallenge.tan_channel === TanChannel.SMS); - - { - // new instance is pending, then is not listed - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } - - { - succeedOrThrow( - await merchantClient.sendChallenge(firstChallenge.challenge_id), - ); - const res = await wait2FaCode(mfaConfig.email.path); - console.log(`MFA message: ${res.message}`); - const loginMatch = res.message.match(/^Login: (.*)$/m); - const login = loginMatch?.[1]; - console.log(`login account: ${login}`); - t.assertDeepEqual(res.address, instanceInfo.email); - succeedOrThrow( - await merchantClient.confirmChallenge(firstChallenge.challenge_id, { - tan: res.code, - }), - ); - } - - { - succeedOrThrow( - await merchantClient.sendChallenge(secondChallenge.challenge_id), - ); - const res = await wait2FaCode(mfaConfig.sms.path); - t.assertDeepEqual(res.address, instanceInfo.phone_number); - succeedOrThrow( - await merchantClient.confirmChallenge(secondChallenge.challenge_id, { - tan: res.code, - }), - ); - } - - const completeSignup = await merchantClient.createInstanceSelfProvision( - instanceInfo, - { - challengeIds: [firstChallenge.challenge_id, secondChallenge.challenge_id], - }, - ); - - t.assertDeepEqual(completeSignup.type, "ok"); - - const instanceApi = new TalerMerchantInstanceHttpClient( - merchantClient.getSubInstanceAPI(instanceInfo.id), - merchantClient.httpLib, - ); - - { - // new instance is completed, now it should be visible - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 3); - } - - const loginChallenge = alternativeOrThrow( - await instanceApi.createAccessToken( - instanceInfo.id, - instanceInfo.auth.password, - { - scope: LoginTokenScope.All, - }, - ), - HttpStatusCode.Accepted, - ); - - await solveMFA(t, merchantClient, loginChallenge, mfaConfig); - - const { access_token: token } = succeedOrThrow( - await instanceApi.createAccessToken( - instanceInfo.id, - instanceInfo.auth.password, - { - scope: LoginTokenScope.All, - }, - { - challengeIds: loginChallenge.challenges.map((c) => c.challenge_id), - }, - ), - ); - - const det = succeedOrThrow( - await instanceApi.getCurrentInstanceDetails(token), - ); - - // check that the instance has the new email - t.assertDeepEqual(det.email, instanceInfo.email); - t.assertDeepEqual(det.email_validated, true); - t.assertDeepEqual(det.phone_number, instanceInfo.phone_number); - t.assertDeepEqual(det.phone_validated, true); -} - -runMerchantSelfProvisionCasingTest.suites = ["merchant", "self-provision"]; diff --git a/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts @@ -1,34 +1,24 @@ /* This file is part of GNU Taler - (C) 2021 Taler Systems S.A. + (C) 2021, 2026 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, ConfirmPayResultType, + encodeCrock, + getRandomBytes, + succeedOrThrow, TalerCorebankApiClient, TalerMerchantInstanceHttpClient, TransactionMajorState, TransactionMinorState, TransactionType, URL, - encodeCrock, - getRandomBytes, - succeedOrThrow, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { @@ -39,8 +29,8 @@ import { import { ExchangeService, GlobalTestState, - MerchantService, harnessHttpLib, + MerchantService, } from "../harness/harness.js"; interface Context { @@ -51,84 +41,73 @@ interface Context { exchange: ExchangeService; } -const httpLib = harnessHttpLib; +async function getJson( + t: GlobalTestState, + url: URL, + expectedStatus: number, +): Promise<any> { + const response = await harnessHttpLib.fetch(url.href); + const body = await response.json(); + t.assertDeepEqual(response.status, expectedStatus); + return body; +} -async function testWithClaimToken( +/** Exercise the public order state machine with or without claim tokens. */ +async function testPublicOrderScenario( t: GlobalTestState, c: Context, + useClaimToken: boolean, ): Promise<void> { + const suffix = useClaimToken ? "claim-token" : "no-claim-token"; const { walletClient } = await createWalletDaemonWithClient(t, { - name: "wct", + name: `public-order-${suffix}`, }); - const { bankClient, exchange } = c; - const { merchant, merchantBaseUrl, merchantAccessToken } = c; const wres = await withdrawViaBankV3(t, { walletClient, - bankClient, - exchange, + bankClient: c.bankClient, + exchange: c.exchange, amount: "TESTKUDOS:20", }); await wres.withdrawalFinishedCond; - const sessionId = "mysession"; + const sessionId = `session-${suffix}`; const merchantClient = new TalerMerchantInstanceHttpClient( - merchant.makeInstanceBaseUrl(), + c.merchant.makeInstanceBaseUrl(), ); - const orderResp = succeedOrThrow( - await merchantClient.createOrder(merchantAccessToken, { + const createOrder = () => + merchantClient.createOrder(c.merchantAccessToken, { order: { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "https://example.com/article42", public_reorder_url: "https://example.com/article42-share", }, - }), - ); - - const claimToken = orderResp.token; + create_token: useClaimToken, + }); + const orderResp = succeedOrThrow(await createOrder()); const orderId = orderResp.order_id; - t.assertTrue(!!claimToken); - let talerPayUri: string; + const claimToken = orderResp.token; + t.assertDeepEqual(!!claimToken, useClaimToken); - { - const httpResp = await httpLib.fetch( - new URL(`orders/${orderId}`, merchantBaseUrl).href, - ); - const r = await httpResp.json(); - t.assertDeepEqual(httpResp.status, 202); - console.log(r); - } + const orderUrl = () => new URL(`orders/${orderId}`, c.merchantBaseUrl); + await getJson(t, orderUrl(), useClaimToken ? 202 : 402); - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - talerPayUri = r.taler_pay_uri; - t.assertTrue(!!talerPayUri); - } + const claimUrl = orderUrl(); + if (claimToken) claimUrl.searchParams.set("token", claimToken); + const claimBody = await getJson(t, claimUrl, 402); + t.assertTrue(!!claimBody.taler_pay_uri); - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.fetch(url.href, { - headers: { - Accept: "text/html", - }, - }); - const r = await httpResp.text(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - } + const htmlClaimUrl = orderUrl(); + if (claimToken) htmlClaimUrl.searchParams.set("token", claimToken); + const htmlClaimResp = await harnessHttpLib.fetch(htmlClaimUrl.href, { + headers: { Accept: "text/html" }, + }); + await htmlClaimResp.text(); + t.assertDeepEqual(htmlClaimResp.status, 402); const preparePayResp = await walletClient.call( WalletApiOperation.PreparePayForUriV2, - { - talerPayUri, - }, + { talerPayUri: claimBody.taler_pay_uri }, ); - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { transactionId: preparePayResp.transactionId, txState: { @@ -136,545 +115,112 @@ async function testWithClaimToken( minor: TransactionMinorState.Proposed, }, }); - const payTx = await walletClient.call(WalletApiOperation.GetTransactionById, { transactionId: preparePayResp.transactionId, }); - - t.assertTrue(payTx.type === TransactionType.Payment); + t.assertDeepEqual(payTx.type, TransactionType.Payment); t.assertTrue(payTx.info != null); - const contractTermsHash = payTx.info.contractTermsHash; - const proposalTransactionId = preparePayResp.transactionId; - - // claimed, unpaid, access with wrong h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const hcWrong = encodeCrock(getRandomBytes(64)); - url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // claimed, unpaid, access with wrong claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const ctWrong = encodeCrock(getRandomBytes(16)); - url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // claimed, unpaid, access with correct claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // claimed, unpaid, access with correct contract terms hash - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // claimed, unpaid, access without credentials - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 202); - } - - const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, { - transactionId: proposalTransactionId, - choiceIndex: 0, - }); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); - - // paid, access without credentials - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 202); - } - - // paid, access with wrong h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const hcWrong = encodeCrock(getRandomBytes(64)); - url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // paid, access with wrong claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const ctWrong = encodeCrock(getRandomBytes(16)); - url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // paid, access with correct h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - } - // paid, access with correct claim token, JSON - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - const respFulfillmentUrl = r.fulfillment_url; - t.assertDeepEqual(respFulfillmentUrl, "https://example.com/article42"); - } - - // paid, access with correct claim token, HTML - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.fetch(url.href, { - headers: { Accept: "text/html" }, - }); - t.assertDeepEqual(httpResp.status, 200); - } - - const confirmPayRes2 = await walletClient.call( + const wrongHashUrl = orderUrl(); + wrongHashUrl.searchParams.set("h_contract", encodeCrock(getRandomBytes(64))); + await getJson(t, wrongHashUrl, 403); + const wrongTokenUrl = orderUrl(); + wrongTokenUrl.searchParams.set("token", encodeCrock(getRandomBytes(16))); + await getJson(t, wrongTokenUrl, 403); + await getJson(t, claimUrl, 402); + const correctHashUrl = orderUrl(); + correctHashUrl.searchParams.set("h_contract", contractTermsHash); + await getJson(t, correctHashUrl, 402); + await getJson(t, orderUrl(), useClaimToken ? 202 : 402); + + const confirmPayRes = await walletClient.call( WalletApiOperation.ConfirmPay, - { - transactionId: proposalTransactionId, - sessionId: sessionId, - }, + { transactionId: preparePayResp.transactionId, choiceIndex: 0 }, ); - - t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done); - - // Create another order with identical fulfillment URL to test the "already paid" flow - const alreadyPaidOrderResp = succeedOrThrow( - await merchantClient.createOrder(merchantAccessToken, { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }), + t.assertDeepEqual(confirmPayRes.type, ConfirmPayResultType.Done); + + await getJson(t, orderUrl(), useClaimToken ? 202 : 200); + await getJson(t, wrongHashUrl, 403); + await getJson(t, wrongTokenUrl, 403); + await getJson(t, correctHashUrl, 200); + const paidBody = await getJson(t, claimUrl, 200); + t.assertDeepEqual( + paidBody.fulfillment_url, + "https://example.com/article42", ); - - const apOrderId = alreadyPaidOrderResp.order_id; - const apToken = alreadyPaidOrderResp.token; - t.assertTrue(!!apToken); - - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // Check for already paid session ID, JSON - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - const alreadyPaidOrderId = r.already_paid_order_id; - t.assertDeepEqual(alreadyPaidOrderId, orderId); - } - - // Check for already paid session ID, HTML - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.fetch(url.href, { - headers: { Accept: "text/html" }, - redirect: "manual", - }); - console.log( - `requesting GET ${url.href}, expected 302 got ${httpResp.status}`, - ); - t.assertDeepEqual(httpResp.status, 302); - const location = httpResp.headers.get("Location"); - console.log("location header:", location); - t.assertDeepEqual(location, "https://example.com/article42"); - } -} - -async function testWithoutClaimToken( - t: GlobalTestState, - c: Context, -): Promise<void> { - const { walletClient } = await createWalletDaemonWithClient(t, { - name: "wnoct", + const paidHtmlResp = await harnessHttpLib.fetch(claimUrl.href, { + headers: { Accept: "text/html" }, }); - const sessionId = "mysession2"; - const { bankClient, exchange } = c; - const { merchant, merchantBaseUrl, merchantAccessToken } = c; - const merchantClient = new TalerMerchantInstanceHttpClient( - merchant.makeInstanceBaseUrl(), - ); - const wres = await withdrawViaBankV3(t, { - walletClient, - bankClient, - exchange, - amount: "TESTKUDOS:20", - }); - await wres.withdrawalFinishedCond; - const orderResp = succeedOrThrow( - await merchantClient.createOrder(merchantAccessToken, { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - create_token: false, - }), - ); - - const orderId = orderResp.order_id; - let talerPayUri: string; - - { - const httpResp = await httpLib.fetch( - new URL(`orders/${orderId}`, merchantBaseUrl).href, - ); - const r = await httpResp.json(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - } - - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - talerPayUri = r.taler_pay_uri; - t.assertTrue(!!talerPayUri); - } - - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.fetch(url.href, { - headers: { - Accept: "text/html", - }, - }); - const r = await httpResp.text(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - } - - const preparePayResp = await walletClient.call( - WalletApiOperation.PreparePayForUriV2, - { - talerPayUri, - }, - ); - - console.log(preparePayResp); - - await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { - transactionId: preparePayResp.transactionId, - txState: { - major: TransactionMajorState.Dialog, - minor: TransactionMinorState.Proposed, - }, - }); - - const payTx = await walletClient.call(WalletApiOperation.GetTransactionById, { - transactionId: preparePayResp.transactionId, - }); - - t.assertTrue(payTx.type === TransactionType.Payment); - t.assertTrue(payTx.info != null); - - const contractTermsHash = payTx.info.contractTermsHash; - const proposalTransactionId = preparePayResp.transactionId; - - // claimed, unpaid, access with wrong h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const hcWrong = encodeCrock(getRandomBytes(64)); - url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // claimed, unpaid, access with wrong claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const ctWrong = encodeCrock(getRandomBytes(16)); - url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // claimed, unpaid, no claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // claimed, unpaid, access with correct contract terms hash - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // claimed, unpaid, access without credentials - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - // No credentials, but the order doesn't require a claim token. - // This effectively means that the order ID is already considered - // enough authentication, at least to check for the basic order status - t.assertDeepEqual(httpResp.status, 402); - } - - const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, { - transactionId: proposalTransactionId, - choiceIndex: 0, - }); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); + t.assertDeepEqual(paidHtmlResp.status, 200); - // paid, access without credentials - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - } - - // paid, access with wrong h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const hcWrong = encodeCrock(getRandomBytes(64)); - url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // paid, access with wrong claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const ctWrong = encodeCrock(getRandomBytes(16)); - url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // paid, access with correct h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - } - - // paid, JSON - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - const respFulfillmentUrl = r.fulfillment_url; - t.assertDeepEqual(respFulfillmentUrl, "https://example.com/article42"); - } - - // paid, HTML - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.fetch(url.href, { - headers: { Accept: "text/html" }, - }); - t.assertDeepEqual(httpResp.status, 200); - } - - const confirmPayRes2 = await walletClient.call( + const confirmPayWithSession = await walletClient.call( WalletApiOperation.ConfirmPay, - { - transactionId: proposalTransactionId, - sessionId: sessionId, - }, + { transactionId: preparePayResp.transactionId, sessionId }, ); + t.assertDeepEqual(confirmPayWithSession.type, ConfirmPayResultType.Done); - t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done); - - // Create another order with identical fulfillment URL to test the "already paid" flow - const alreadyPaidOrderResp = succeedOrThrow( - await merchantClient.createOrder(merchantAccessToken, { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }), + const alreadyPaidOrder = succeedOrThrow(await createOrder()); + const alreadyPaidUrl = () => { + const url = new URL( + `orders/${alreadyPaidOrder.order_id}`, + c.merchantBaseUrl, + ); + if (alreadyPaidOrder.token) { + url.searchParams.set("token", alreadyPaidOrder.token); + } + return url; + }; + await getJson(t, alreadyPaidUrl(), 402); + const sessionUrl = alreadyPaidUrl(); + sessionUrl.searchParams.set("session_id", sessionId); + const alreadyPaidBody = await getJson(t, sessionUrl, 402); + t.assertDeepEqual(alreadyPaidBody.already_paid_order_id, orderId); + + const sessionHtmlResp = await harnessHttpLib.fetch(sessionUrl.href, { + headers: { Accept: "text/html" }, + redirect: "manual", + }); + t.assertDeepEqual(sessionHtmlResp.status, 302); + t.assertDeepEqual( + sessionHtmlResp.headers.get("Location"), + "https://example.com/article42", ); - - const apOrderId = alreadyPaidOrderResp.order_id; - const apToken = alreadyPaidOrderResp.token; - t.assertTrue(!!apToken); - - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // Check for already paid session ID, JSON - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.fetch(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - const alreadyPaidOrderId = r.already_paid_order_id; - t.assertDeepEqual(alreadyPaidOrderId, orderId); - } - - // Check for already paid session ID, HTML - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.fetch(url.href, { - headers: { Accept: "text/html" }, - redirect: "manual", - }); - t.assertDeepEqual(httpResp.status, 302); - const location = httpResp.headers.get("Location"); - console.log("location header:", location); - t.assertDeepEqual(location, "https://example.com/article42"); - } } -/** - * Checks for the /orders/{id} endpoint of the merchant. - * - * The tests here should exercise all code paths in the executable - * specification of the endpoint. - */ +/** Exercise every public-order endpoint branch for both token policies. */ export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) { const { bankClient, exchange, merchant, merchantAdminAccessToken } = await createSimpleTestkudosEnvironmentV3(t); - - // Base URL for the default instance. const merchantBaseUrl = merchant.makeInstanceBaseUrl(); - { - const httpResp = await httpLib.fetch( - new URL("config", merchantBaseUrl).href, - ); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(r.currency, "TESTKUDOS"); - } - - { - const httpResp = await httpLib.fetch( - new URL("orders/foo", merchantBaseUrl).href, - ); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 404); - // FIXME: also check Taler error code - } - - { - const httpResp = await httpLib.fetch( - new URL("orders/foo", merchantBaseUrl).href, - { - headers: { - Accept: "text/html", - }, - }, - ); - const r = await httpResp.text(); - console.log(r); - t.assertDeepEqual(httpResp.status, 404); - // FIXME: also check Taler error code - } + const configResp = await harnessHttpLib.fetch( + new URL("config", merchantBaseUrl).href, + ); + const config = await configResp.json(); + t.assertDeepEqual(config.currency, "TESTKUDOS"); - await testWithClaimToken(t, { - merchant, - merchantBaseUrl, - exchange, - bankClient, - merchantAccessToken: merchantAdminAccessToken, - }); + const missingJsonResp = await harnessHttpLib.fetch( + new URL("orders/foo", merchantBaseUrl).href, + ); + await missingJsonResp.json(); + t.assertDeepEqual(missingJsonResp.status, 404); + const missingHtmlResp = await harnessHttpLib.fetch( + new URL("orders/foo", merchantBaseUrl).href, + { headers: { Accept: "text/html" } }, + ); + await missingHtmlResp.text(); + t.assertDeepEqual(missingHtmlResp.status, 404); - await testWithoutClaimToken(t, { + const context = { merchant, merchantBaseUrl, exchange, bankClient, merchantAccessToken: merchantAdminAccessToken, - }); + }; + await testPublicOrderScenario(t, context, true); + await testPublicOrderScenario(t, context, false); } runMerchantSpecPublicOrdersTest.suites = ["merchant"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -95,7 +95,6 @@ import { runKycFailRecoverSimpleTest } from "./test-kyc-fail-recover-simple.js"; import { runKycFormBadMeasureTest } from "./test-kyc-form-bad-measure.js"; import { runKycFormCompressionTest } from "./test-kyc-form-compression.js"; import { runKycFormValidationTest } from "./test-kyc-form-validation.js"; -import { runKycFormWithdrawalTest } from "./test-kyc-form-withdrawal.js"; import { runKycMerchantActivateBankAccountTest } from "./test-kyc-merchant-activate-bank-account.js"; import { runKycMerchantAggregateTest } from "./test-kyc-merchant-aggregate.js"; import { runKycMerchantDepositFormTest } from "./test-kyc-merchant-deposit-form.js"; @@ -107,7 +106,6 @@ import { runKycNewMeasuresProgTest } from "./test-kyc-new-measures-prog.js"; import { runKycPeerPullTest } from "./test-kyc-peer-pull.js"; import { runKycPeerPushTest } from "./test-kyc-peer-push.js"; import { runKycSkipExpirationTest } from "./test-kyc-skip-expiration.js"; -import { runKycThresholdWithdrawalTest } from "./test-kyc-threshold-withdrawal.js"; import { runKycTwoFormsTest } from "./test-kyc-two-forms.js"; import { runKycWalletDepositAbortTest } from "./test-kyc-wallet-deposit-abort.js"; import { runKycWithdrawalVerbotenTest } from "./test-kyc-withdrawal-verboten.js"; @@ -131,9 +129,7 @@ import { runMerchantPaytoReuseTest } from "./test-merchant-payto-reuse.js"; import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js"; import { runMerchantRefundFeesTest } from "./test-merchant-refund-fees.js"; import { runMerchantReportsTest } from "./test-merchant-reports.js"; -import { runMerchantSelfProvisionActivationTwoBankAccountsTest } from "./test-merchant-self-provision-activation-two-bank-account.js"; import { runMerchantSelfProvisionActivationTest } from "./test-merchant-self-provision-activation.js"; -import { runMerchantSelfProvisionCasingTest } from "./test-merchant-self-provision-casing.js"; import { runMerchantSelfProvisionForgotPasswordTest } from "./test-merchant-self-provision-forgot-password.js"; import { runMerchantSelfProvisionInactiveAccountPermissionsTest } from "./test-merchant-self-provision-inactive-account-permissions.js"; import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js"; @@ -392,18 +388,15 @@ const allTests: TestMainFunction[] = [ runExchangeMasterPubChangeTest, runMerchantCategoriesTest, runMerchantSelfProvisionActivationTest, - runMerchantSelfProvisionActivationTwoBankAccountsTest, runWebMerchantLoginTest, runMerchantSelfProvisionForgotPasswordTest, runMerchantSelfProvisionInactiveAccountPermissionsTest, runWithdrawalExternalTest, runWithdrawalIdempotentTest, - runKycThresholdWithdrawalTest, runKycExchangeWalletTest, runKycPeerPushTest, runKycPeerPullTest, runKycDepositAggregateTest, - runKycFormWithdrawalTest, runKycBalanceWithdrawalTest, runKycNewMeasureTest, runKycSkipExpirationTest, @@ -502,7 +495,6 @@ const allTests: TestMainFunction[] = [ runTopsNexusBasicTest, runTopsNexusSwtTest, runTopsMerchantSwtKycauthTest, - runMerchantSelfProvisionCasingTest, runTopsMerchantSimpleKycauthTest, runMerchantOrderListingTest, ];