taler-typescript-core

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

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:
Mpackages/taler-harness/src/harness/fake-challenger.ts | 3++-
Mpackages/taler-harness/src/integrationtests/test-tops-aml-basic.ts | 1+
Mpackages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-postal.ts | 1+
Apackages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-sms.ts | 274+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
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 {