commit a1d706bfd1a7b80cb0011a377677df52c8212b78
parent c1556862b503e3460cb8903a4404dd41c818e348
Author: Florian Dold <florian@dold.me>
Date: Tue, 6 May 2025 23:19:09 +0200
harness: more TOPS tests
Diffstat:
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 {