commit f61a654888b9ee3312c7064502fdde18ebf19088
parent a2b0317a7dfeb78b655456390895fa03333220ce
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 9 Jul 2025 11:19:58 -0300
reproduce #10158
Diffstat:
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,