commit d89f0bf6322a8b4540a633c0760cecbfa24e6355
parent 7f0956d858351eb04479b9b27e88ed902d07d159
Author: Florian Dold <florian@dold.me>
Date: Tue, 6 May 2025 03:33:33 +0200
harness: test tops SMS validation
Diffstat:
5 files changed, 280 insertions(+), 1 deletion(-)
diff --git a/packages/taler-harness/src/harness/fake-challenger.ts b/packages/taler-harness/src/harness/fake-challenger.ts
@@ -74,10 +74,11 @@ function respondJson(
*/
export async function startFakeChallenger(options: {
port: number;
+ addressType: string;
}): Promise<TestfakeChallengerService> {
let nextNonceId = 1;
- const addressType = "postal-ch";
+ const addressType = options.addressType;
const infoForNonceId: Map<
number,
diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-basic.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-basic.ts
@@ -55,6 +55,7 @@ export async function runTopsAmlBasicTest(t: GlobalTestState) {
const challenger = await startFakeChallenger({
port: 6001,
+ addressType: "postal-ch",
});
// Withdrawal below threshold succeeds!
diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-postal.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-postal.ts
@@ -55,6 +55,7 @@ export async function runTopsAmlCustomAddrPostalTest(t: GlobalTestState) {
const challenger = await startFakeChallenger({
port: 6001,
+ addressType: "postal-ch",
});
const merchantClient = new TalerMerchantInstanceHttpClient(
diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-sms.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-sms.ts
@@ -0,0 +1,274 @@
+/*
+ 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 {
+ AccessToken,
+ decodeCrock,
+ encodeCrock,
+ hashNormalizedPaytoUri,
+ j2s,
+ KycStatusLongPollingReason,
+ Logger,
+ OfficerAccount,
+ OfficerId,
+ parsePaytoUriOrThrow,
+ succeedOrThrow,
+ TalerExchangeHttpClient,
+ TalerMerchantInstanceHttpClient,
+ TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
+import { startFakeChallenger } from "../harness/fake-challenger.js";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+import { createTopsEnvironment } from "../harness/topsConfig.js";
+
+const logger = new Logger("test-tops-aml.ts");
+
+/**
+ * Test for the custom address validation measures.
+ */
+export async function runTopsAmlCustomAddrSmsTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ exchange,
+ amlKeypair,
+ merchant,
+ exchangeBankAccount,
+ wireGatewayApi,
+ } = await createTopsEnvironment(t);
+
+ const challenger = await startFakeChallenger({
+ port: 6002,
+ addressType: "phone",
+ });
+
+ const merchantClient = new TalerMerchantInstanceHttpClient(
+ merchant.makeInstanceBaseUrl(),
+ );
+ // Do KYC auth transfer
+ {
+ const kycStatus = await merchantClient.getCurrentInstanceKycStatus(
+ undefined,
+ {},
+ );
+
+ console.log(`kyc status: ${j2s(kycStatus)}`);
+
+ t.assertDeepEqual(kycStatus.case, "ok");
+
+ t.assertTrue(kycStatus.body != null);
+
+ t.assertDeepEqual(kycStatus.body.kyc_data[0].status, "kyc-wire-required");
+
+ const depositPaytoUri = kycStatus.body.kyc_data[0].payto_uri;
+ t.assertTrue(kycStatus.body.kyc_data[0].payto_kycauths != null);
+ const authTxPayto = parsePaytoUriOrThrow(
+ kycStatus.body.kyc_data[0]?.payto_kycauths[0],
+ );
+ const authTxMessage = authTxPayto?.params["message"];
+ t.assertTrue(typeof authTxMessage === "string");
+ t.assertTrue(authTxMessage.startsWith("KYC:"));
+ const accountPub = authTxMessage.substring(4);
+ logger.info(`merchant account pub: ${accountPub}`);
+ await wireGatewayApi.addKycAuth({
+ auth: exchangeBankAccount.wireGatewayAuth,
+ body: {
+ amount: "CHF:0.1",
+ debit_account: depositPaytoUri,
+ account_pub: accountPub,
+ },
+ });
+ }
+
+ let accessToken: AccessToken;
+ let merchantPaytoHash: string;
+
+ // Wait for auth transfer to be registered by the exchange
+ {
+ const kycStatus = await merchantClient.getCurrentInstanceKycStatus(
+ undefined,
+ {
+ reason: KycStatusLongPollingReason.AUTH_TRANSFER,
+ timeout: 30000,
+ },
+ );
+ logger.info(`kyc status after transfer: ${j2s(kycStatus)}`);
+ t.assertDeepEqual(kycStatus.case, "ok");
+ t.assertTrue(kycStatus.body != null);
+ t.assertDeepEqual(kycStatus.body.kyc_data[0].status, "kyc-required");
+ t.assertTrue(typeof kycStatus.body.kyc_data[0].access_token === "string");
+ accessToken = kycStatus.body.kyc_data[0].access_token as AccessToken;
+ merchantPaytoHash = encodeCrock(
+ hashNormalizedPaytoUri(kycStatus.body.kyc_data[0].payto_uri),
+ );
+ }
+
+ const exchangeClient = new TalerExchangeHttpClient(exchange.baseUrl, {
+ httpClient: harnessHttpLib,
+ });
+
+ // Accept ToS
+ {
+ const kycInfo = await exchangeClient.checkKycInfo(
+ accessToken,
+ undefined,
+ undefined,
+ );
+ console.log(j2s(kycInfo));
+
+ t.assertDeepEqual(kycInfo.case, "ok");
+ t.assertDeepEqual(kycInfo.body.requirements.length, 1);
+ t.assertDeepEqual(kycInfo.body.requirements[0].form, "accept-tos");
+ const requirementId = kycInfo.body.requirements[0].id;
+ t.assertTrue(typeof requirementId === "string");
+
+ const uploadRes = await exchangeClient.uploadKycForm(requirementId, {
+ FORM_ID: "accept-tos",
+ FORM_VERSION: 1,
+ ACCEPTED_TERMS_OF_SERVICE: "v1",
+ });
+ console.log("upload res", uploadRes);
+ t.assertDeepEqual(uploadRes.case, "ok");
+ }
+
+ {
+ const kycInfo = await exchangeClient.checkKycInfo(
+ accessToken,
+ undefined,
+ undefined,
+ );
+ console.log(j2s(kycInfo));
+
+ // FIXME: Do we expect volunary measures here?
+ // => not yet, see https://bugs.gnunet.org/view.php?id=9879
+ }
+
+ await merchant.runKyccheckOnce();
+
+ {
+ const kycStatus = await merchantClient.getCurrentInstanceKycStatus(
+ undefined,
+ {
+ reason: KycStatusLongPollingReason.AUTH_TRANSFER,
+ timeout: 30000,
+ },
+ );
+ logger.info(`kyc status after accept-tos: ${j2s(kycStatus)}`);
+ }
+
+ const officerAcc: OfficerAccount = {
+ id: amlKeypair.pub as OfficerId,
+ signingKey: decodeCrock(amlKeypair.priv),
+ };
+
+ // Trigger postal registration check
+ // via AML officer.
+ {
+ const decisionsResp = succeedOrThrow(
+ await exchangeClient.getAmlDecisions(officerAcc, {
+ active: true,
+ }),
+ );
+ console.log(j2s(decisionsResp));
+
+ t.assertDeepEqual(decisionsResp.records.length, 1);
+ const rec = decisionsResp.records[0];
+
+ t.assertDeepEqual(merchantPaytoHash, rec.h_payto);
+
+ succeedOrThrow(
+ await exchangeClient.makeAmlDesicion(officerAcc, {
+ decision_time: TalerProtocolTimestamp.now(),
+ h_payto: rec.h_payto,
+ justification: "bla",
+ properties: rec.properties ?? {},
+ keep_investigating: rec.to_investigate,
+ new_measures: "my-sms-registration",
+ new_rules: {
+ custom_measures: {
+ "my-sms-registration": {
+ prog_name: "challenger-sms-from-context",
+ context: {
+ CONTACT_PHONE: "+4123456789",
+ },
+ check_name: "SKIP",
+ },
+ },
+ expiration_time: TalerProtocolTimestamp.never(),
+ rules: rec.limits.rules,
+ },
+ }),
+ );
+ }
+
+ {
+ 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, {
+ CONTACT_PHONE: "+4123456789",
+ });
+
+ 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)}`);
+
+ t.assertDeepEqual(setupReq.CONTACT_PHONE, "+4123456789");
+ t.assertDeepEqual(setupReq.read_only, true);
+ }
+}
+
+runTopsAmlCustomAddrSmsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -121,6 +121,7 @@ import { runTimetravelAutorefreshTest } from "./test-timetravel-autorefresh.js";
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 { runTermOfServiceFormatTest } from "./test-tos-format.js";
import { runWalletBackupBasicTest } from "./test-wallet-backup-basic.js";
import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend.js";
@@ -322,6 +323,7 @@ const allTests: TestMainFunction[] = [
runWalletDevexpFakeprotoverTest,
runTopsAmlBasicTest,
runTopsAmlCustomAddrPostalTest,
+ runTopsAmlCustomAddrSmsTest,
];
export interface TestRunSpec {