taler-typescript-core

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

commit f61a654888b9ee3312c7064502fdde18ebf19088
parent a2b0317a7dfeb78b655456390895fa03333220ce
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed,  9 Jul 2025 11:19:58 -0300

reproduce #10158

Diffstat:
Mpackages/taler-harness/src/index.ts | 13+++++++++++++
Apackages/taler-harness/src/integrationtests/test-kyc-fail-recover-double.ts | 259+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-harness/src/integrationtests/test-kyc-fail-recover-simple.ts | 265+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 4++++
4 files changed, 541 insertions(+), 0 deletions(-)

diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -98,6 +98,8 @@ import { import { AML_PROGRAM_TEST_KYC_NEW_MEASURES_PROG } from "./integrationtests/test-kyc-new-measures-prog.js"; import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; import { lintExchangeDeployment, lintExchangeUrl } from "./lint.js"; +import { execSync } from "node:child_process"; +import { AML_PROGRAM_FAIL_RECOVER } from "integrationtests/test-kyc-fail-recover-simple.js"; const logger = new Logger("taler-harness:index.ts"); @@ -1643,6 +1645,16 @@ const allAmlPrograms: TalerKycAml.AmlProgramDefinition[] = [ requiredContext: [], }, { + name: "fail-exec-child-error", + logic: async (_input, _config) => { + execSync("something-that-doesnt-exists make-this-fail"); + throw Error("unreachable") + }, + requiredAttributes: [], + requiredInputs: [], + requiredContext: [], + }, + { name: "hang", logic: async (_input, _config) => { console.log("going to wait ..."); @@ -1659,6 +1671,7 @@ const allAmlPrograms: TalerKycAml.AmlProgramDefinition[] = [ }, AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT, AML_PROGRAM_NEXT_MEASURE_FORM, + AML_PROGRAM_FAIL_RECOVER, AML_PROGRAM_TEST_KYC_NEW_MEASURES_PROG, ]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-fail-recover-double.ts b/packages/taler-harness/src/integrationtests/test-kyc-fail-recover-double.ts @@ -0,0 +1,259 @@ +/* + This file is part of GNU Taler + (C) 2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + AmountString, + bufferFromAmount, + buildSigPS, + codecForAccountKycStatus, + codecForKycProcessClientInformation, + codecForLegitimizationNeededResponse, + codecOptional, + Configuration, + createNewWalletKycAccount, + eddsaSign, + encodeCrock, + j2s, + Logger, + TalerSignaturePurpose, + WalletKycRequest, +} from "@gnu-taler/taler-util"; +import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { + configureCommonKyc, + createKycTestkudosEnvironment, +} from "../harness/environments.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; + +const logger = new Logger(`test-kyc-merchant-fail-recover.ts`); + +function adjustExchangeConfig(config: Configuration) { + configureCommonKyc(config); + + // trigger based on balance + config.setString("KYC-RULE-R1", "operation_type", "balance"); + 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", "forever"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + // normal measure + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + // program that will failed based on input, but also + // the fallback measure will fail when this program fail + config.setString( + "AML-PROGRAM-P1", + "command", + "taler-harness aml-program run-program --name FAIL_RECOVER", + ); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString( + "AML-PROGRAM-P1", + "description", + "just fail based on the name", + ); + config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P1", "fallback", "FREEZE-fail"); + + // normal form + config.setString("KYC-CHECK-C1", "type", "FORM"); + config.setString("KYC-CHECK-C1", "form_name", "firstForm"); + config.setString("KYC-CHECK-C1", "description", "starting check!"); + config.setString("KYC-CHECK-C1", "description_i18n", "{}"); + config.setString("KYC-CHECK-C1", "outputs", "NAME"); + config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); + + // fallback measure that will fail to recover + config.setString("KYC-MEASURE-FREEZE-fail", "check_name", "SKIP"); + config.setString("KYC-MEASURE-FREEZE-fail", "context", "{}"); + config.setString("KYC-MEASURE-FREEZE-fail", "program", "FREEZE-fail"); + + config.setString( + "AML-PROGRAM-FREEZE-fail", + "command", + "taler-harness aml-program run-program --name fail-exec-child-error", + ); + config.setString("AML-PROGRAM-FREEZE-fail", "enabled", "true"); + config.setString( + "AML-PROGRAM-FREEZE-fail", + "description", + "try to freeze but fail", + ); + config.setString("AML-PROGRAM-FREEZE-fail", "description_i18n", "{}"); + config.setString("AML-PROGRAM-FREEZE-fail", "fallback", "FREEZE-fail"); +} + +export async function runKycFailRecoverDoubleTest(t: GlobalTestState) { + // Set up test environment + + const { exchange, amlKeypair } = await createKycTestkudosEnvironment(t, { + adjustExchangeConfig, + onWalletNotification: () => {}, + }); + + // Withdraw digital cash into the wallet. + let kycPaytoHash: string; + let accessToken: string; + let latestFormId: string; + + const account = await createNewWalletKycAccount(new Uint8Array()); + logger.info("step 1) Check balance to trigger AML"); + { + const balance: AmountString = "TESTKUDOS:20"; + const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP) + .put(bufferFromAmount(balance)) + .build(); + const body: WalletKycRequest = { + balance, + reserve_pub: account.id, + reserve_sig: encodeCrock(eddsaSign(sigBlob, account.signingKey)), + }; + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-wallet`, exchange.baseUrl).href, + { + method: "POST", + body, + }, + ); + + t.assertDeepEqual(infoResp.status, 451); + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecOptional(codecForLegitimizationNeededResponse()), + ); + + t.assertTrue(clientInfo?.h_payto !== undefined); + kycPaytoHash = clientInfo?.h_payto; + } + + logger.info("step 2) Get account access token"); + { + const sigBlob = buildSigPS(TalerSignaturePurpose.KYC_AUTH).build(); + + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-check/${kycPaytoHash}`, exchange.baseUrl).href, + { + headers: { + "Account-Owner-Signature": encodeCrock( + eddsaSign(sigBlob, account.signingKey), + ), + }, + }, + ); + + t.assertDeepEqual(infoResp.status, 202); + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecOptional(codecForAccountKycStatus()), + ); + t.assertTrue(clientInfo?.access_token !== undefined); + accessToken = clientInfo?.access_token; + } + + logger.info("step 3) Check KYC info, should be waiting for the first form"); + { + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${accessToken}?timeout_ms=1000`, exchange.baseUrl).href, + ); + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecOptional(codecForKycProcessClientInformation()), + ); + + console.log(j2s(clientInfo)); + t.assertDeepEqual(infoResp.status, 200); + t.assertDeepEqual(clientInfo?.requirements.length, 1); + t.assertDeepEqual(clientInfo?.requirements[0].form, "firstForm"); + t.assertTrue(!!clientInfo?.requirements[0].id); + latestFormId = clientInfo?.requirements[0].id; + } + + logger.info("step 4) Complete form expecting to fail"); + { + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-upload/${latestFormId}`, exchange.baseUrl).href, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: { NAME: "child-fail" }, + }, + ); + + t.assertDeepEqual(infoResp.status, 500); + } + + logger.info("step 4) Complete form expecting to fail again"); + { + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-upload/${latestFormId}`, exchange.baseUrl).href, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: { NAME: "child-fail" }, + }, + ); + + t.assertDeepEqual(infoResp.status, 500); + } + + logger.info("step 5) Complete form expecting but this time it should work"); + { + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-upload/${latestFormId}`, exchange.baseUrl).href, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: { NAME: "just-kidding" }, + }, + ); + + t.assertDeepEqual(infoResp.status, 204); + } + + { + logger.info( + "step 6) Check KYC info again after some time, here the exchange fails", + ); + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${accessToken}?timeout_ms=1000`, exchange.baseUrl).href, + ); + + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecOptional(codecForKycProcessClientInformation()), + ); + + console.log(j2s(clientInfo)); + t.assertDeepEqual(infoResp.status, 200); + t.assertDeepEqual(clientInfo?.requirements.length, 0); + } +} + +runKycFailRecoverDoubleTest.suites = ["wallet", "merchant", "kyc"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-fail-recover-simple.ts b/packages/taler-harness/src/integrationtests/test-kyc-fail-recover-simple.ts @@ -0,0 +1,265 @@ +/* + This file is part of GNU Taler + (C) 2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + AmountString, + bufferFromAmount, + buildSigPS, + codecForAccountKycStatus, + codecForKycProcessClientInformation, + codecForLegitimizationNeededResponse, + codecOptional, + Configuration, + createNewWalletKycAccount, + eddsaSign, + encodeCrock, + j2s, + Logger, + TalerKycAml, + TalerSignaturePurpose, + WalletKycRequest +} from "@gnu-taler/taler-util"; +import { + readResponseJsonOrThrow +} from "@gnu-taler/taler-util/http"; +import { execSync } from "node:child_process"; +import { + configureCommonKyc, + createKycTestkudosEnvironment, +} from "../harness/environments.js"; +import { + GlobalTestState, + harnessHttpLib +} from "../harness/harness.js"; + +const logger = new Logger(`test-kyc-merchant-fail-recover.ts`); + +export const AML_PROGRAM_FAIL_RECOVER: TalerKycAml.AmlProgramDefinition = { + name: "FAIL_RECOVER", + logic: async (input, config) => { + logger.info("INPUT", input); + if (input?.attributes?.NAME === "trow-error") { + throw Error("i was asked to fail"); + } + if (input?.attributes?.NAME === "exit-fail") { + process.exit(1); + } + if (input?.attributes?.NAME === "child-fail") { + execSync("something-that-doesnt-exists make-this-fail"); + } + + const outcome: TalerKycAml.AmlOutcome = { + to_investigate: false, + properties: {}, + events: [], + new_rules: { + expiration_time: { t_s: 1 }, + rules: [], + successor_measure: "M2", + custom_measures: {}, + }, + }; + logger.info("aml program FAIL AND RECOVER outcome", j2s(outcome)); + + return outcome; + }, + requiredAttributes: ["NAME"], + requiredInputs: ["attributes"], + requiredContext: [], +}; + +function adjustExchangeConfig(config: Configuration) { + configureCommonKyc(config); + + // trigger based on balance + config.setString("KYC-RULE-R1", "operation_type", "balance"); + 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", "forever"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + // normal measure: form and program + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + // a program that will fail based on the form input + config.setString( + "AML-PROGRAM-P1", + "command", + "taler-harness aml-program run-program --name FAIL_RECOVER", + ); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString( + "AML-PROGRAM-P1", + "description", + "just fail based on the name", + ); + config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P1", "fallback", "FREEZE"); + + // normal form + config.setString("KYC-CHECK-C1", "type", "FORM"); + config.setString("KYC-CHECK-C1", "form_name", "firstForm"); + config.setString("KYC-CHECK-C1", "description", "starting check!"); + config.setString("KYC-CHECK-C1", "description_i18n", "{}"); + config.setString("KYC-CHECK-C1", "outputs", "NAME"); + config.setString("KYC-CHECK-C1", "fallback", "FREEZE"); + +} + +export async function runKycFailRecoverSimpleTest(t: GlobalTestState) { + // Set up test environment + + const { exchange, amlKeypair } = await createKycTestkudosEnvironment(t, { + adjustExchangeConfig, + onWalletNotification: () => {}, + }); + + // Withdraw digital cash into the wallet. + let kycPaytoHash: string; + let accessToken: string; + let latestFormId: string; + + const account = await createNewWalletKycAccount(new Uint8Array()); + logger.info("step 1) Check balance to trigger AML"); + { + const balance: AmountString = "TESTKUDOS:20"; + const sigBlob = buildSigPS(TalerSignaturePurpose.WALLET_ACCOUNT_SETUP) + .put(bufferFromAmount(balance)) + .build(); + const body: WalletKycRequest = { + balance, + reserve_pub: account.id, + reserve_sig: encodeCrock(eddsaSign(sigBlob, account.signingKey)), + }; + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-wallet`, exchange.baseUrl).href, + { + method: "POST", + body, + }, + ); + + t.assertDeepEqual(infoResp.status, 451); + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecOptional(codecForLegitimizationNeededResponse()), + ); + + t.assertTrue(clientInfo?.h_payto !== undefined); + kycPaytoHash = clientInfo?.h_payto; + } + + logger.info("step 2) Get account access token"); + { + const sigBlob = buildSigPS(TalerSignaturePurpose.KYC_AUTH).build(); + + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-check/${kycPaytoHash}`, exchange.baseUrl).href, + { + headers: { + "Account-Owner-Signature": encodeCrock( + eddsaSign(sigBlob, account.signingKey), + ), + }, + }, + ); + + t.assertDeepEqual(infoResp.status, 202); + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecOptional(codecForAccountKycStatus()), + ); + t.assertTrue(clientInfo?.access_token !== undefined); + accessToken = clientInfo?.access_token; + } + + logger.info("step 3) Check KYC info, should be waiting for the first form"); + { + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${accessToken}?timeout_ms=1000`, exchange.baseUrl).href, + ); + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecOptional(codecForKycProcessClientInformation()), + ); + + console.log(j2s(clientInfo)); + t.assertDeepEqual(infoResp.status, 200); + t.assertDeepEqual(clientInfo?.requirements.length, 1); + t.assertDeepEqual(clientInfo?.requirements[0].form, "firstForm"); + t.assertTrue(!!clientInfo?.requirements[0].id); + latestFormId = clientInfo?.requirements[0].id; + } + + logger.info("step 4) Complete form expecting to fail"); + { + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-upload/${latestFormId}`, exchange.baseUrl).href, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: { NAME: "child-fail" }, + }, + ); + + t.assertDeepEqual(infoResp.status, 500); + } + + logger.info("step 5) Complete form expecting but this time it should work"); + { + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-upload/${latestFormId}`, exchange.baseUrl).href, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: { NAME: "just-kidding" }, + }, + ); + + t.assertDeepEqual(infoResp.status, 204); + } + + { + logger.info( + "step 6) Check KYC info again after some time, here the exchange fails", + ); + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${accessToken}?timeout_ms=1000`, exchange.baseUrl).href, + ); + + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecOptional(codecForKycProcessClientInformation()), + ); + + console.log(j2s(clientInfo)); + t.assertDeepEqual(infoResp.status, 200); + t.assertDeepEqual(clientInfo?.requirements.length, 0); + } +} + +runKycFailRecoverSimpleTest.suites = ["wallet", "merchant", "kyc"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -185,6 +185,8 @@ import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js"; import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js"; +import { runKycFailRecoverDoubleTest } from "./test-kyc-fail-recover-double.js"; +import { runKycFailRecoverSimpleTest } from "./test-kyc-fail-recover-simple.js"; /** * Test runner. @@ -311,6 +313,8 @@ const allTests: TestMainFunction[] = [ runKycNewMeasureTest, runKycSkipExpirationTest, runKycTwoFormsTest, + runKycFailRecoverDoubleTest, + runKycFailRecoverSimpleTest, runKycDepositDepositTest, runKycMerchantDepositTest, runKycMerchantDepositRewriteTest,