commit 64e63d5089f5ab4049dfd466bcb64bb194e69aeb
parent 28f5e9fbf7c2497709fce19d6d56dd9b9b3b4183
Author: Florian Dold <florian@dold.me>
Date: Tue, 21 Jan 2025 15:36:08 +0100
harness: test for new_measures
Diffstat:
10 files changed, 345 insertions(+), 10 deletions(-)
diff --git a/packages/taler-harness/src/harness/environments.ts b/packages/taler-harness/src/harness/environments.ts
@@ -1031,7 +1031,7 @@ export async function postAmlDecision(
amlPriv: string;
amlPub: string;
newRules: LegitimizationRuleSet;
- newMeasure?: string | undefined;
+ newMeasures?: string;
properties?: AccountProperties;
},
) {
@@ -1043,7 +1043,7 @@ export async function postAmlDecision(
justification: "Bla",
keep_investigating: false,
new_rules: req.newRules,
- new_measures: req.newMeasure,
+ new_measures: req.newMeasures,
properties: req.properties ?? {},
};
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
@@ -86,6 +86,7 @@ import {
runTestWithState,
waitMs,
} from "./harness/harness.js";
+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 } from "./lint.js";
@@ -1550,6 +1551,7 @@ const allAmlPrograms: TalerKycAml.AmlProgramDefinition[] = [
},
AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT,
AML_PROGRAM_NEXT_MEASURE_FORM,
+ AML_PROGRAM_TEST_KYC_NEW_MEASURES_PROG,
];
amlProgramCli
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-decisions.ts b/packages/taler-harness/src/integrationtests/test-kyc-decisions.ts
@@ -26,6 +26,7 @@ import {
encodeCrock,
hashNormalizedPaytoUri,
j2s,
+ LimitOperationType,
TalerProtocolTimestamp,
TransactionMajorState,
TransactionMinorState,
@@ -123,7 +124,7 @@ export async function runKycDecisionsTest(t: GlobalTestState) {
// Strict rules for this particular merchant!
rules: [
{
- operation_type: "DEPOSIT",
+ operation_type: LimitOperationType.deposit,
display_priority: 1,
exposed: true,
measures: ["verboten"],
@@ -135,7 +136,7 @@ export async function runKycDecisionsTest(t: GlobalTestState) {
),
},
{
- operation_type: "WITHDRAW",
+ operation_type: LimitOperationType.withdraw,
display_priority: 1,
exposed: true,
measures: ["verboten"],
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-new-measure.ts b/packages/taler-harness/src/integrationtests/test-kyc-new-measure.ts
@@ -209,7 +209,7 @@ export async function runKycNewMeasureTest(t: GlobalTestState) {
amlPub: amlKeypair.pub,
exchangeBaseUrl: exchange.baseUrl,
paytoHash: kycPaytoHash,
- newMeasure: "m3",
+ newMeasures: "m3",
newRules: {
expiration_time: TalerProtocolTimestamp.never(),
custom_measures: {},
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
@@ -0,0 +1,318 @@
+/*
+ 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 {
+ 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,
+ createKycTestkudosEnvironment,
+ postAmlDecision,
+ withdrawViaBankV3,
+} from "../harness/environments.js";
+import { GlobalTestState, harnessHttpLib, waitMs } from "../harness/harness.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.
+ await waitMs(500);
+ const outcome: TalerKycAml.AmlOutcome = {
+ to_investigate: false,
+ // pushing to info into properties for testing purposes
+ properties: {
+ "this comes": "from the program",
+ input: _input as any,
+ config,
+ },
+ events: [],
+ new_measures: "ask_more_info ask_basic_info",
+ new_rules: {
+ expiration_time: TalerProtocolTimestamp.never(),
+ rules: [],
+ custom_measures: {
+ ask_basic_info: {
+ context: {
+ // this is the context info that the KYC-SPA will see
+ 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?",
+ },
+ 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.
+ */
+export async function runKycNewMeasuresProgTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(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
+ 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()),
+ );
+
+ if (clientInfo?.requirements.length == 0) {
+ console.log("requirements empty, waiting ...");
+ await waitMs(1000);
+ continue;
+ }
+
+ 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;
+ }
+}
+
+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
@@ -46,6 +46,8 @@ export const AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT: TalerKycAml.AmlProgramDefin
{
name: "from-attr-to-context",
logic: async (_input, config) => {
+ // Artificially delay the AML program.
+ await waitMs(500);
const outcome: TalerKycAml.AmlOutcome = {
to_investigate: false,
// pushing to info into properties for testing purposes
@@ -137,9 +139,6 @@ function adjustExchangeConfig(config: Configuration) {
config.setString("KYC-CHECK-C3", "fallback", "FREEZE");
}
-/**
- * Test setting a `new_measure` as the AML officer.
- */
export async function runKycSkipExpirationTest(t: GlobalTestState) {
// Set up test environment
@@ -219,7 +218,7 @@ export async function runKycSkipExpirationTest(t: GlobalTestState) {
amlPub: amlKeypair.pub,
exchangeBaseUrl: exchange.baseUrl,
paytoHash: kycPaytoHash,
- newMeasure: "M3",
+ newMeasures: "M3",
properties: {
form: { name: "string" },
},
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-withdrawal-verboten.ts b/packages/taler-harness/src/integrationtests/test-kyc-withdrawal-verboten.ts
@@ -19,6 +19,7 @@
*/
import {
Duration,
+ LimitOperationType,
NotificationType,
TalerProtocolTimestamp,
TransactionMajorState,
@@ -186,7 +187,7 @@ export async function runKycWithdrawalVerbotenTest(t: GlobalTestState) {
expiration_time: TalerProtocolTimestamp.never(),
rules: [
{
- operation_type: "WITHDRAW",
+ operation_type: LimitOperationType.withdraw,
display_priority: 1,
measures: ["verboten"],
threshold: "TESTKUDOS:0",
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -64,6 +64,7 @@ import { runKycMerchantActivateBankAccountTest } from "./test-kyc-merchant-activ
import { runKycMerchantAggregateTest } from "./test-kyc-merchant-aggregate.js";
import { runKycMerchantDepositTest } from "./test-kyc-merchant-deposit.js";
import { runKycNewMeasureTest } from "./test-kyc-new-measure.js";
+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";
@@ -302,6 +303,7 @@ const allTests: TestMainFunction[] = [
runWithdrawalCashacceptorTest,
runWithdrawalConflictTest,
runBankWopTest,
+ runKycNewMeasuresProgTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts
@@ -1789,6 +1789,7 @@ export type LimitOperationType2 =
| "REFUND"
| "CLOSE"
| "TRANSACTION";
+
export enum LimitOperationType {
withdraw = "WITHDRAW",
deposit = "DEPOSIT",
@@ -1799,6 +1800,7 @@ export enum LimitOperationType {
close = "CLOSE",
transaction = "TRANSACTION",
}
+
export interface AccountLimit {
// Operation that is limited.
operation_type: LimitOperationType;
diff --git a/packages/taler-util/src/types-taler-kyc-aml.ts b/packages/taler-util/src/types-taler-kyc-aml.ts
@@ -203,6 +203,16 @@ export interface AmlOutcome {
// the successor measure to apply after the
// expiration time.
new_rules: LegitimizationRuleSet;
+
+ // Space-separated list of measures to trigger
+ // immediately on the account.
+ // Prefixed with a "+" to indicate that the
+ // measures should be ANDed.
+ // Should typically be used to give the user some
+ // information or request additional information.
+ //
+ // At most one measure with a SKIP check may be specified.
+ new_measures?: string;
}
// All fields in this object are optional. The actual