taler-typescript-core

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

commit a1d706bfd1a7b80cb0011a377677df52c8212b78
parent c1556862b503e3460cb8903a4404dd41c818e348
Author: Florian Dold <florian@dold.me>
Date:   Tue,  6 May 2025 23:19:09 +0200

harness: more TOPS tests

Diffstat:
Mpackages/taler-harness/src/harness/tops.ts | 362+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mpackages/taler-harness/src/integrationtests/test-tops-aml-measures.ts | 396++++++++++---------------------------------------------------------------------
Apackages/taler-harness/src/integrationtests/test-tops-challenger-twice.ts | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 4+++-
4 files changed, 478 insertions(+), 356 deletions(-)

diff --git a/packages/taler-harness/src/harness/tops.ts b/packages/taler-harness/src/harness/tops.ts @@ -16,11 +16,19 @@ import { AccessToken, + AmlDecision, + Amounts, + AmountString, + ConfigSources, + Configuration, decodeCrock, + Duration, encodeCrock, hashNormalizedPaytoUri, j2s, + KycRule, KycStatusLongPollingReason, + LimitOperationType, OfficerAccount, OfficerId, parsePaytoUriOrThrow, @@ -30,6 +38,7 @@ import { TalerExchangeHttpClient, TalerMerchantInstanceHttpClient, TalerProtocolDuration, + TalerProtocolTimestamp, TalerWireGatewayHttpClient, } from "@gnu-taler/taler-util"; import { @@ -39,7 +48,10 @@ import { } from "@gnu-taler/taler-wallet-core"; import { logger } from "../integrationtests/test-tops-aml-kyx-natural.js"; import { CoinConfig, defaultCoinConfig } from "./denomStructures.js"; -import { TestfakeChallengerService } from "./fake-challenger.js"; +import { + startFakeChallenger, + TestfakeChallengerService, +} from "./fake-challenger.js"; import { BankService, DbInfo, @@ -508,8 +520,8 @@ KYC_OAUTH2_VALIDITY = 2 years KYC_OAUTH2_AUTHORIZE_URL = http://localhost:6002/authorize#setup KYC_OAUTH2_TOKEN_URL = http://localhost:6002/token KYC_OAUTH2_INFO_URL = http://localhost:6002/info -KYC_OAUTH2_CLIENT_ID = test-postal-id -KYC_OAUTH2_CLIENT_SECRET = test-postal-secret +KYC_OAUTH2_CLIENT_ID = test-sms-id +KYC_OAUTH2_CLIENT_SECRET = test-sms-secret KYC_OAUTH2_POST_URL = http://localhost:6002/done KYC_OAUTH2_CONVERTER_HELPER = taler-exchange-kyc-challenger-sms-converter KYC_OAUTH2_DEBUG_MODE = YES @@ -520,8 +532,8 @@ KYC_OAUTH2_VALIDITY = 2 years KYC_OAUTH2_AUTHORIZE_URL = http://localhost:6003/authorize#setup KYC_OAUTH2_TOKEN_URL = http://localhost:6003/token KYC_OAUTH2_INFO_URL = http://localhost:6003/info -KYC_OAUTH2_CLIENT_ID = test-postal-id -KYC_OAUTH2_CLIENT_SECRET = test-postal-secret +KYC_OAUTH2_CLIENT_ID = test-email-id +KYC_OAUTH2_CLIENT_SECRET = test-email-secret KYC_OAUTH2_POST_URL = http://localhost:6003/done KYC_OAUTH2_CONVERTER_HELPER = taler-exchange-kyc-challenger-email-converter KYC_OAUTH2_DEBUG_MODE = YES @@ -912,3 +924,343 @@ export async function doTopsAcceptTos( logger.info(`kyc status after accept-tos: ${j2s(kycStatus)}`); } } + +/** + * Helpers for the test. + */ +export interface MeasuresTestEnvironment { + expectInfo: () => Promise<void>; + expectFrozen: () => Promise<void>; + expectNotFrozen: () => Promise<void>; + expectInvestigate: () => Promise<void>; + expectNoInvestigate: () => Promise<void>; + fakeChallenger: ( + challenger: TestfakeChallengerService, + address: any, + ) => Promise<void>; + submitForm: (form: string, data: any) => Promise<void>; + decideMeasure: (measure: string) => Promise<{ + currentDecision: AmlDecision; + }>; + decideReset: () => Promise<void>; + challengerPostal: TestfakeChallengerService; + challengerSms: TestfakeChallengerService; +} + +export async function setupMeasuresTestEnvironment( + t: GlobalTestState, +): Promise<MeasuresTestEnvironment> { + // Set up test environment + const { + exchange, + officerAcc, + merchant, + exchangeBankAccount, + wireGatewayApi, + } = await createTopsEnvironment(t); + + const challengerPostal = await startFakeChallenger({ + port: 6001, + addressType: "postal-ch", + }); + const challengerSms = await startFakeChallenger({ + port: 6002, + addressType: "phone", + }); + + const merchantClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(), + ); + const exchangeClient = new TalerExchangeHttpClient(exchange.baseUrl, { + httpClient: harnessHttpLib, + }); + + // Do KYC auth transfer + const { accessToken, merchantPaytoHash } = await doTopsKycAuth(t, { + merchantClient, + exchangeBankAccount, + wireGatewayApi, + }); + + const myTriggerMeasure = (measure: string) => + doTriggerMeasure(t, { + officerAcc, + exchangeClient, + merchantPaytoHash, + measure, + }); + + const myTriggerReset = () => + doTriggerReset(t, { + officerAcc, + exchangeClient, + merchantPaytoHash, + }); + + const submitForm = async (form: string, data: any) => { + const kycInfoResp = await exchangeClient.checkKycInfo( + accessToken, + undefined, + undefined, + ); + t.assertDeepEqual(kycInfoResp.case, "ok"); + t.assertDeepEqual(kycInfoResp.body.requirements[0].form, form); + const requirementId = kycInfoResp.body.requirements[0].id; + t.assertTrue(typeof requirementId === "string"); + const uploadRes = await exchangeClient.uploadKycForm(requirementId, data); + console.log("upload res", uploadRes); + t.assertDeepEqual(uploadRes.case, "ok"); + }; + + const fakeChallenger = async ( + challenger: TestfakeChallengerService, + address: any, + ) => { + { + const kycInfoResp = await exchangeClient.checkKycInfo( + accessToken, + undefined, + undefined, + ); + console.log( + `kyc info after my-postal-registration measure`, + j2s(kycInfoResp), + ); + t.assertDeepEqual(kycInfoResp.case, "ok"); + const kycInfo = kycInfoResp.body; + t.assertDeepEqual(kycInfo.requirements[0].form, "LINK"); + t.assertTrue(typeof kycInfo.requirements[0].id === "string"); + t.assertDeepEqual(kycInfo.requirements.length, 1); + + const startResp = succeedOrThrow( + await exchangeClient.startExternalKycProcess( + kycInfo.requirements[0].id, + {}, + ), + ); + console.log(`start resp`, j2s(startResp)); + + let challengerRedirectUrl = startResp.redirect_url; + + const resp = await harnessHttpLib.fetch(challengerRedirectUrl); + const respJson = await resp.json(); + console.log(`challenger resp: ${j2s(respJson)}`); + + const nonce = respJson.nonce; + t.assertTrue(typeof nonce === "string"); + const proofRedirectUrl = respJson.redirect_url; + + challenger.fakeVerification(nonce, address); + + console.log("nonce", nonce); + console.log("proof redirect URL", proofRedirectUrl); + + const proofResp = await harnessHttpLib.fetch(proofRedirectUrl, { + redirect: "manual", + }); + console.log("proof status:", proofResp.status); + if (proofResp.status === 404) { + console.log(j2s(await proofResp.text())); + } + t.assertDeepEqual(proofResp.status, 303); + + const setupReq = challenger.getSetupRequest(nonce); + console.log(`setup request: ${j2s(setupReq)}`); + } + }; + + const expectInfo = async () => { + const kycInfoResp = await exchangeClient.checkKycInfo( + accessToken, + undefined, + undefined, + ); + t.assertDeepEqual(kycInfoResp.case, "ok"); + t.assertDeepEqual(kycInfoResp.body.requirements[0].form, "INFO"); + }; + + const getCurrentDecision = async () => { + const decisionsResp = succeedOrThrow( + await exchangeClient.getAmlDecisions(officerAcc, { + active: true, + }), + ); + console.log(j2s(decisionsResp)); + return decisionsResp.records[0]; + }; + + return { + challengerPostal, + challengerSms, + expectInfo, + fakeChallenger, + decideMeasure: myTriggerMeasure, + decideReset: myTriggerReset, + submitForm, + async expectFrozen() { + const dec = await getCurrentDecision(); + t.assertTrue(isFrozen(dec)); + }, + async expectNotFrozen() { + const dec = await getCurrentDecision(); + t.assertTrue(!isFrozen(dec)); + }, + async expectInvestigate() { + const dec = await getCurrentDecision(); + t.assertDeepEqual(dec.to_investigate, true); + }, + async expectNoInvestigate() { + const dec = await getCurrentDecision(); + t.assertDeepEqual(dec.to_investigate, false); + }, + }; +} + +export function isFrozen(decision: AmlDecision): boolean { + const txTypes = new Set<string>(); + for (const r of decision.limits.rules) { + if (!Amounts.isZero(r.threshold)) { + return false; + } + txTypes.add(r.operation_type); + } + if ( + !( + txTypes.has("WITHDRAW") && + txTypes.has("DEPOSIT") && + txTypes.has("AGGREGATE") && + txTypes.has("MERGE") && + txTypes.has("BALANCE") && + txTypes.has("CLOSE") && + txTypes.has("TRANSACTION") && + txTypes.has("REFUND") + ) + ) { + return false; + } + return true; +} + +async function doTriggerReset( + t: GlobalTestState, + args: { + officerAcc: OfficerAccount; + exchangeClient: TalerExchangeHttpClient; + merchantPaytoHash: string; + }, +): Promise<void> { + const { officerAcc, exchangeClient, merchantPaytoHash } = args; + const config = new Configuration(ConfigSources["taler-exchange"]); + config.loadFromString(topsKycRulesConf); + const rules: KycRule[] = []; + for (const secName of config.getSectionNames()) { + const rulePrefix = "kyc-rule-"; + if (!secName.toLowerCase().startsWith(rulePrefix)!) { + continue; + } + const enabled = config.getYesNo(secName, "ENABLED").required(); + if (!enabled) { + continue; + } + console.log("duration", config.getString(secName, "TIMEFRAME").required()); + rules.push({ + display_priority: 0, + measures: config + .getString(secName, "NEXT_MEASURES") + .required() + .split(/[ ]+/), + operation_type: config + .getString(secName, "OPERATION_TYPE") + .required() as LimitOperationType, + threshold: config + .getString(secName, "THRESHOLD") + .required() as AmountString, + timeframe: Duration.toTalerProtocolDuration( + Duration.fromPrettyString( + config.getString(secName, "TIMEFRAME").required(), + ), + ), + exposed: config.getYesNo(secName, "EXPOSED").required(), + is_and_combinator: config + .getYesNo(secName, "IS_AND_COMBINATIOR") + .orDefault(false), + rule_name: secName.substring(rulePrefix.length), + }); + } + succeedOrThrow( + await exchangeClient.makeAmlDesicion(officerAcc, { + decision_time: TalerProtocolTimestamp.now(), + h_payto: merchantPaytoHash, + justification: "reset", + properties: {}, + keep_investigating: false, + new_rules: { + custom_measures: {}, + expiration_time: TalerProtocolTimestamp.never(), + rules, + }, + }), + ); +} + +async function doTriggerMeasure( + t: GlobalTestState, + args: { + officerAcc: OfficerAccount; + exchangeClient: TalerExchangeHttpClient; + merchantPaytoHash: string; + measure: string; + }, +): Promise<{ currentDecision: AmlDecision }> { + const { officerAcc, exchangeClient, merchantPaytoHash } = args; + const decisionsResp = succeedOrThrow( + await exchangeClient.getAmlDecisions(officerAcc, { + active: true, + }), + ); + console.log(j2s(decisionsResp)); + + let toInvestigate: boolean; + let properties; + let rules: KycRule[]; + + if (decisionsResp.records.length == 0) { + toInvestigate = false; + properties = {}; + rules = []; + } else { + t.assertDeepEqual(decisionsResp.records.length, 1); + const rec = decisionsResp.records[0]; + t.assertDeepEqual(merchantPaytoHash, rec.h_payto); + toInvestigate = rec.to_investigate; + properties = rec.properties ?? {}; + rules = rec.limits.rules; + } + + succeedOrThrow( + await exchangeClient.makeAmlDesicion(officerAcc, { + decision_time: TalerProtocolTimestamp.now(), + h_payto: merchantPaytoHash, + justification: "bla", + properties: properties, + keep_investigating: toInvestigate, + new_measures: args.measure, + new_rules: { + custom_measures: {}, + expiration_time: TalerProtocolTimestamp.never(), + rules, + }, + }), + ); + + const decisionsRespAfter = succeedOrThrow( + await exchangeClient.getAmlDecisions(officerAcc, { + active: true, + }), + ); + + t.assertDeepEqual(decisionsRespAfter.records.length, 1); + return { + currentDecision: decisionsRespAfter.records[0], + }; +} diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-measures.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-measures.ts @@ -17,33 +17,9 @@ /** * Imports. */ -import { - AmlDecision, - Amounts, - AmountString, - ConfigSources, - Configuration, - Duration, - j2s, - KycRule, - LimitOperationType, - Logger, - OfficerAccount, - succeedOrThrow, - TalerExchangeHttpClient, - TalerMerchantInstanceHttpClient, - TalerProtocolTimestamp, -} from "@gnu-taler/taler-util"; -import { - startFakeChallenger, - TestfakeChallengerService, -} from "../harness/fake-challenger.js"; -import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; -import { - createTopsEnvironment, - doTopsKycAuth, - topsKycRulesConf, -} from "../harness/tops.js"; +import { j2s, Logger } from "@gnu-taler/taler-util"; +import { GlobalTestState } from "../harness/harness.js"; +import { isFrozen, setupMeasuresTestEnvironment } from "../harness/tops.js"; export const logger = new Logger("test-tops-aml-measures.ts"); @@ -54,17 +30,21 @@ export async function runTopsAmlMeasuresTest(t: GlobalTestState) { // Setup is done, now the real testing can start! const { - myTriggerMeasure, - myTriggerReset, + decideMeasure, + decideReset, submitForm, - challengerPostal, - challengerSms, expectInfo, + expectInvestigate, + expectNoInvestigate, + expectFrozen, fakeChallenger, + challengerPostal, + challengerSms, } = await setupMeasuresTestEnvironment(t); { - await myTriggerMeasure("kyx"); + await decideMeasure("kyx"); + await expectNoInvestigate(); await submitForm("vqf_902_1_customer", { FORM_ID: "vqf_902_1_customer", FORM_VERSION: 1, @@ -73,37 +53,59 @@ export async function runTopsAmlMeasuresTest(t: GlobalTestState) { FULL_NAME: "Alice A", DOMICILE_ADDRESS: "Castle St. 1\nWondertown", }); + await expectNoInvestigate(); + // FIXME: Re-enable once this doesn't confuse the exchange anymore + // await fakeChallenger(challengerPostal, { + // CONTACT_NAME: "Richard Stallman", + // ADDRESS_LINES: "Bundesgasse 1\n1234 Bern", + // }); + // await expectInvestigate(); + } + + await decideReset(); + + // Test with weird customer type + { + await decideMeasure("kyx"); + await expectNoInvestigate(); + await submitForm("vqf_902_1_customer", { + FORM_ID: "vqf_902_1_customer", + FORM_VERSION: 1, + CUSTOMER_TYPE: "FOO", + CUSTOMER_TYPE_VQF: "FOO", + }); + await expectInvestigate(); } await t.assertThrowsTalerErrorAsync(async () => { - await myTriggerMeasure("foobar"); + await decideMeasure("foobar"); }); { - const res = await myTriggerMeasure("freeze-investigate"); + const res = await decideMeasure("freeze-investigate"); console.log("after freeze-investigate:", j2s(res)); t.assertTrue(isFrozen(res.currentDecision)); } - await myTriggerReset(); + await decideReset(); { - const res = await myTriggerMeasure("inform-investigate"); + const res = await decideMeasure("inform-investigate"); console.log("after inform-investigate:", j2s(res)); t.assertTrue(!isFrozen(res.currentDecision)); } - await myTriggerReset(); + await decideReset(); { - await myTriggerMeasure("sms-registration"); + await decideMeasure("sms-registration"); await fakeChallenger(challengerSms, { CONTACT_PHONE: "+4123456789", }); } { - const res = await myTriggerMeasure("postal-registration"); + await decideMeasure("postal-registration"); await fakeChallenger(challengerPostal, { CONTACT_NAME: "Richard Stallman", ADDRESS_LINES: "Bundesgasse 1\n1234 Bern", @@ -111,7 +113,7 @@ export async function runTopsAmlMeasuresTest(t: GlobalTestState) { } { - await myTriggerMeasure("accept-tos"); + await decideMeasure("accept-tos"); await submitForm("accept-tos", { FORM_ID: "accept-tos", FORM_VERSION: 1, @@ -120,7 +122,7 @@ export async function runTopsAmlMeasuresTest(t: GlobalTestState) { } { - await myTriggerMeasure("form-vqf-902.9"); + await decideMeasure("form-vqf-902.9"); await submitForm("vqf_902_9_customer", { // Form is not validated yet FORM_ID: "vqf_902_9_customer", @@ -129,7 +131,7 @@ export async function runTopsAmlMeasuresTest(t: GlobalTestState) { } { - await myTriggerMeasure("form-vqf-902.11"); + await decideMeasure("form-vqf-902.11"); // Form is not validated yet await submitForm("vqf_902_11_customer", { FORM_ID: "vqf_902_11_customer", @@ -143,10 +145,10 @@ export async function runTopsAmlMeasuresTest(t: GlobalTestState) { }); } - await myTriggerReset(); + await decideReset(); { - await myTriggerMeasure("form-vqf-902.11"); + await decideMeasure("form-vqf-902.11"); // Form is not validated yet await submitForm("vqf_902_11_customer", { FORM_ID: "vqf_902_11_customer", @@ -158,310 +160,4 @@ export async function runTopsAmlMeasuresTest(t: GlobalTestState) { } } -/** - * Helpers for the test. - */ -interface MeasuresTestEnvironment { - expectInfo: () => Promise<void>; - fakeChallenger: ( - challenger: TestfakeChallengerService, - address: any, - ) => Promise<void>; - submitForm: (form: string, data: any) => Promise<void>; - myTriggerMeasure: (measure: string) => Promise<{ - currentDecision: AmlDecision; - }>; - myTriggerReset: () => Promise<void>; - challengerPostal: TestfakeChallengerService; - challengerSms: TestfakeChallengerService; -} - -async function setupMeasuresTestEnvironment( - t: GlobalTestState, -): Promise<MeasuresTestEnvironment> { - // Set up test environment - const { - exchange, - officerAcc, - merchant, - exchangeBankAccount, - wireGatewayApi, - } = await createTopsEnvironment(t); - - const challengerPostal = await startFakeChallenger({ - port: 6001, - addressType: "postal-ch", - }); - const challengerSms = await startFakeChallenger({ - port: 6002, - addressType: "phone", - }); - - const merchantClient = new TalerMerchantInstanceHttpClient( - merchant.makeInstanceBaseUrl(), - ); - const exchangeClient = new TalerExchangeHttpClient(exchange.baseUrl, { - httpClient: harnessHttpLib, - }); - - // Do KYC auth transfer - const { accessToken, merchantPaytoHash } = await doTopsKycAuth(t, { - merchantClient, - exchangeBankAccount, - wireGatewayApi, - }); - - const myTriggerMeasure = (measure: string) => - triggerMeasure(t, { - officerAcc, - exchangeClient, - merchantPaytoHash, - measure, - }); - - const myTriggerReset = () => - triggerReset(t, { - officerAcc, - exchangeClient, - merchantPaytoHash, - }); - - const submitForm = async (form: string, data: any) => { - const kycInfoResp = await exchangeClient.checkKycInfo( - accessToken, - undefined, - undefined, - ); - t.assertDeepEqual(kycInfoResp.case, "ok"); - t.assertDeepEqual(kycInfoResp.body.requirements[0].form, form); - const requirementId = kycInfoResp.body.requirements[0].id; - t.assertTrue(typeof requirementId === "string"); - const uploadRes = await exchangeClient.uploadKycForm(requirementId, data); - console.log("upload res", uploadRes); - t.assertDeepEqual(uploadRes.case, "ok"); - }; - - const fakeChallenger = async ( - challenger: TestfakeChallengerService, - address: any, - ) => { - { - const kycInfoResp = await exchangeClient.checkKycInfo( - accessToken, - undefined, - undefined, - ); - console.log( - `kyc info after my-postal-registration measure`, - j2s(kycInfoResp), - ); - t.assertDeepEqual(kycInfoResp.case, "ok"); - const kycInfo = kycInfoResp.body; - t.assertDeepEqual(kycInfo.requirements[0].form, "LINK"); - t.assertTrue(typeof kycInfo.requirements[0].id === "string"); - - const startResp = succeedOrThrow( - await exchangeClient.startExternalKycProcess( - kycInfo.requirements[0].id, - {}, - ), - ); - console.log(`start resp`, j2s(startResp)); - - let challengerRedirectUrl = startResp.redirect_url; - - const resp = await harnessHttpLib.fetch(challengerRedirectUrl); - const respJson = await resp.json(); - console.log(`challenger resp: ${j2s(respJson)}`); - - const nonce = respJson.nonce; - t.assertTrue(typeof nonce === "string"); - const proofRedirectUrl = respJson.redirect_url; - - challenger.fakeVerification(nonce, address); - - console.log("nonce", nonce); - console.log("proof redirect URL", proofRedirectUrl); - - const proofResp = await harnessHttpLib.fetch(proofRedirectUrl, { - redirect: "manual", - }); - console.log("proof status:", proofResp.status); - t.assertDeepEqual(proofResp.status, 303); - - const setupReq = challenger.getSetupRequest(nonce); - console.log(`setup request: ${j2s(setupReq)}`); - } - }; - - const expectInfo = async () => { - const kycInfoResp = await exchangeClient.checkKycInfo( - accessToken, - undefined, - undefined, - ); - t.assertDeepEqual(kycInfoResp.case, "ok"); - t.assertDeepEqual(kycInfoResp.body.requirements[0].form, "INFO"); - }; - - return { - challengerPostal, - challengerSms, - expectInfo, - fakeChallenger, - myTriggerMeasure, - myTriggerReset, - submitForm, - }; -} - -function isFrozen(decision: AmlDecision): boolean { - const txTypes = new Set<string>(); - for (const r of decision.limits.rules) { - if (!Amounts.isZero(r.threshold)) { - return false; - } - txTypes.add(r.operation_type); - } - if ( - !( - txTypes.has("WITHDRAW") && - txTypes.has("DEPOSIT") && - txTypes.has("AGGREGATE") && - txTypes.has("MERGE") && - txTypes.has("BALANCE") && - txTypes.has("CLOSE") && - txTypes.has("TRANSACTION") && - txTypes.has("REFUND") - ) - ) { - return false; - } - return true; -} - -async function triggerReset( - t: GlobalTestState, - args: { - officerAcc: OfficerAccount; - exchangeClient: TalerExchangeHttpClient; - merchantPaytoHash: string; - }, -): Promise<void> { - const { officerAcc, exchangeClient, merchantPaytoHash } = args; - const config = new Configuration(ConfigSources["taler-exchange"]); - config.loadFromString(topsKycRulesConf); - const rules: KycRule[] = []; - for (const secName of config.getSectionNames()) { - const rulePrefix = "kyc-rule-"; - if (!secName.toLowerCase().startsWith(rulePrefix)!) { - continue; - } - const enabled = config.getYesNo(secName, "ENABLED").required(); - if (!enabled) { - continue; - } - console.log("duration", config.getString(secName, "TIMEFRAME").required()); - rules.push({ - display_priority: 0, - measures: config - .getString(secName, "NEXT_MEASURES") - .required() - .split(/[ ]+/), - operation_type: config - .getString(secName, "OPERATION_TYPE") - .required() as LimitOperationType, - threshold: config - .getString(secName, "THRESHOLD") - .required() as AmountString, - timeframe: Duration.toTalerProtocolDuration( - Duration.fromPrettyString( - config.getString(secName, "TIMEFRAME").required(), - ), - ), - exposed: config.getYesNo(secName, "EXPOSED").required(), - is_and_combinator: config - .getYesNo(secName, "IS_AND_COMBINATIOR") - .orDefault(false), - rule_name: secName.substring(rulePrefix.length), - }); - } - succeedOrThrow( - await exchangeClient.makeAmlDesicion(officerAcc, { - decision_time: TalerProtocolTimestamp.now(), - h_payto: merchantPaytoHash, - justification: "reset", - properties: {}, - keep_investigating: false, - new_rules: { - custom_measures: {}, - expiration_time: TalerProtocolTimestamp.never(), - rules, - }, - }), - ); -} - -async function triggerMeasure( - t: GlobalTestState, - args: { - officerAcc: OfficerAccount; - exchangeClient: TalerExchangeHttpClient; - merchantPaytoHash: string; - measure: string; - }, -): Promise<{ currentDecision: AmlDecision }> { - const { officerAcc, exchangeClient, merchantPaytoHash } = args; - const decisionsResp = succeedOrThrow( - await exchangeClient.getAmlDecisions(officerAcc, { - active: true, - }), - ); - console.log(j2s(decisionsResp)); - - let toInvestigate: boolean; - let properties; - let rules: KycRule[]; - - if (decisionsResp.records.length == 0) { - toInvestigate = false; - properties = {}; - rules = []; - } else { - t.assertDeepEqual(decisionsResp.records.length, 1); - const rec = decisionsResp.records[0]; - t.assertDeepEqual(merchantPaytoHash, rec.h_payto); - toInvestigate = rec.to_investigate; - properties = rec.properties ?? {}; - rules = rec.limits.rules; - } - - succeedOrThrow( - await exchangeClient.makeAmlDesicion(officerAcc, { - decision_time: TalerProtocolTimestamp.now(), - h_payto: merchantPaytoHash, - justification: "bla", - properties: properties, - keep_investigating: toInvestigate, - new_measures: args.measure, - new_rules: { - custom_measures: {}, - expiration_time: TalerProtocolTimestamp.never(), - rules, - }, - }), - ); - - const decisionsRespAfter = succeedOrThrow( - await exchangeClient.getAmlDecisions(officerAcc, { - active: true, - }), - ); - - t.assertDeepEqual(decisionsRespAfter.records.length, 1); - return { - currentDecision: decisionsRespAfter.records[0], - }; -} - runTopsAmlMeasuresTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-tops-challenger-twice.ts b/packages/taler-harness/src/integrationtests/test-tops-challenger-twice.ts @@ -0,0 +1,72 @@ +/* + This file is part of GNU Taler + (C) 2025 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 { Logger } from "@gnu-taler/taler-util"; +import { GlobalTestState } from "../harness/harness.js"; +import { setupMeasuresTestEnvironment } from "../harness/tops.js"; + +export const logger = new Logger("test-tops-aml-measures.ts"); + +/** + * Test that reproduces an exchange bug, where the exchange + * gets confused by the same account doing the same challenger + * authentication twice. + */ +export async function runTopsChallengerTwiceTest(t: GlobalTestState) { + const { + decideMeasure, + decideReset, + submitForm, + expectInvestigate, + expectNoInvestigate, + fakeChallenger, + challengerPostal, + } = await setupMeasuresTestEnvironment(t); + + { + await decideMeasure("kyx"); + await expectNoInvestigate(); + await submitForm("vqf_902_1_customer", { + FORM_ID: "vqf_902_1_customer", + FORM_VERSION: 1, + CUSTOMER_TYPE: "NATURAL_PERSON", + CUSTOMER_TYPE_VQF: "NATURAL_PERSON", + FULL_NAME: "Alice A", + DOMICILE_ADDRESS: "Castle St. 1\nWondertown", + }); + await expectNoInvestigate(); + await fakeChallenger(challengerPostal, { + CONTACT_NAME: "Richard Stallman", + ADDRESS_LINES: "Bundesgasse 1\n1234 Bern", + }); + await expectInvestigate(); + } + + await decideReset(); + + { + await decideMeasure("postal-registration"); + await fakeChallenger(challengerPostal, { + CONTACT_NAME: "Richard Stallman", + ADDRESS_LINES: "Bundesgasse 1\n1234 Bern", + }); + } +} + +runTopsChallengerTwiceTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -122,9 +122,10 @@ import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw.js"; import { runTopsAmlBasicTest } from "./test-tops-aml-basic.js"; import { runTopsAmlCustomAddrPostalTest } from "./test-tops-aml-custom-addr-postal.js"; import { runTopsAmlCustomAddrSmsTest } from "./test-tops-aml-custom-addr-sms.js"; -import { runTopsAmlMeasuresTest } from "./test-tops-aml-measures.js"; import { runTopsAmlKyxNaturalTest } from "./test-tops-aml-kyx-natural.js"; import { runTopsAmlLegiTest } from "./test-tops-aml-legi.js"; +import { runTopsAmlMeasuresTest } from "./test-tops-aml-measures.js"; +import { runTopsChallengerTwiceTest } from "./test-tops-challenger-twice.js"; import { runTermOfServiceFormatTest } from "./test-tos-format.js"; import { runWalletBackupBasicTest } from "./test-wallet-backup-basic.js"; import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend.js"; @@ -330,6 +331,7 @@ const allTests: TestMainFunction[] = [ runTopsAmlKyxNaturalTest, runTopsAmlMeasuresTest, runTopsAmlLegiTest, + runTopsChallengerTwiceTest, ]; export interface TestRunSpec {