/* 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 */ /** * Helpers to create typical test environments. * * @author Florian Dold */ /** * Imports */ import { AmountString, ConfirmPayResultType, MerchantContractTerms, Duration, PreparePayResultType, NotificationType, WalletNotification, TransactionMajorState, Logger, MerchantApiClient, } from "@gnu-taler/taler-util"; import { BankAccessApiClient, HarnessExchangeBankAccount, WalletApiOperation, } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "./denomStructures.js"; import { FaultInjectedExchangeService, FaultInjectedMerchantService, } from "./faultInjection.js"; import { BankService, DbInfo, ExchangeService, ExchangeServiceInterface, FakebankService, getPayto, GlobalTestState, MerchantService, MerchantServiceInterface, setupDb, setupSharedDb, WalletCli, WalletClient, WalletService, WithAuthorization, } from "./harness.js"; import * as fs from "fs"; const logger = new Logger("helpers.ts"); /** * @deprecated */ export interface SimpleTestEnvironment { commonDb: DbInfo; bank: BankService; exchange: ExchangeService; exchangeBankAccount: HarnessExchangeBankAccount; merchant: MerchantService; wallet: WalletCli; } /** * Improved version of the simple test environment, * with the daemonized wallet. */ export interface SimpleTestEnvironmentNg { commonDb: DbInfo; bank: BankService; exchange: ExchangeService; exchangeBankAccount: HarnessExchangeBankAccount; merchant: MerchantService; walletClient: WalletClient; walletService: WalletService; } export interface EnvOptions { /** * If provided, enable age restrictions with the specified age mask string. */ ageMaskSpec?: string; mixedAgeRestriction?: boolean; additionalExchangeConfig?(e: ExchangeService): void; additionalMerchantConfig?(m: MerchantService): void; additionalBankConfig?(b: BankService): void; } export function getSharedTestDir(): string { return `/tmp/taler-harness@${process.env.USER}`; } export async function useSharedTestkudosEnvironment(t: GlobalTestState) { const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); const sharedDir = getSharedTestDir(); fs.mkdirSync(sharedDir, { recursive: true }); const db = await setupSharedDb(t); let bank: FakebankService; const prevSetupDone = fs.existsSync(sharedDir + "/setup-done"); logger.info(`previous setup done: ${prevSetupDone}`); // Wallet has longer startup-time and no dependencies, // so we start it rather early. const walletStartProm = createWalletDaemonWithClient(t, { name: "wallet" }); if (fs.existsSync(sharedDir + "/bank.conf")) { logger.info("reusing existing bank"); bank = BankService.fromExistingConfig(t, { overridePath: sharedDir, }); } else { logger.info("creating new bank config"); bank = await BankService.create(t, { allowRegistrations: true, currency: "TESTKUDOS", database: db.connStr, httpPort: 8082, overrideTestDir: sharedDir, }); } logger.info("setting up exchange"); const exchangeName = "testexchange-1"; const exchangeConfigFilename = sharedDir + `/exchange-${exchangeName}.conf`; logger.info(`exchange config filename: ${exchangeConfigFilename}`); let exchange: ExchangeService; if (fs.existsSync(exchangeConfigFilename)) { logger.info("reusing existing exchange config"); exchange = ExchangeService.fromExistingConfig(t, exchangeName, { overridePath: sharedDir, }); } else { logger.info("creating new exchange config"); exchange = ExchangeService.create(t, { name: "testexchange-1", currency: "TESTKUDOS", httpPort: 8081, database: db.connStr, overrideTestDir: sharedDir, }); } logger.info("setting up merchant"); let merchant: MerchantService; const merchantName = "testmerchant-1"; const merchantConfigFilename = sharedDir + `/merchant-${merchantName}}`; if (fs.existsSync(merchantConfigFilename)) { merchant = MerchantService.fromExistingConfig(t, merchantName, { overridePath: sharedDir, }); } else { merchant = await MerchantService.create(t, { name: "testmerchant-1", currency: "TESTKUDOS", httpPort: 8083, database: db.connStr, overrideTestDir: sharedDir, }); } logger.info("creating bank account for exchange"); const exchangeBankAccount = await bank.createExchangeAccount( "myexchange", "x", ); logger.info("creating exchange bank account"); await exchange.addBankAccount("1", exchangeBankAccount); bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); exchange.addCoinConfigList(coinConfig); merchant.addExchange(exchange); logger.info("basic setup done, starting services"); if (!prevSetupDone) { // Must be done sequentially, due to a concurrency // issue in the *-dbinit tools. await exchange.dbinit(); await merchant.dbinit(); } const bankStart = async () => { await bank.start(); await bank.pingUntilAvailable(); }; const exchangeStart = async () => { await exchange.start({ skipDbinit: true, skipKeyup: prevSetupDone, }); await exchange.pingUntilAvailable(); }; const merchStart = async () => { await merchant.start({ skipDbinit: true, }); await merchant.pingUntilAvailable(); if (!prevSetupDone) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), }); } }; await bankStart(); const res = await Promise.all([ exchangeStart(), merchStart(), undefined, walletStartProm, ]); const walletClient = res[3].walletClient; const walletService = res[3].walletService; fs.writeFileSync(sharedDir + "/setup-done", "OK"); logger.info("setup done!"); return { commonDb: db, exchange, merchant, walletClient, walletService, bank, exchangeBankAccount, }; } /** * 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); if (opts.additionalBankConfig) { opts.additionalBankConfig(bank); } 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); } if (opts.additionalExchangeConfig) { opts.additionalExchangeConfig(exchange); } await exchange.start(); await exchange.pingUntilAvailable(); merchant.addExchange(exchange); if (opts.additionalMerchantConfig) { opts.additionalMerchantConfig(merchant); } await merchant.start(); await merchant.pingUntilAvailable(); await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), }); const { walletClient, walletService } = await createWalletDaemonWithClient( t, { name: "wallet" }, ); console.log("setup done!"); return { commonDb: db, exchange, merchant, walletClient, walletService, bank, exchangeBankAccount, }; } export interface CreateWalletArgs { handleNotification?(wn: WalletNotification): void; name: string; persistent?: boolean; } export async function createWalletDaemonWithClient( t: GlobalTestState, args: CreateWalletArgs, ): Promise<{ walletClient: WalletClient; walletService: WalletService }> { const walletService = new WalletService(t, { name: args.name, useInMemoryDb: !args.persistent, }); await walletService.start(); await walletService.pingUntilAvailable(); const walletClient = new WalletClient({ unixPath: walletService.socketPath, onNotification(n) { console.log(`got ${args.name} notification`, n); if (args.handleNotification) { args.handleNotification(n); } }, }); await walletClient.connect(); await walletClient.client.call(WalletApiOperation.InitWallet, { skipDefaults: true, }); return { walletClient, walletService }; } export interface FaultyMerchantTestEnvironment { commonDb: DbInfo; bank: BankService; exchange: ExchangeService; faultyExchange: FaultInjectedExchangeService; exchangeBankAccount: HarnessExchangeBankAccount; merchant: MerchantService; faultyMerchant: FaultInjectedMerchantService; walletClient: WalletClient; } /** * Run a test case with a simple TESTKUDOS Taler environment, consisting * of one exchange, one bank and one merchant. */ export async function createFaultInjectedMerchantTestkudosEnvironment( t: GlobalTestState, ): 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 faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083); const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081); // Base URL must contain port that the proxy is listening on. await exchange.modifyConfig(async (config) => { config.setString("exchange", "base_url", "http://localhost:9081/"); }); const exchangeBankAccount = await bank.createExchangeAccount( "myexchange", "x", ); exchange.addBankAccount("1", exchangeBankAccount); bank.setSuggestedExchange( faultyExchange, exchangeBankAccount.accountPaytoUri, ); await bank.start(); await bank.pingUntilAvailable(); exchange.addOfferedCoins(defaultCoinConfig); await exchange.start(); await exchange.pingUntilAvailable(); merchant.addExchange(faultyExchange); await merchant.start(); await merchant.pingUntilAvailable(); await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", paytoUris: [getPayto("merchant-default")], }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", paytoUris: [getPayto("minst1")], }); console.log("setup done!"); const { walletClient } = await createWalletDaemonWithClient(t, { name: "default", }); return { commonDb: db, exchange, merchant, walletClient, bank, exchangeBankAccount, faultyMerchant, faultyExchange, }; } export interface WithdrawViaBankResult { withdrawalFinishedCond: Promise; } /** * Withdraw via a bank with the testing API enabled. * Uses the new notification-based mechanism to wait for the * operation to finish. */ export async function withdrawViaBankV2( t: GlobalTestState, p: { walletClient: WalletClient; bank: BankService; exchange: ExchangeServiceInterface; amount: AmountString; restrictAge?: number; }, ): Promise { const { walletClient: wallet, bank, exchange, amount } = p; const bankClient = new BankAccessApiClient(bank.bankAccessApiBaseUrl); const user = await bankClient.createRandomBankUser(); const wop = await bankClient.createWithdrawalOperation(user.username, amount); // Hand it to the wallet await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { talerWithdrawUri: wop.taler_withdraw_uri, restrictAge: p.restrictAge, }); // Withdraw (AKA select) const acceptRes = await wallet.client.call( WalletApiOperation.AcceptBankIntegratedWithdrawal, { exchangeBaseUrl: exchange.baseUrl, talerWithdrawUri: wop.taler_withdraw_uri, restrictAge: p.restrictAge, }, ); const withdrawalFinishedCond = wallet.waitForNotificationCond( (x) => x.type === NotificationType.TransactionStateTransition && x.newTxState.major === TransactionMajorState.Done && x.transactionId === acceptRes.transactionId, ); // Confirm it await bankClient.confirmWithdrawalOperation(user.username, wop); return { withdrawalFinishedCond, }; } export async function applyTimeTravelV2( timetravelOffsetMs: number, s: { exchange?: ExchangeService; merchant?: MerchantService; walletClient?: WalletClient; }, ): Promise { if (s.exchange) { await s.exchange.stop(); s.exchange.setTimetravel(timetravelOffsetMs); await s.exchange.start(); await s.exchange.pingUntilAvailable(); } if (s.merchant) { await s.merchant.stop(); s.merchant.setTimetravel(timetravelOffsetMs); await s.merchant.start(); await s.merchant.pingUntilAvailable(); } if (s.walletClient) { await s.walletClient.call(WalletApiOperation.TestingSetTimetravel, { offsetMs: timetravelOffsetMs, }); } } /** * Make a simple payment and check that it succeeded. */ export async function makeTestPaymentV2( t: GlobalTestState, args: { merchant: MerchantServiceInterface; walletClient: WalletClient; order: Partial; instance?: string; }, auth: WithAuthorization = {}, ): Promise { // Set up order. const { walletClient, merchant, instance } = args; const merchantClient = new MerchantApiClient( merchant.makeInstanceBaseUrl(instance), ); const orderResp = await merchantClient.createOrder({ order: args.order, }); let orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, }); t.assertTrue(orderStatus.order_status === "unpaid"); // Make wallet pay for the order const preparePayResult = await walletClient.call( WalletApiOperation.PreparePayForUri, { talerPayUri: orderStatus.taler_pay_uri, }, ); t.assertTrue( preparePayResult.status === PreparePayResultType.PaymentPossible, ); const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, { proposalId: preparePayResult.proposalId, }); t.assertTrue(r2.type === ConfirmPayResultType.Done); // Check if payment was successful. orderStatus = await merchantClient.queryPrivateOrderStatus({ orderId: orderResp.order_id, instance, }); t.assertTrue(orderStatus.order_status === "paid"); }