From 96101238afb82d200cf9d5005ffc2fc0391f23e4 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 2 Feb 2023 20:20:58 +0100 Subject: harness,wallet-cli: notification-based testing with RPC wallet --- packages/taler-harness/src/harness/harness.ts | 117 +++++++++++++++++++++++++- packages/taler-harness/src/harness/helpers.ts | 108 ++++++++++++++++++++++++ 2 files changed, 221 insertions(+), 4 deletions(-) (limited to 'packages/taler-harness/src/harness') diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 134709541..83c8f60d1 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -21,8 +21,6 @@ * @author Florian Dold */ -const logger = new Logger("harness.ts"); - /** * Imports */ @@ -43,6 +41,7 @@ import { parsePaytoUri, stringToBytes, TalerProtocolDuration, + WalletNotification, } from "@gnu-taler/taler-util"; import { BankAccessApi, @@ -57,9 +56,9 @@ import { import { deepStrictEqual } from "assert"; import axiosImp, { AxiosError } from "axios"; import { ChildProcess, spawn } from "child_process"; -import * as child_process from "child_process"; import * as fs from "fs"; import * as http from "http"; +import * as net from "node:net"; import * as path from "path"; import * as readline from "readline"; import { URL } from "url"; @@ -76,6 +75,15 @@ import { TipCreateRequest, TippingReserveStatus, } from "./merchantApiTypes.js"; +import { + createRemoteWallet, + getClientFromRemoteWallet, + makeNotificationWaiter, + RemoteWallet, + WalletNotificationWaiter, +} from "@gnu-taler/taler-wallet-core/remote"; + +const logger = new Logger("harness.ts"); const axios = axiosImp.default; @@ -1831,7 +1839,7 @@ export async function runTestWithState( const handleSignal = (s: string) => { logger.warn( - `**** received fatal process event, terminating test ${testName}`, + `**** received fatal process event (${s}), terminating test ${testName}`, ); gc.shutdownSync(); process.exit(1); @@ -1885,6 +1893,107 @@ export interface WalletCliOpts { cryptoWorkerType?: "sync" | "node-worker-thread"; } +function tryUnixConnect(socketPath: string): Promise { + return new Promise((resolve, reject) => { + const client = net.createConnection(socketPath); + client.on("error", (e) => { + reject(e); + }); + client.on("connect", () => { + client.end(); + resolve(); + }); + }); +} + +export class WalletService { + walletProc: ProcessWrapper | undefined; + + constructor(private globalState: GlobalTestState, private name: string) {} + + get socketPath() { + const unixPath = path.join(this.globalState.testDir, `${this.name}.sock`); + return unixPath; + } + + async start(): Promise { + const dbPath = path.join( + this.globalState.testDir, + `walletdb-${this.name}.json`, + ); + const unixPath = this.socketPath; + this.globalState.spawnService( + "taler-wallet-cli", + [ + "--wallet-db", + dbPath, + "advanced", + "serve", + "--unix-path", + unixPath, + ], + `wallet-${this.name}`, + ); + } + + async pingUntilAvailable(): Promise { + while (1) { + try { + await tryUnixConnect(this.socketPath); + } catch (e) { + logger.info(`connection attempt failed: ${e}`); + await delayMs(200); + continue; + } + logger.info("connection to wallet-core succeeded"); + break; + } + } +} + +export interface WalletClientArgs { + unixPath: string; + onNotification?(n: WalletNotification): void; +} + +export class WalletClient { + remoteWallet: RemoteWallet | undefined = undefined; + waiter: WalletNotificationWaiter = makeNotificationWaiter(); + + constructor(private args: WalletClientArgs) {} + + async connect(): Promise { + const waiter = this.waiter; + const walletClient = this; + const w = await createRemoteWallet({ + socketFilename: this.args.unixPath, + notificationHandler(n) { + if (walletClient.args.onNotification) { + walletClient.args.onNotification(n); + } + waiter.notify(n); + console.log("got notification from wallet-core in WalletClient"); + }, + }); + this.remoteWallet = w; + + this.waiter.waitForNotificationCond; + } + + get client() { + if (!this.remoteWallet) { + throw Error("wallet not connected"); + } + return getClientFromRemoteWallet(this.remoteWallet); + } + + waitForNotificationCond( + cond: (n: WalletNotification) => boolean, + ): Promise { + return this.waiter.waitForNotificationCond(cond); + } +} + export class WalletCli { private currentTimetravel: Duration | undefined; private _client: WalletCoreApiClient; diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts index 96b34f9d9..59a37e4b8 100644 --- a/packages/taler-harness/src/harness/helpers.ts +++ b/packages/taler-harness/src/harness/helpers.ts @@ -180,6 +180,114 @@ export async function createSimpleTestkudosEnvironment( }; } +/** + * Run a test case with a simple TESTKUDOS Taler environment, consisting + * of one exchange, one bank and one merchant. + * + * V2 uses a daemonized wallet instead of the CLI wallet. + */ +export async function createSimpleTestkudosEnvironmentV2( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + const exchangeBankAccount = await bank.createExchangeAccount( + "myexchange", + "x", + ); + await exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [getPayto("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: [getPayto("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + return { + commonDb: db, + exchange, + merchant, + wallet, + bank, + exchangeBankAccount, + }; +} + export interface FaultyMerchantTestEnvironment { commonDb: DbInfo; bank: BankService; -- cgit v1.2.3