taler-typescript-core

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

commit af12cf24122fe2dba1ff8fed747e7c7813da2aad
parent 16ca230ad87bdb72eb845d29dc9761e86ae31bf2
Author: Florian Dold <florian@dold.me>
Date:   Fri,  8 May 2026 16:30:13 +0200

harness: fix and simplify self-provisioning tests (still WIP)

Diffstat:
Mpackages/taler-harness/src/harness/tan-helper.ts | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mpackages/taler-harness/src/index.ts | 64++++++++++++++++++++++++++++++++++------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-self-provision-activation-two-bank-account.ts | 176+++++++++++++++++++++++++++++++++++++------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts | 106++++++++++++++++++++++++++++++++++---------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-self-provision-forgot-password.ts | 88++++++++++++++++++++++++++++---------------------------------------------------
Mpackages/taler-harness/src/integrationtests/test-merchant-self-provision-inactive-account-permissions.ts | 111++++++++++++++++++++++++-------------------------------------------------------
6 files changed, 361 insertions(+), 360 deletions(-)

diff --git a/packages/taler-harness/src/harness/tan-helper.ts b/packages/taler-harness/src/harness/tan-helper.ts @@ -14,53 +14,149 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Logger } from "@gnu-taler/taler-util"; -import { createServer } from "net"; +import * as fs from "node:fs"; + +import { + ChallengeResponse, + Configuration, + Logger, + succeedOrThrow, + TalerMerchantInstanceHttpClient, + TanChannel, +} from "@gnu-taler/taler-util"; +import { GlobalTestState, waitMs } from "./harness.js"; const logger = new Logger("tan-helper.ts"); -export interface TestTanHelperService { - stop: () => void; - getLastCodeForAddress(addr: string): string; +export async function wait2FaCode( + path: string, +): Promise<{ address: string; code: string }> { + while (true) { + if (!fs.existsSync(path)) { + logger.info(`waiting for 2fa code at ${path}`); + await waitMs(500); + continue; + } + const res = JSON.parse(fs.readFileSync(path, "utf-8")); + if (typeof res.address != "string" || typeof res.code != "string") { + throw Error("bad format"); + } + return res; + } } -/** - * Tan helper - */ -export async function startTanHelper(opts: { - socketFile?: string; -}): Promise<TestTanHelperService> { - const lastCodeForAddr: Record<string, string> = {}; - - const server = createServer((socket) => { - socket.on("data", (data) => { - try { - const jsonData = JSON.parse(data.toString().trim()); - const args: string[] = jsonData.args; - const idx = args.findIndex(a => a === "--") - if (idx === -1) throw Error("missing '--' mark") - const addr = args[idx+1] - const code = jsonData.stdin; - lastCodeForAddr[addr] = code; - } catch (error) { - console.error("Error parsing JSON:", error); - } - }); - - socket.on("end", () => { - console.log("Client disconnected"); - }); - }); - - await new Promise<void>((resolve, reject) => { - server.listen(opts.socketFile ?? "/tmp/tan-helper.socket", () => resolve()); - }); +export type TestMfaChannelConfig = Partial< + Record<TanChannel, { path: string; address: string }> +>; + +export async function solveMFA( + t: GlobalTestState, + api: TalerMerchantInstanceHttpClient, + resp: ChallengeResponse, + addrs: TestMfaChannelConfig, +) { + for (const { + challenge_id: challenge, + tan_channel: type, + } of resp.challenges) { + const addrUsed = addrs[type]; + if (!addrUsed) { + throw Error( + `The MFA is asking unexpected channel: ${type} id ${challenge}`, + ); + } + await doChallenge(t, api, challenge, addrUsed.address, addrUsed.path); + } +} + +export async function doChallenge( + t: GlobalTestState, + merchantClient: TalerMerchantInstanceHttpClient, + challengeId: string, + address: string, + mfaHelperResultPath: string, +): Promise<void> { + succeedOrThrow(await merchantClient.sendChallenge(challengeId)); + const res = await wait2FaCode(mfaHelperResultPath); + t.assertDeepEqual(res.address, address); + succeedOrThrow( + await merchantClient.confirmChallenge(challengeId, { + tan: res.code, + }), + ); +} + +export interface TestMfaChannelConfigEmailSms { + [TanChannel.EMAIL]: { + path: string; + address: string; + }; + + [TanChannel.SMS]: { + path: string; + address: string; + }; +} + +export function makeMfaConfigEmailSms( + t: GlobalTestState, + email: string, + sms: string, +): TestMfaChannelConfigEmailSms { + const pathSms = `${t.testDir}/2fa-sms.json`; + const pathEmail = `${t.testDir}/2fa-email.json`; return { - stop() { - server.close(); + [TanChannel.EMAIL]: { + address: email, + path: pathEmail, }, - getLastCodeForAddress(address: string): string { - return lastCodeForAddr[address]; + [TanChannel.SMS]: { + address: sms, + path: pathSms, }, }; } + +export interface TestMfaChannelConfigEmailOnly { + [TanChannel.EMAIL]: { + path: string; + address: string; + }; +} + +export function makeMfaConfigEmailOnly( + t: GlobalTestState, + email: string, +): TestMfaChannelConfigEmailOnly { + const pathEmail = `${t.testDir}/2fa-email.json`; + return { + [TanChannel.EMAIL]: { + address: email, + path: pathEmail, + }, + }; +} + +export function configureTestMerchantMfa( + cfg: Configuration, + config: TestMfaChannelConfig, +): void { + let channels = []; + if (config.email != null) { + cfg.setString( + "merchant", + "HELPER_EMAIL", + `taler-harness helper-2fa-dump ${config.email.path}`, + ); + channels.push("email"); + } + if (config.sms != null) { + cfg.setString( + "merchant", + "HELPER_SMS", + `taler-harness helper-2fa-dump ${config.sms.path}`, + ); + channels.push("sms"); + } + cfg.setString("merchant", "MANDATORY_TAN_CHANNELS", channels.join(" ")); +} diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -83,7 +83,6 @@ import { AML_PROGRAM_FROM_ATTRIBUTES_TO_CONTEXT } from "integrationtests/test-ky import { AML_PROGRAM_NEXT_MEASURE_FORM } from "integrationtests/test-kyc-two-forms.js"; import { execSync } from "node:child_process"; import fs from "node:fs"; -import net from "node:net"; import os from "node:os"; import path from "node:path"; import postgres from "postgres"; @@ -930,13 +929,13 @@ deploymentCli } bankAdminAuth = { type: "bearer", - token: resp.body.access_token - } + token: resp.body.access_token, + }; } else if (bankAdminTokenArg != null) { bankAdminAuth = { type: "bearer", - token: bankAdminTokenArg - } + token: bankAdminTokenArg, + }; } let merchantAdminToken: AccessToken | undefined; @@ -975,9 +974,9 @@ deploymentCli contact_data: email || phone ? { - email: email, - phone: phone, - } + email: email, + phone: phone, + } : undefined, }); @@ -1340,10 +1339,10 @@ deploymentCli credit_facade_credentials: bankUser && bankPassword ? { - type: "basic", - username: bankUser, - password: bankPassword, - } + type: "basic", + username: bankUser, + password: bankPassword, + } : undefined, }); if (createAccountResp.type != "ok") { @@ -1777,26 +1776,31 @@ export const helpersCli = talerHarnessCli.subcommand( ); talerHarnessCli - .subcommand("runHelper", "run-helper") - .requiredOption("socket", ["-s", "--socket"], clk.STRING) - .maybeArgument("rest1", clk.STRING) - .maybeArgument("rest2", clk.STRING) - .maybeArgument("rest3", clk.STRING) - .maybeArgument("rest4", clk.STRING) - .maybeArgument("rest5", clk.STRING) - .maybeArgument("rest6", clk.STRING) + .subcommand("helper2faDump", "helper-2fa-dump", { + help: "Helper used in integration tests.", + }) + .requiredArgument("path", clk.STRING) + .requiredArgument("address", clk.STRING) .action(async (args) => { - const socketPath = args.runHelper.socket; - const input = fs.readFileSync(0, "utf-8").trim(); - - const stream = net.createConnection({ path: socketPath }); - stream.write( + const path = args.helper2faDump.path; + const address = args.helper2faDump.address; + const input = await read(process.stdin); + logger.info( + `started 2fa dump helper with target path ${path} and address ${address}`, + ); + logger.info(`got input: ${input}`); + const searchRes = /[0-9-]+/.exec(input); + if (!searchRes) { + throw Error("could not find code in message"); + } + const code = searchRes[0]; + fs.writeFileSync( + path, JSON.stringify({ - stdin: input, - args: process.argv, + address, + code, }), ); - stream.end(); }); export const amlProgramCli = talerHarnessCli.subcommand( @@ -2001,8 +2005,8 @@ merchantCli const scope = args.token.scope ?? "all"; const duration = args.token.duration ? Duration.toTalerProtocolDuration( - Duration.fromPrettyString(args.token.duration), - ) + Duration.fromPrettyString(args.token.duration), + ) : undefined; const tokResp = await merchantApi.createAccessToken(instance, password, { scope: scope as LoginTokenScope, diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation-two-bank-account.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation-two-bank-account.ts @@ -26,14 +26,18 @@ import { succeedOrThrow, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient, - TanChannel, } from "@gnu-taler/taler-util"; import { createSimpleTestkudosEnvironmentV3 } from "harness/environments.js"; -import { startTanHelper } from "harness/tan-helper.js"; -import { randomBytes } from "node:crypto"; -import { chmodSync, writeFileSync } from "node:fs"; -import { getTestHarnessPaytoForLabel, GlobalTestState } from "../harness/harness.js"; -import { solveMFA } from "./test-merchant-self-provision-inactive-account-permissions.js"; +import { + getTestHarnessPaytoForLabel, + GlobalTestState, +} from "../harness/harness.js"; +import { + configureTestMerchantMfa, + doChallenge, + makeMfaConfigEmailSms, + solveMFA, +} from "../harness/tan-helper.js"; /** * Do basic checks on instance management and authentication. @@ -43,44 +47,6 @@ export async function runMerchantSelfProvisionActivationTwoBankAccountsTest( ) { // Set up test environment - // FIXME: maybe merchant can use commands? - const RND = randomBytes(10).toString("hex"); - const socketFile = `${t.testDir}/tan-helper-${RND}.socket`; - const helperScript = `${t.testDir}/harness-helper-${RND}.sh`; - writeFileSync( - helperScript, - `#!/bin/bash -taler-harness run-helper --socket ${socketFile} -- $@ -`, - ); - chmodSync(helperScript, "777"); - - const { - merchant, - merchantAdminAccessToken, - } = await createSimpleTestkudosEnvironmentV3(t, undefined, { - additionalMerchantConfig(m) { - m.modifyConfig(async (cfg) => { - cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); - cfg.setString("merchant", "HELPER_SMS", helperScript); - cfg.setString("merchant", "HELPER_EMAIL", helperScript); - cfg.setString("merchant", "MANDATORY_TAN_CHANNELS", "email sms"); - }); - }, - }); - const helper = await startTanHelper({ socketFile }); - - const merchantClient = new TalerMerchantManagementHttpClient( - merchant.makeInstanceBaseUrl(), - ); - - { - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } - const instanceInfo = { id: "self-instance", name: "My instance", @@ -100,6 +66,34 @@ taler-harness run-helper --socket ${socketFile} -- $@ phone_number: "+1111", use_stefan: false, }; + + const mfaConfig = makeMfaConfigEmailSms( + t, + instanceInfo.email, + instanceInfo.phone_number, + ); + + const { merchant, merchantAdminAccessToken } = + await createSimpleTestkudosEnvironmentV3(t, undefined, { + additionalMerchantConfig(m) { + m.modifyConfig(async (cfg) => { + cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); + configureTestMerchantMfa(cfg, mfaConfig); + }); + }, + }); + + const merchantClient = new TalerMerchantManagementHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + { + const r = succeedOrThrow( + await merchantClient.listInstances(merchantAdminAccessToken), + ); + t.assertDeepEqual(r.instances.length, 2); + } + const signupStart = alternativeOrThrow( await merchantClient.createInstanceSelfProvision(instanceInfo), HttpStatusCode.Accepted, @@ -120,33 +114,21 @@ taler-harness run-helper --socket ${socketFile} -- $@ t.assertDeepEqual(r.instances.length, 2); } - { - succeedOrThrow( - await merchantClient.sendChallenge(firstChallenge.challenge_id), - ); - - const message = helper.getLastCodeForAddress(instanceInfo.phone_number); - const [tanCode] = message.split("\n"); - succeedOrThrow( - await merchantClient.confirmChallenge(firstChallenge.challenge_id, { - tan: tanCode, - }), - ); - } - - { - succeedOrThrow( - await merchantClient.sendChallenge(secondChallenge.challenge_id), - ); + await doChallenge( + t, + merchantClient, + firstChallenge.challenge_id, + instanceInfo.phone_number, + mfaConfig.sms.path, + ); - const message = helper.getLastCodeForAddress(instanceInfo.email); - const [tanCode] = message.split("\n"); - succeedOrThrow( - await merchantClient.confirmChallenge(secondChallenge.challenge_id, { - tan: tanCode, - }), - ); - } + await doChallenge( + t, + merchantClient, + secondChallenge.challenge_id, + instanceInfo.email, + mfaConfig.email.path, + ); const completeSignup = await merchantClient.createInstanceSelfProvision( instanceInfo, @@ -181,10 +163,7 @@ taler-harness run-helper --socket ${socketFile} -- $@ HttpStatusCode.Accepted, ); - await solveMFA(merchantClient, helper, loginChallenge, { - [TanChannel.EMAIL]: instanceInfo.email, - [TanChannel.SMS]: instanceInfo.phone_number, - }); + await solveMFA(t, merchantClient, loginChallenge, mfaConfig); const { access_token: token } = succeedOrThrow( await instanceApi.createAccessToken( @@ -199,28 +178,37 @@ taler-harness run-helper --socket ${socketFile} -- $@ ), ); - const bankAccount = succeedOrThrow(await instanceApi.addBankAccount(token, { - payto_uri: getTestHarnessPaytoForLabel("account1") - })) - - const secondBankAccountChallenge = alternativeOrThrow(await instanceApi.addBankAccount(token, { - payto_uri: getTestHarnessPaytoForLabel("account2") - }), HttpStatusCode.Accepted) - - await solveMFA(instanceApi, helper, secondBankAccountChallenge, { - [TanChannel.EMAIL]: instanceInfo.email, - [TanChannel.SMS]: instanceInfo.phone_number, - }); + const bankAccount = succeedOrThrow( + await instanceApi.addBankAccount(token, { + payto_uri: getTestHarnessPaytoForLabel("account1"), + }), + ); - const secondBankAccount = succeedOrThrow(await instanceApi.addBankAccount(token, { - payto_uri: getTestHarnessPaytoForLabel("account2") - }, { - challengeIds: secondBankAccountChallenge.challenges.map((c) => c.challenge_id) - })) + const secondBankAccountChallenge = alternativeOrThrow( + await instanceApi.addBankAccount(token, { + payto_uri: getTestHarnessPaytoForLabel("account2"), + }), + HttpStatusCode.Accepted, + ); - + await solveMFA(t, instanceApi, secondBankAccountChallenge, mfaConfig); - helper.stop(); + const secondBankAccount = succeedOrThrow( + await instanceApi.addBankAccount( + token, + { + payto_uri: getTestHarnessPaytoForLabel("account2"), + }, + { + challengeIds: secondBankAccountChallenge.challenges.map( + (c) => c.challenge_id, + ), + }, + ), + ); } -runMerchantSelfProvisionActivationTwoBankAccountsTest.suites = ["merchant", "self-provision"]; +runMerchantSelfProvisionActivationTwoBankAccountsTest.suites = [ + "merchant", + "self-provision", +]; diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-activation.ts @@ -21,6 +21,7 @@ import { alternativeOrThrow, Duration, HttpStatusCode, + Logger, LoginTokenScope, MerchantAuthMethod, succeedOrThrow, @@ -29,11 +30,15 @@ import { TanChannel, } from "@gnu-taler/taler-util"; import { createSimpleTestkudosEnvironmentV3 } from "harness/environments.js"; -import { startTanHelper } from "harness/tan-helper.js"; -import { randomBytes } from "node:crypto"; -import { chmodSync, writeFileSync } from "node:fs"; import { GlobalTestState } from "../harness/harness.js"; -import { solveMFA } from "./test-merchant-self-provision-inactive-account-permissions.js"; +import { + configureTestMerchantMfa, + makeMfaConfigEmailSms, + solveMFA, + wait2FaCode, +} from "../harness/tan-helper.js"; + +export const logger = new Logger("test-merchant-self-provision-activation.ts"); /** * Do basic checks on instance management and authentication. @@ -43,48 +48,6 @@ export async function runMerchantSelfProvisionActivationTest( ) { // Set up test environment - // FIXME: maybe merchant can use commands? - const RND = randomBytes(10).toString("hex"); - const socketFile = `${t.testDir}/tan-helper-${RND}.socket`; - const helperScript = `${t.testDir}/harness-helper-${RND}.sh`; - writeFileSync( - helperScript, - `#!/bin/bash -taler-harness run-helper --socket ${socketFile} -- $@ -`, - ); - chmodSync(helperScript, "777"); - - const { - walletClient, - bankClient, - exchange, - merchant, - bank, - merchantAdminAccessToken, - } = await createSimpleTestkudosEnvironmentV3(t, undefined, { - additionalMerchantConfig(m) { - m.modifyConfig(async (cfg) => { - cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); - cfg.setString("merchant", "HELPER_SMS", helperScript); - cfg.setString("merchant", "HELPER_EMAIL", helperScript); - cfg.setString("merchant", "MANDATORY_TAN_CHANNELS", "email sms"); - }); - }, - }); - const helper = await startTanHelper({ socketFile }); - - const merchantClient = new TalerMerchantManagementHttpClient( - merchant.makeInstanceBaseUrl(), - ); - - { - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } - const instanceInfo = { id: "self-instance", name: "My instance", @@ -104,6 +67,34 @@ taler-harness run-helper --socket ${socketFile} -- $@ phone_number: "+1111", use_stefan: false, }; + + const mfaConfig = makeMfaConfigEmailSms( + t, + instanceInfo.email, + instanceInfo.phone_number, + ); + + const { merchant, merchantAdminAccessToken } = + await createSimpleTestkudosEnvironmentV3(t, undefined, { + additionalMerchantConfig(m) { + m.modifyConfig(async (cfg) => { + cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); + configureTestMerchantMfa(cfg, mfaConfig); + }); + }, + }); + + const merchantClient = new TalerMerchantManagementHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + { + const r = succeedOrThrow( + await merchantClient.listInstances(merchantAdminAccessToken), + ); + t.assertDeepEqual(r.instances.length, 2); + } + const signupStart = alternativeOrThrow( await merchantClient.createInstanceSelfProvision(instanceInfo), HttpStatusCode.Accepted, @@ -116,7 +107,7 @@ taler-harness run-helper --socket ${socketFile} -- $@ const firstChallenge = signupStart.challenges[0]; const secondChallenge = signupStart.challenges[1]; - //FIXME: check the order + // FIXME: check the order // always first emails since is cheaper t.assertTrue(firstChallenge.tan_channel === TanChannel.EMAIL); t.assertTrue(secondChallenge.tan_channel === TanChannel.SMS); @@ -133,12 +124,11 @@ taler-harness run-helper --socket ${socketFile} -- $@ succeedOrThrow( await merchantClient.sendChallenge(firstChallenge.challenge_id), ); - - const message = helper.getLastCodeForAddress(instanceInfo.phone_number); - const [tanCode] = message.split("\n"); + const res = await wait2FaCode(mfaConfig.email.path); + t.assertDeepEqual(res.address, instanceInfo.email); succeedOrThrow( await merchantClient.confirmChallenge(firstChallenge.challenge_id, { - tan: tanCode, + tan: res.code, }), ); } @@ -147,12 +137,11 @@ taler-harness run-helper --socket ${socketFile} -- $@ succeedOrThrow( await merchantClient.sendChallenge(secondChallenge.challenge_id), ); - - const message = helper.getLastCodeForAddress(instanceInfo.email); - const [tanCode] = message.split("\n"); + const res = await wait2FaCode(mfaConfig.sms.path); + t.assertDeepEqual(res.address, instanceInfo.phone_number); succeedOrThrow( await merchantClient.confirmChallenge(secondChallenge.challenge_id, { - tan: tanCode, + tan: res.code, }), ); } @@ -190,10 +179,7 @@ taler-harness run-helper --socket ${socketFile} -- $@ HttpStatusCode.Accepted, ); - await solveMFA(merchantClient, helper, loginChallenge, { - [TanChannel.EMAIL]: instanceInfo.email, - [TanChannel.SMS]: instanceInfo.phone_number, - }); + await solveMFA(t, merchantClient, loginChallenge, mfaConfig); const { access_token: token } = succeedOrThrow( await instanceApi.createAccessToken( @@ -217,8 +203,6 @@ taler-harness run-helper --socket ${socketFile} -- $@ t.assertDeepEqual(det.email_validated, true); t.assertDeepEqual(det.phone_number, instanceInfo.phone_number); t.assertDeepEqual(det.phone_validated, true); - - helper.stop(); } runMerchantSelfProvisionActivationTest.suites = ["merchant", "self-provision"]; diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-forgot-password.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-forgot-password.ts @@ -26,14 +26,14 @@ import { succeedOrThrow, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient, - TanChannel, } from "@gnu-taler/taler-util"; import { createSimpleTestkudosEnvironmentV3 } from "harness/environments.js"; -import { startTanHelper } from "harness/tan-helper.js"; -import { randomBytes } from "node:crypto"; -import { chmodSync, writeFileSync } from "node:fs"; +import { + configureTestMerchantMfa, + makeMfaConfigEmailOnly, + solveMFA, +} from "harness/tan-helper.js"; import { GlobalTestState } from "../harness/harness.js"; -import { solveMFA } from "./test-merchant-self-provision-inactive-account-permissions.js"; /** * The merchant should get the TAN code on request to be used to activate the account. @@ -43,47 +43,6 @@ export async function runMerchantSelfProvisionForgotPasswordTest( ) { // Set up test environment - // FIXME: maybe merchant can use commands? - const RND = randomBytes(10).toString("hex"); - const socketFile = `${t.testDir}/tan-helper-${RND}.socket`; - const helperScript = `${t.testDir}/harness-helper-${RND}.sh`; - writeFileSync( - helperScript, - `#!/bin/bash -taler-harness run-helper --socket ${socketFile} -- $@ -`, - ); - chmodSync(helperScript, "777"); - - const { - walletClient, - bankClient, - exchange, - merchant, - merchantAdminAccessToken, - bank, - } = await createSimpleTestkudosEnvironmentV3(t, undefined, { - additionalMerchantConfig(m) { - m.modifyConfig(async (cfg) => { - cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); - cfg.setString("merchant", "HELPER_EMAIL", helperScript); - cfg.setString("merchant", "MANDATORY_TAN_CHANNELS", "email"); - }); - }, - }); - const helper = await startTanHelper({ socketFile }); - - const merchantClient = new TalerMerchantManagementHttpClient( - merchant.makeInstanceBaseUrl(), - ); - - { - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } - const instanceInfo = { id: "self-instance", name: "My instance", @@ -103,14 +62,35 @@ taler-harness run-helper --socket ${socketFile} -- $@ use_stefan: false, }; + const mfaConfig = makeMfaConfigEmailOnly(t, instanceInfo.email); + + const { merchant, merchantAdminAccessToken } = + await createSimpleTestkudosEnvironmentV3(t, undefined, { + additionalMerchantConfig(m) { + m.modifyConfig(async (cfg) => { + cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); + configureTestMerchantMfa(cfg, mfaConfig); + }); + }, + }); + + const merchantClient = new TalerMerchantManagementHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + { + const r = succeedOrThrow( + await merchantClient.listInstances(merchantAdminAccessToken), + ); + t.assertDeepEqual(r.instances.length, 2); + } + const tryCreation = alternativeOrThrow( await merchantClient.createInstanceSelfProvision(instanceInfo), HttpStatusCode.Accepted, ); - await solveMFA(merchantClient, helper, tryCreation, { - [TanChannel.EMAIL]: instanceInfo.email, - }); + await solveMFA(t, merchantClient, tryCreation, mfaConfig); succeedOrThrow( await merchantClient.createInstanceSelfProvision(instanceInfo, { @@ -133,9 +113,7 @@ taler-harness run-helper --socket ${socketFile} -- $@ HttpStatusCode.Accepted, ); - await solveMFA(instanceApi, helper, mfa, { - [TanChannel.EMAIL]: instanceInfo.email, - }); + await solveMFA(t, instanceApi, mfa, mfaConfig); succeedOrThrow( await instanceApi.forgotPasswordSelfProvision(newPassword, { @@ -150,9 +128,7 @@ taler-harness run-helper --socket ${socketFile} -- $@ HttpStatusCode.Accepted, ); - await solveMFA(instanceApi, helper, mfa2, { - [TanChannel.EMAIL]: instanceInfo.email, - }); + await solveMFA(t, instanceApi, mfa2, mfaConfig); const tk = succeedOrThrow( await instanceApi.createAccessToken( @@ -166,8 +142,6 @@ taler-harness run-helper --socket ${socketFile} -- $@ }, ), ); - - helper.stop(); } runMerchantSelfProvisionForgotPasswordTest.suites = [ diff --git a/packages/taler-harness/src/integrationtests/test-merchant-self-provision-inactive-account-permissions.ts b/packages/taler-harness/src/integrationtests/test-merchant-self-provision-inactive-account-permissions.ts @@ -19,7 +19,6 @@ */ import { alternativeOrThrow, - ChallengeResponse, Duration, HttpStatusCode, LoginTokenScope, @@ -27,12 +26,14 @@ import { succeedOrThrow, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient, - TanChannel, } from "@gnu-taler/taler-util"; import { createSimpleTestkudosEnvironmentV3 } from "harness/environments.js"; -import { startTanHelper, TestTanHelperService } from "harness/tan-helper.js"; -import { randomBytes } from "node:crypto"; -import { chmodSync, writeFileSync } from "node:fs"; +import { + configureTestMerchantMfa, + makeMfaConfigEmailOnly, + solveMFA, + wait2FaCode, +} from "harness/tan-helper.js"; import { GlobalTestState } from "../harness/harness.js"; /** @@ -44,47 +45,6 @@ export async function runMerchantSelfProvisionInactiveAccountPermissionsTest( ) { // Set up test environment - // FIXME: maybe merchant can use commands? - const RND = randomBytes(10).toString("hex"); - const socketFile = `${t.testDir}/tan-helper-${RND}.socket`; - const helperScript = `${t.testDir}/harness-helper-${RND}.sh`; - writeFileSync( - helperScript, - `#!/bin/bash - taler-harness run-helper --socket ${socketFile} -- $@ - `, - ); - chmodSync(helperScript, "777"); - - const { - walletClient, - bankClient, - exchange, - merchant, - bank, - merchantAdminAccessToken, - } = await createSimpleTestkudosEnvironmentV3(t, undefined, { - additionalMerchantConfig(m) { - m.modifyConfig(async (cfg) => { - cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); - cfg.setString("merchant", "HELPER_SMS", helperScript); - cfg.setString("merchant", "HELPER_EMAIL", helperScript); - cfg.setString("merchant", "MANDATORY_TAN_CHANNELS", "email"); - }); - }, - }); - const helper = await startTanHelper({ socketFile }); - - const merchantClient = new TalerMerchantManagementHttpClient( - merchant.makeInstanceBaseUrl(), - ); - - { - const r = succeedOrThrow( - await merchantClient.listInstances(merchantAdminAccessToken), - ); - t.assertDeepEqual(r.instances.length, 2); - } const instanceInfo = { id: "self-instance", name: "My instance", @@ -104,6 +64,29 @@ export async function runMerchantSelfProvisionInactiveAccountPermissionsTest( use_stefan: false, }; + const mfaConfig = makeMfaConfigEmailOnly(t, instanceInfo.email); + + const { merchant, merchantAdminAccessToken } = + await createSimpleTestkudosEnvironmentV3(t, undefined, { + additionalMerchantConfig(m) { + m.modifyConfig(async (cfg) => { + cfg.setString("merchant", "ENABLE_SELF_PROVISIONING", "yes"); + configureTestMerchantMfa(cfg, mfaConfig); + }); + }, + }); + + const merchantClient = new TalerMerchantManagementHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + { + const r = succeedOrThrow( + await merchantClient.listInstances(merchantAdminAccessToken), + ); + t.assertDeepEqual(r.instances.length, 2); + } + const signupStart = alternativeOrThrow( await merchantClient.createInstanceSelfProvision(instanceInfo), HttpStatusCode.Accepted, @@ -130,7 +113,8 @@ export async function runMerchantSelfProvisionInactiveAccountPermissionsTest( succeedOrThrow(await merchantClient.sendChallenge(firstChallenge)); - const [tan] = helper.getLastCodeForAddress(instanceInfo.email).split("\n"); + const res = await wait2FaCode(mfaConfig.email.path); + const tan = res.code; t.assertTrue(tan.length > 0, "the mechant didn't send the code to the email"); @@ -148,9 +132,7 @@ export async function runMerchantSelfProvisionInactiveAccountPermissionsTest( t.assertDeepEqual(emailChange.challenges.length, 1); t.assertDeepEqual(emailChange.combi_and, true); - await solveMFA(merchantClient, helper, emailChange, { - [TanChannel.EMAIL]: instanceInfo.email, - }); + await solveMFA(t, merchantClient, emailChange, mfaConfig); const wrongInfo = { ...instanceInfo, @@ -186,9 +168,7 @@ export async function runMerchantSelfProvisionInactiveAccountPermissionsTest( HttpStatusCode.Accepted, ); - await solveMFA(merchantClient, helper, loginChallenge, { - [TanChannel.EMAIL]: instanceInfo.email, - }); + await solveMFA(t, merchantClient, loginChallenge, mfaConfig); const { access_token: token } = succeedOrThrow( await instanceApi.createAccessToken( @@ -210,34 +190,9 @@ export async function runMerchantSelfProvisionInactiveAccountPermissionsTest( // check that the instance has the new email t.assertDeepEqual(det.email, instanceInfo.email); t.assertDeepEqual(det.email_validated, true); - helper.stop(); } runMerchantSelfProvisionInactiveAccountPermissionsTest.suites = [ "merchant", "self-provision", ]; - -export async function solveMFA( - api: TalerMerchantInstanceHttpClient, - helper: TestTanHelperService, - resp: ChallengeResponse, - addrs: Partial<Record<TanChannel, string>>, -) { - for (const { - challenge_id: challenge, - tan_channel: type, - } of resp.challenges) { - const addrUsed = addrs[type]; - if (!addrUsed) { - throw Error( - `The MFA is asking unexpected channel: ${type} id ${challenge}`, - ); - } - succeedOrThrow(await api.sendChallenge(challenge)); - - const [tan] = helper.getLastCodeForAddress(addrUsed).split("\n"); - - succeedOrThrow(await api.confirmChallenge(challenge, { tan })); - } -}