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 ++++++++++++++ .../integrationtests/test-wallet-notifications.ts | 163 +++++++++++++++++++++ .../src/integrationtests/testrunner.ts | 6 +- 4 files changed, 388 insertions(+), 6 deletions(-) create mode 100644 packages/taler-harness/src/integrationtests/test-wallet-notifications.ts (limited to 'packages/taler-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; diff --git a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts new file mode 100644 index 000000000..23c71ea2f --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts @@ -0,0 +1,163 @@ +/* + This file is part of GNU Taler + (C) 2020 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 + */ + +/** + * Imports. + */ +import { + Amounts, + Duration, + NotificationType, + PreparePayResultType, +} from "@gnu-taler/taler-util"; +import { + BankAccessApi, + BankApi, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + ExchangeService, + FakebankService, + getRandomIban, + GlobalTestState, + MerchantService, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; + +/** + * Test for wallet-core notifications. + */ +export async function runWalletNotificationsTest(t: GlobalTestState) { + // Set up test environment + + const db = await setupDb(t); + + const bank = await FakebankService.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", + ); + exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); + exchange.addCoinConfigList(coinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + // Fakebank uses x-taler-bank, but merchant is configured to only accept sepa! + const label = "mymerchant"; + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [ + `payto://iban/SANDBOXX/${getRandomIban(label)}?receiver-name=${label}`, + ], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + console.log("setup done!"); + + const walletService = new WalletService(t, "wallet"); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + skipDefaults: true, + }); + + const user = await BankApi.createRandomBankUser(bank); + const wop = await BankAccessApi.createWithdrawalOperation( + bank, + user, + "TESTKUDOS:20", + ); + + // Hand it to the wallet + + await walletClient.client.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: wop.taler_withdraw_uri, + }, + ); + + // Withdraw (AKA select) + + const withdrawalFinishedReceivedPromise = + walletClient.waitForNotificationCond((x) => { + return x.type === NotificationType.WithdrawGroupFinished; + }); + + await walletClient.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + }, + ); + + // Confirm it + + await BankApi.confirmWithdrawalOperation(bank, user, wop); + + await withdrawalFinishedReceivedPromise; +} + +runWalletNotificationsTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index f04bc2950..3d70e6860 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -92,13 +92,14 @@ import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrat import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js"; import { runTestWithdrawalManualTest } from "./test-withdrawal-manual.js"; import { runAgeRestrictionsPeerTest } from "./test-age-restrictions-peer.js"; -import { runWalletBalanceTest } from "./test-wallet-balance.js"; +import { runWalletNotificationsTest } from "./test-wallet-notifications.js"; import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js"; import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js"; import { runWithdrawalHighTest } from "./test-withdrawal-high.js"; import { runKycTest } from "./test-kyc.js"; import { runPaymentAbortTest } from "./test-payment-abort.js"; import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js"; +import { runWalletBalanceTest } from "./test-wallet-balance.js"; /** * Test runner. @@ -166,6 +167,7 @@ const allTests: TestMainFunction[] = [ runPaymentTransientTest, runPaymentZeroTest, runPayPaidTest, + runWalletBalanceTest, runPaywallFlowTest, runPeerToPeerPullTest, runPeerToPeerPushTest, @@ -180,7 +182,7 @@ const allTests: TestMainFunction[] = [ runTippingTest, runWalletBackupBasicTest, runWalletBackupDoublespendTest, - runWalletBalanceTest, + runWalletNotificationsTest, runWalletCryptoWorkerTest, runWalletDblessTest, runWallettestingTest, -- cgit v1.2.3