taler-typescript-core

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

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:
Mpackages/taler-harness/src/harness/environments.ts | 4++--
Mpackages/taler-harness/src/index.ts | 2++
Mpackages/taler-harness/src/integrationtests/test-kyc-decisions.ts | 5+++--
Mpackages/taler-harness/src/integrationtests/test-kyc-new-measure.ts | 2+-
Apackages/taler-harness/src/integrationtests/test-kyc-new-measures-prog.ts | 318+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/test-kyc-skip-expiration.ts | 7+++----
Mpackages/taler-harness/src/integrationtests/test-kyc-withdrawal-verboten.ts | 3++-
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-util/src/types-taler-exchange.ts | 2++
Mpackages/taler-util/src/types-taler-kyc-aml.ts | 10++++++++++
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