From 589c2a338284e038cf03e4c8734671c8f9f8ebda Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 20 Oct 2021 13:06:31 +0200 Subject: wallet-cli: benchmarking --- packages/taler-wallet-cli/src/bench1.ts | 89 + packages/taler-wallet-cli/src/env1.ts | 68 + .../src/harness/denomStructures.ts | 151 ++ .../taler-wallet-cli/src/harness/faultInjection.ts | 256 +++ packages/taler-wallet-cli/src/harness/harness.ts | 1779 ++++++++++++++++++++ packages/taler-wallet-cli/src/harness/helpers.ts | 406 +++++ packages/taler-wallet-cli/src/harness/libeufin.ts | 1676 ++++++++++++++++++ .../src/harness/merchantApiTypes.ts | 318 ++++ packages/taler-wallet-cli/src/harness/sync.ts | 118 ++ packages/taler-wallet-cli/src/index.ts | 33 +- .../src/integrationtests/denomStructures.ts | 151 -- .../src/integrationtests/faultInjection.ts | 256 --- .../src/integrationtests/harness.ts | 1764 ------------------- .../src/integrationtests/helpers.ts | 406 ----- .../src/integrationtests/libeufin.ts | 1676 ------------------ .../src/integrationtests/merchantApiTypes.ts | 318 ---- .../integrationtests/scenario-prompt-payment.ts | 4 +- .../taler-wallet-cli/src/integrationtests/sync.ts | 118 -- .../src/integrationtests/test-bank-api.ts | 4 +- .../src/integrationtests/test-claim-loop.ts | 4 +- .../src/integrationtests/test-denom-unoffered.ts | 4 +- .../src/integrationtests/test-deposit.ts | 4 +- .../integrationtests/test-exchange-management.ts | 6 +- .../integrationtests/test-exchange-timetravel.ts | 6 +- .../src/integrationtests/test-fee-regression.ts | 4 +- .../test-libeufin-api-bankaccount.ts | 4 +- .../test-libeufin-api-bankconnection.ts | 4 +- .../test-libeufin-api-facade-bad-request.ts | 4 +- .../integrationtests/test-libeufin-api-facade.ts | 4 +- .../test-libeufin-api-permissions.ts | 4 +- .../test-libeufin-api-sandbox-camt.ts | 4 +- .../test-libeufin-api-sandbox-transactions.ts | 4 +- .../test-libeufin-api-scheduling.ts | 8 +- .../integrationtests/test-libeufin-api-users.ts | 4 +- .../integrationtests/test-libeufin-bad-gateway.ts | 6 +- .../src/integrationtests/test-libeufin-basic.ts | 8 +- .../src/integrationtests/test-libeufin-c5x.ts | 4 +- .../test-libeufin-facade-anastasis.ts | 4 +- .../integrationtests/test-libeufin-keyrotation.ts | 4 +- .../test-libeufin-nexus-balance.ts | 4 +- .../test-libeufin-refund-multiple-users.ts | 4 +- .../src/integrationtests/test-libeufin-refund.ts | 4 +- .../test-libeufin-sandbox-wire-transfer-cli.ts | 4 +- .../src/integrationtests/test-libeufin-tutorial.ts | 4 +- .../test-merchant-exchange-confusion.ts | 8 +- .../test-merchant-instances-delete.ts | 2 +- .../test-merchant-instances-urls.ts | 2 +- .../integrationtests/test-merchant-instances.ts | 2 +- .../integrationtests/test-merchant-longpolling.ts | 4 +- .../integrationtests/test-merchant-refund-api.ts | 4 +- .../test-merchant-spec-public-orders.ts | 4 +- .../src/integrationtests/test-pay-abort.ts | 6 +- .../src/integrationtests/test-pay-paid.ts | 6 +- .../src/integrationtests/test-payment-claim.ts | 4 +- .../src/integrationtests/test-payment-fault.ts | 6 +- .../integrationtests/test-payment-forgettable.ts | 4 +- .../integrationtests/test-payment-idempotency.ts | 4 +- .../src/integrationtests/test-payment-multiple.ts | 6 +- .../src/integrationtests/test-payment-on-demo.ts | 4 +- .../src/integrationtests/test-payment-transient.ts | 6 +- .../src/integrationtests/test-payment-zero.ts | 4 +- .../src/integrationtests/test-payment.ts | 4 +- .../src/integrationtests/test-paywall-flow.ts | 4 +- .../src/integrationtests/test-refund-auto.ts | 4 +- .../src/integrationtests/test-refund-gone.ts | 4 +- .../integrationtests/test-refund-incremental.ts | 4 +- .../src/integrationtests/test-refund.ts | 4 +- .../src/integrationtests/test-revocation.ts | 6 +- .../test-timetravel-autorefresh.ts | 6 +- .../integrationtests/test-timetravel-withdraw.ts | 4 +- .../src/integrationtests/test-tipping.ts | 4 +- .../integrationtests/test-wallet-backup-basic.ts | 6 +- .../test-wallet-backup-doublespend.ts | 6 +- .../src/integrationtests/test-wallettesting.ts | 6 +- .../integrationtests/test-withdrawal-abort-bank.ts | 4 +- .../test-withdrawal-bank-integrated.ts | 4 +- .../integrationtests/test-withdrawal-fakebank.ts | 6 +- .../src/integrationtests/test-withdrawal-manual.ts | 4 +- .../src/integrationtests/testrunner.ts | 2 +- packages/taler-wallet-cli/src/lint.ts | 2 +- packages/taler-wallet-core/src/wallet-api-types.ts | 4 + 81 files changed, 5038 insertions(+), 4831 deletions(-) create mode 100644 packages/taler-wallet-cli/src/bench1.ts create mode 100644 packages/taler-wallet-cli/src/env1.ts create mode 100644 packages/taler-wallet-cli/src/harness/denomStructures.ts create mode 100644 packages/taler-wallet-cli/src/harness/faultInjection.ts create mode 100644 packages/taler-wallet-cli/src/harness/harness.ts create mode 100644 packages/taler-wallet-cli/src/harness/helpers.ts create mode 100644 packages/taler-wallet-cli/src/harness/libeufin.ts create mode 100644 packages/taler-wallet-cli/src/harness/merchantApiTypes.ts create mode 100644 packages/taler-wallet-cli/src/harness/sync.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/denomStructures.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/faultInjection.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/harness.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/helpers.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/libeufin.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/sync.ts diff --git a/packages/taler-wallet-cli/src/bench1.ts b/packages/taler-wallet-cli/src/bench1.ts new file mode 100644 index 000000000..5563fc453 --- /dev/null +++ b/packages/taler-wallet-cli/src/bench1.ts @@ -0,0 +1,89 @@ +/* + This file is part of GNU Taler + (C) 2021 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 { buildCodecForObject, codecForString } from "@gnu-taler/taler-util"; +import { + getDefaultNodeWallet, + NodeHttpLib, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; + +/** + * Entry point for the benchmark. + * + * The benchmark runs against an existing Taler deployment and does not + * set up its own services. + */ +export async function runBench1(configJson: any): Promise { + // Validate the configuration file for this benchmark. + const b1conf = codecForBench1Config().decode(configJson); + + const myHttpLib = new NodeHttpLib(); + const wallet = await getDefaultNodeWallet({ + // No persistent DB storage. + persistentStoragePath: undefined, + httpLib: myHttpLib, + }); + await wallet.client.call(WalletApiOperation.InitWallet, {}); + + await wallet.client.call(WalletApiOperation.WithdrawFakebank, { + amount: "TESTKUDOS:10", + bank: b1conf.bank, + exchange: b1conf.exchange, + }); + + await wallet.runTaskLoop({ + stopWhenDone: true, + }); + + await wallet.client.call(WalletApiOperation.CreateDepositGroup, { + amount: "TESTKUDOS:5", + depositPaytoUri: "payto://x-taler-bank/localhost/foo", + }); + + await wallet.runTaskLoop({ + stopWhenDone: true, + }); + + wallet.stop(); +} + +/** + * Format of the configuration file passed to the benchmark + */ +interface Bench1Config { + /** + * Base URL of the bank. + */ + bank: string; + + /** + * Base URL of the exchange. + */ + exchange: string; +} + +/** + * Schema validation codec for Bench1Config. + */ +const codecForBench1Config = () => + buildCodecForObject() + .property("bank", codecForString()) + .property("exchange", codecForString()) + .build("Bench1Config"); diff --git a/packages/taler-wallet-cli/src/env1.ts b/packages/taler-wallet-cli/src/env1.ts new file mode 100644 index 000000000..eb7da352c --- /dev/null +++ b/packages/taler-wallet-cli/src/env1.ts @@ -0,0 +1,68 @@ +/* + This file is part of GNU Taler + (C) 2021 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 { URL } from "@gnu-taler/taler-util"; +import { CoinConfig, defaultCoinConfig } from "./harness/denomStructures.js"; +import { + GlobalTestState, + setupDb, + FakeBankService, + ExchangeService, +} from "./harness/harness.js"; + +/** + * Entry point for the benchmark. + * + * The benchmark runs against an existing Taler deployment and does not + * set up its own services. + */ +export async function runEnv1(t: GlobalTestState): Promise { + const db = await setupDb(t); + + const bank = await FakeBankService.create(t, { + currency: "TESTKUDOS", + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + exchange.addBankAccount("1", { + accountName: "exchange", + accountPassword: "x", + wireGatewayApiBaseUrl: new URL("/exchange/", bank.baseUrl).href, + accountPaytoUri: "payto://x-taler-bank/localhost/exchange", + }); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); + exchange.addCoinConfigList(coinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + console.log("setup done!"); +} diff --git a/packages/taler-wallet-cli/src/harness/denomStructures.ts b/packages/taler-wallet-cli/src/harness/denomStructures.ts new file mode 100644 index 000000000..5ab9aca00 --- /dev/null +++ b/packages/taler-wallet-cli/src/harness/denomStructures.ts @@ -0,0 +1,151 @@ +/* + 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 + */ + +export interface CoinConfig { + name: string; + value: string; + durationWithdraw: string; + durationSpend: string; + durationLegal: string; + feeWithdraw: string; + feeDeposit: string; + feeRefresh: string; + feeRefund: string; + rsaKeySize: number; +} + +const coinCommon = { + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + rsaKeySize: 1024, +}; + +export const coin_ct1 = (curr: string): CoinConfig => ({ + ...coinCommon, + name: `${curr}_ct1`, + value: `${curr}:0.01`, + feeDeposit: `${curr}:0.00`, + feeRefresh: `${curr}:0.01`, + feeRefund: `${curr}:0.00`, + feeWithdraw: `${curr}:0.01`, +}); + +export const coin_ct10 = (curr: string): CoinConfig => ({ + ...coinCommon, + name: `${curr}_ct10`, + value: `${curr}:0.10`, + feeDeposit: `${curr}:0.01`, + feeRefresh: `${curr}:0.01`, + feeRefund: `${curr}:0.00`, + feeWithdraw: `${curr}:0.01`, +}); + +export const coin_u1 = (curr: string): CoinConfig => ({ + ...coinCommon, + name: `${curr}_u1`, + value: `${curr}:1`, + feeDeposit: `${curr}:0.02`, + feeRefresh: `${curr}:0.02`, + feeRefund: `${curr}:0.02`, + feeWithdraw: `${curr}:0.02`, +}); + +export const coin_u2 = (curr: string): CoinConfig => ({ + ...coinCommon, + name: `${curr}_u2`, + value: `${curr}:2`, + feeDeposit: `${curr}:0.02`, + feeRefresh: `${curr}:0.02`, + feeRefund: `${curr}:0.02`, + feeWithdraw: `${curr}:0.02`, +}); + +export const coin_u4 = (curr: string): CoinConfig => ({ + ...coinCommon, + name: `${curr}_u4`, + value: `${curr}:4`, + feeDeposit: `${curr}:0.02`, + feeRefresh: `${curr}:0.02`, + feeRefund: `${curr}:0.02`, + feeWithdraw: `${curr}:0.02`, +}); + +export const coin_u8 = (curr: string): CoinConfig => ({ + ...coinCommon, + name: `${curr}_u8`, + value: `${curr}:8`, + feeDeposit: `${curr}:0.16`, + feeRefresh: `${curr}:0.16`, + feeRefund: `${curr}:0.16`, + feeWithdraw: `${curr}:0.16`, +}); + +const coin_u10 = (curr: string): CoinConfig => ({ + ...coinCommon, + name: `${curr}_u10`, + value: `${curr}:10`, + feeDeposit: `${curr}:0.2`, + feeRefresh: `${curr}:0.2`, + feeRefund: `${curr}:0.2`, + feeWithdraw: `${curr}:0.2`, +}); + +export const defaultCoinConfig = [ + coin_ct1, + coin_ct10, + coin_u1, + coin_u2, + coin_u4, + coin_u8, + coin_u10, +]; + +const coinCheapCommon = (curr: string) => ({ + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + rsaKeySize: 1024, + feeRefresh: `${curr}:0.2`, + feeRefund: `${curr}:0.2`, + feeWithdraw: `${curr}:0.2`, +}); + +export function makeNoFeeCoinConfig(curr: string): CoinConfig[] { + const cc: CoinConfig[] = []; + + for (let i = 0; i < 16; i++) { + const ct = 2 ** i; + + const unit = Math.floor(ct / 100); + const cent = ct % 100; + + cc.push({ + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + rsaKeySize: 1024, + name: `${curr}-u${i}`, + feeDeposit: `${curr}:0`, + feeRefresh: `${curr}:0`, + feeRefund: `${curr}:0`, + feeWithdraw: `${curr}:0`, + value: `${curr}:${unit}.${cent}`, + }); + } + + return cc; +} diff --git a/packages/taler-wallet-cli/src/harness/faultInjection.ts b/packages/taler-wallet-cli/src/harness/faultInjection.ts new file mode 100644 index 000000000..4c3d0c123 --- /dev/null +++ b/packages/taler-wallet-cli/src/harness/faultInjection.ts @@ -0,0 +1,256 @@ +/* + 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 + */ + +/** + * Fault injection proxy. + * + * @author Florian Dold + */ + +/** + * Imports + */ +import * as http from "http"; +import { URL } from "url"; +import { + GlobalTestState, + ExchangeService, + ExchangeServiceInterface, + MerchantServiceInterface, + MerchantService, +} from "../harness/harness.js"; + +export interface FaultProxyConfig { + inboundPort: number; + targetPort: number; +} + +/** + * Fault injection context. Modified by fault injection functions. + */ +export interface FaultInjectionRequestContext { + requestUrl: string; + method: string; + requestHeaders: Record; + requestBody?: Buffer; + dropRequest: boolean; +} + +export interface FaultInjectionResponseContext { + request: FaultInjectionRequestContext; + statusCode: number; + responseHeaders: Record; + responseBody: Buffer | undefined; + dropResponse: boolean; +} + +export interface FaultSpec { + modifyRequest?: (ctx: FaultInjectionRequestContext) => Promise; + modifyResponse?: (ctx: FaultInjectionResponseContext) => Promise; +} + +export class FaultProxy { + constructor( + private globalTestState: GlobalTestState, + private faultProxyConfig: FaultProxyConfig, + ) {} + + private currentFaultSpecs: FaultSpec[] = []; + + start() { + const server = http.createServer((req, res) => { + const requestChunks: Buffer[] = []; + const requestUrl = `http://localhost:${this.faultProxyConfig.inboundPort}${req.url}`; + console.log("request for", new URL(requestUrl)); + req.on("data", (chunk) => { + requestChunks.push(chunk); + }); + req.on("end", async () => { + console.log("end of data"); + let requestBuffer: Buffer | undefined; + if (requestChunks.length > 0) { + requestBuffer = Buffer.concat(requestChunks); + } + console.log("full request body", requestBuffer); + + const faultReqContext: FaultInjectionRequestContext = { + dropRequest: false, + method: req.method!!, + requestHeaders: req.headers, + requestUrl, + requestBody: requestBuffer, + }; + + for (const faultSpec of this.currentFaultSpecs) { + if (faultSpec.modifyRequest) { + await faultSpec.modifyRequest(faultReqContext); + } + } + + if (faultReqContext.dropRequest) { + res.destroy(); + return; + } + + const faultedUrl = new URL(faultReqContext.requestUrl); + + const proxyRequest = http.request({ + method: faultReqContext.method, + host: "localhost", + port: this.faultProxyConfig.targetPort, + path: faultedUrl.pathname + faultedUrl.search, + headers: faultReqContext.requestHeaders, + }); + + console.log( + `proxying request to target path '${ + faultedUrl.pathname + faultedUrl.search + }'`, + ); + + if (faultReqContext.requestBody) { + proxyRequest.write(faultReqContext.requestBody); + } + proxyRequest.end(); + proxyRequest.on("response", (proxyResp) => { + console.log("gotten response from target", proxyResp.statusCode); + const respChunks: Buffer[] = []; + proxyResp.on("data", (proxyRespData) => { + respChunks.push(proxyRespData); + }); + proxyResp.on("end", async () => { + console.log("end of target response"); + let responseBuffer: Buffer | undefined; + if (respChunks.length > 0) { + responseBuffer = Buffer.concat(respChunks); + } + const faultRespContext: FaultInjectionResponseContext = { + request: faultReqContext, + dropResponse: false, + responseBody: responseBuffer, + responseHeaders: proxyResp.headers, + statusCode: proxyResp.statusCode!!, + }; + for (const faultSpec of this.currentFaultSpecs) { + const modResponse = faultSpec.modifyResponse; + if (modResponse) { + await modResponse(faultRespContext); + } + } + if (faultRespContext.dropResponse) { + req.destroy(); + return; + } + if (faultRespContext.responseBody) { + // We must accommodate for potentially changed content length + faultRespContext.responseHeaders[ + "content-length" + ] = `${faultRespContext.responseBody.byteLength}`; + } + console.log("writing response head"); + res.writeHead( + faultRespContext.statusCode, + http.STATUS_CODES[faultRespContext.statusCode], + faultRespContext.responseHeaders, + ); + if (faultRespContext.responseBody) { + res.write(faultRespContext.responseBody); + } + res.end(); + }); + }); + }); + }); + + server.listen(this.faultProxyConfig.inboundPort); + this.globalTestState.servers.push(server); + } + + addFault(f: FaultSpec) { + this.currentFaultSpecs.push(f); + } + + clearAllFaults() { + this.currentFaultSpecs = []; + } +} + +export class FaultInjectedExchangeService implements ExchangeServiceInterface { + baseUrl: string; + port: number; + faultProxy: FaultProxy; + + get name(): string { + return this.innerExchange.name; + } + + get masterPub(): string { + return this.innerExchange.masterPub; + } + + private innerExchange: ExchangeService; + + constructor( + t: GlobalTestState, + e: ExchangeService, + proxyInboundPort: number, + ) { + this.innerExchange = e; + this.faultProxy = new FaultProxy(t, { + inboundPort: proxyInboundPort, + targetPort: e.port, + }); + this.faultProxy.start(); + + const exchangeUrl = new URL(e.baseUrl); + exchangeUrl.port = `${proxyInboundPort}`; + this.baseUrl = exchangeUrl.href; + this.port = proxyInboundPort; + } +} + +export class FaultInjectedMerchantService implements MerchantServiceInterface { + baseUrl: string; + port: number; + faultProxy: FaultProxy; + + get name(): string { + return this.innerMerchant.name; + } + + private innerMerchant: MerchantService; + private inboundPort: number; + + constructor( + t: GlobalTestState, + m: MerchantService, + proxyInboundPort: number, + ) { + this.innerMerchant = m; + this.faultProxy = new FaultProxy(t, { + inboundPort: proxyInboundPort, + targetPort: m.port, + }); + this.faultProxy.start(); + this.inboundPort = proxyInboundPort; + } + + makeInstanceBaseUrl(instanceName?: string | undefined): string { + const url = new URL(this.innerMerchant.makeInstanceBaseUrl(instanceName)); + url.port = `${this.inboundPort}`; + return url.href; + } +} diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts new file mode 100644 index 000000000..b4ac16dbf --- /dev/null +++ b/packages/taler-wallet-cli/src/harness/harness.ts @@ -0,0 +1,1779 @@ +/* + 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 + */ + +/** + * Test harness for various GNU Taler components. + * Also provides a fault-injection proxy. + * + * @author Florian Dold + */ + +/** + * Imports + */ +import * as util from "util"; +import * as fs from "fs"; +import * as path from "path"; +import * as http from "http"; +import * as readline from "readline"; +import { deepStrictEqual } from "assert"; +import { ChildProcess, spawn } from "child_process"; +import { URL } from "url"; +import axios, { AxiosError } from "axios"; +import { + codecForMerchantOrderPrivateStatusResponse, + codecForPostOrderResponse, + PostOrderRequest, + PostOrderResponse, + MerchantOrderPrivateStatusResponse, + TippingReserveStatus, + TipCreateConfirmation, + TipCreateRequest, + MerchantInstancesResponse, +} from "./merchantApiTypes"; +import { + openPromise, + OperationFailedError, + WalletCoreApiClient, +} from "@gnu-taler/taler-wallet-core"; +import { + AmountJson, + Amounts, + Configuration, + AmountString, + Codec, + buildCodecForObject, + codecForString, + Duration, + parsePaytoUri, + CoreApiResponse, + createEddsaKeyPair, + eddsaGetPublic, + EddsaKeyPair, + encodeCrock, + getRandomBytes, +} from "@gnu-taler/taler-util"; +import { CoinConfig } from "./denomStructures.js"; + +const exec = util.promisify(require("child_process").exec); + +export async function delayMs(ms: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), ms); + }); +} + +export interface WithAuthorization { + Authorization?: string; +} + +interface WaitResult { + code: number | null; + signal: NodeJS.Signals | null; +} + +/** + * Run a shell command, return stdout. + */ +export async function sh( + t: GlobalTestState, + logName: string, + command: string, + env: { [index: string]: string | undefined } = process.env, +): Promise { + console.log("running command", command); + return new Promise((resolve, reject) => { + const stdoutChunks: Buffer[] = []; + const proc = spawn(command, { + stdio: ["inherit", "pipe", "pipe"], + shell: true, + env: env, + }); + proc.stdout.on("data", (x) => { + if (x instanceof Buffer) { + stdoutChunks.push(x); + } else { + throw Error("unexpected data chunk type"); + } + }); + const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); + const stderrLog = fs.createWriteStream(stderrLogFileName, { + flags: "a", + }); + proc.stderr.pipe(stderrLog); + proc.on("exit", (code, signal) => { + console.log(`child process exited (${code} / ${signal})`); + if (code != 0) { + reject(Error(`Unexpected exit code ${code} for '${command}'`)); + return; + } + const b = Buffer.concat(stdoutChunks).toString("utf-8"); + resolve(b); + }); + proc.on("error", () => { + reject(Error("Child process had error")); + }); + }); +} + +function shellescape(args: string[]) { + const ret = args.map((s) => { + if (/[^A-Za-z0-9_\/:=-]/.test(s)) { + s = "'" + s.replace(/'/g, "'\\''") + "'"; + s = s.replace(/^(?:'')+/g, "").replace(/\\'''/g, "\\'"); + } + return s; + }); + return ret.join(" "); +} + +/** + * Run a shell command, return stdout. + * + * Log stderr to a log file. + */ +export async function runCommand( + t: GlobalTestState, + logName: string, + command: string, + args: string[], + env: { [index: string]: string | undefined } = process.env, +): Promise { + console.log("running command", shellescape([command, ...args])); + return new Promise((resolve, reject) => { + const stdoutChunks: Buffer[] = []; + const proc = spawn(command, args, { + stdio: ["inherit", "pipe", "pipe"], + shell: false, + env: env, + }); + proc.stdout.on("data", (x) => { + if (x instanceof Buffer) { + stdoutChunks.push(x); + } else { + throw Error("unexpected data chunk type"); + } + }); + const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); + const stderrLog = fs.createWriteStream(stderrLogFileName, { + flags: "a", + }); + proc.stderr.pipe(stderrLog); + proc.on("exit", (code, signal) => { + console.log(`child process exited (${code} / ${signal})`); + if (code != 0) { + reject(Error(`Unexpected exit code ${code} for '${command}'`)); + return; + } + const b = Buffer.concat(stdoutChunks).toString("utf-8"); + resolve(b); + }); + proc.on("error", () => { + reject(Error("Child process had error")); + }); + }); +} + +export class ProcessWrapper { + private waitPromise: Promise; + constructor(public proc: ChildProcess) { + this.waitPromise = new Promise((resolve, reject) => { + proc.on("exit", (code, signal) => { + resolve({ code, signal }); + }); + proc.on("error", (err) => { + reject(err); + }); + }); + } + + wait(): Promise { + return this.waitPromise; + } +} + +export class GlobalTestParams { + testDir: string; +} + +export class GlobalTestState { + testDir: string; + procs: ProcessWrapper[]; + servers: http.Server[]; + inShutdown: boolean = false; + constructor(params: GlobalTestParams) { + this.testDir = params.testDir; + this.procs = []; + this.servers = []; + } + + async assertThrowsOperationErrorAsync( + block: () => Promise, + ): Promise { + try { + await block(); + } catch (e) { + if (e instanceof OperationFailedError) { + return e; + } + throw Error(`expected OperationFailedError to be thrown, but got ${e}`); + } + throw Error( + `expected OperationFailedError to be thrown, but block finished without throwing`, + ); + } + + async assertThrowsAsync(block: () => Promise): Promise { + try { + await block(); + } catch (e) { + return e; + } + throw Error( + `expected exception to be thrown, but block finished without throwing`, + ); + } + + assertAxiosError(e: any): asserts e is AxiosError { + if (!e.isAxiosError) { + throw Error("expected axios error"); + } + } + + assertTrue(b: boolean): asserts b { + if (!b) { + throw Error("test assertion failed"); + } + } + + assertDeepEqual(actual: any, expected: T): asserts actual is T { + deepStrictEqual(actual, expected); + } + + assertAmountEquals( + amtActual: string | AmountJson, + amtExpected: string | AmountJson, + ): void { + if (Amounts.cmp(amtActual, amtExpected) != 0) { + throw Error( + `test assertion failed: expected ${Amounts.stringify( + amtExpected, + )} but got ${Amounts.stringify(amtActual)}`, + ); + } + } + + assertAmountLeq(a: string | AmountJson, b: string | AmountJson): void { + if (Amounts.cmp(a, b) > 0) { + throw Error( + `test assertion failed: expected ${Amounts.stringify( + a, + )} to be less or equal (leq) than ${Amounts.stringify(b)}`, + ); + } + } + + shutdownSync(): void { + for (const s of this.servers) { + s.close(); + s.removeAllListeners(); + } + for (const p of this.procs) { + if (p.proc.exitCode == null) { + p.proc.kill("SIGTERM"); + } + } + } + + spawnService( + command: string, + args: string[], + logName: string, + env: { [index: string]: string | undefined } = process.env, + ): ProcessWrapper { + console.log( + `spawning process (${logName}): ${shellescape([command, ...args])}`, + ); + const proc = spawn(command, args, { + stdio: ["inherit", "pipe", "pipe"], + env: env, + }); + console.log(`spawned process (${logName}) with pid ${proc.pid}`); + proc.on("error", (err) => { + console.log(`could not start process (${command})`, err); + }); + proc.on("exit", (code, signal) => { + console.log(`process ${logName} exited`); + }); + const stderrLogFileName = this.testDir + `/${logName}-stderr.log`; + const stderrLog = fs.createWriteStream(stderrLogFileName, { + flags: "a", + }); + proc.stderr.pipe(stderrLog); + const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`; + const stdoutLog = fs.createWriteStream(stdoutLogFileName, { + flags: "a", + }); + proc.stdout.pipe(stdoutLog); + const procWrap = new ProcessWrapper(proc); + this.procs.push(procWrap); + return procWrap; + } + + async shutdown(): Promise { + if (this.inShutdown) { + return; + } + if (shouldLingerInTest()) { + console.log("refusing to shut down, lingering was requested"); + return; + } + this.inShutdown = true; + console.log("shutting down"); + for (const s of this.servers) { + s.close(); + s.removeAllListeners(); + } + for (const p of this.procs) { + if (p.proc.exitCode == null) { + console.log("killing process", p.proc.pid); + p.proc.kill("SIGTERM"); + await p.wait(); + } + } + } +} + +export function shouldLingerInTest(): boolean { + return !!process.env["TALER_TEST_LINGER"]; +} + +export interface TalerConfigSection { + options: Record; +} + +export interface TalerConfig { + sections: Record; +} + +export interface DbInfo { + /** + * Postgres connection string. + */ + connStr: string; + + dbname: string; +} + +export async function setupDb(gc: GlobalTestState): Promise { + const dbname = "taler-integrationtest"; + await exec(`dropdb "${dbname}" || true`); + await exec(`createdb "${dbname}"`); + return { + connStr: `postgres:///${dbname}`, + dbname, + }; +} + +export interface BankConfig { + currency: string; + httpPort: number; + database: string; + allowRegistrations: boolean; + maxDebt?: string; +} + +export interface FakeBankConfig { + currency: string; + httpPort: number; +} + +function setTalerPaths(config: Configuration, home: string) { + config.setString("paths", "taler_home", home); + // We need to make sure that the path of taler_runtime_dir isn't too long, + // as it contains unix domain sockets (108 character limit). + const runDir = fs.mkdtempSync("/tmp/taler-test-"); + config.setString("paths", "taler_runtime_dir", runDir); + config.setString( + "paths", + "taler_data_home", + "$TALER_HOME/.local/share/taler/", + ); + config.setString("paths", "taler_config_home", "$TALER_HOME/.config/taler/"); + config.setString("paths", "taler_cache_home", "$TALER_HOME/.config/taler/"); +} + +function setCoin(config: Configuration, c: CoinConfig) { + const s = `coin_${c.name}`; + config.setString(s, "value", c.value); + config.setString(s, "duration_withdraw", c.durationWithdraw); + config.setString(s, "duration_spend", c.durationSpend); + config.setString(s, "duration_legal", c.durationLegal); + config.setString(s, "fee_deposit", c.feeDeposit); + config.setString(s, "fee_withdraw", c.feeWithdraw); + config.setString(s, "fee_refresh", c.feeRefresh); + config.setString(s, "fee_refund", c.feeRefund); + config.setString(s, "rsa_keysize", `${c.rsaKeySize}`); +} + +/** + * Send an HTTP request until it succeeds or the + * process dies. + */ +export async function pingProc( + proc: ProcessWrapper | undefined, + url: string, + serviceName: string, +): Promise { + if (!proc || proc.proc.exitCode !== null) { + throw Error(`service process ${serviceName} not started, can't ping`); + } + while (true) { + try { + console.log(`pinging ${serviceName}`); + const resp = await axios.get(url); + console.log(`service ${serviceName} available`); + return; + } catch (e: any) { + console.log(`service ${serviceName} not ready:`, e.toString()); + await delayMs(1000); + } + if (!proc || proc.proc.exitCode !== null) { + throw Error(`service process ${serviceName} stopped unexpectedly`); + } + } +} + +export interface HarnessExchangeBankAccount { + accountName: string; + accountPassword: string; + accountPaytoUri: string; + wireGatewayApiBaseUrl: string; +} + +export interface BankServiceInterface { + readonly baseUrl: string; + readonly port: number; +} + +export enum CreditDebitIndicator { + Credit = "credit", + Debit = "debit", +} + +export interface BankAccountBalanceResponse { + balance: { + amount: AmountString; + credit_debit_indicator: CreditDebitIndicator; + }; +} + +export namespace BankAccessApi { + export async function getAccountBalance( + bank: BankServiceInterface, + bankUser: BankUser, + ): Promise { + const url = new URL(`accounts/${bankUser.username}`, bank.baseUrl); + const resp = await axios.get(url.href, { + auth: bankUser, + }); + return resp.data; + } + + export async function createWithdrawalOperation( + bank: BankServiceInterface, + bankUser: BankUser, + amount: string, + ): Promise { + const url = new URL( + `accounts/${bankUser.username}/withdrawals`, + bank.baseUrl, + ); + const resp = await axios.post( + url.href, + { + amount, + }, + { + auth: bankUser, + }, + ); + return codecForWithdrawalOperationInfo().decode(resp.data); + } +} + +export namespace BankApi { + export async function registerAccount( + bank: BankServiceInterface, + username: string, + password: string, + ): Promise { + const url = new URL("testing/register", bank.baseUrl); + await axios.post(url.href, { + username, + password, + }); + return { + password, + username, + accountPaytoUri: `payto://x-taler-bank/localhost/${username}`, + }; + } + + export async function createRandomBankUser( + bank: BankServiceInterface, + ): Promise { + const username = "user-" + encodeCrock(getRandomBytes(10)); + const password = "pw-" + encodeCrock(getRandomBytes(10)); + return await registerAccount(bank, username, password); + } + + export async function adminAddIncoming( + bank: BankServiceInterface, + params: { + exchangeBankAccount: HarnessExchangeBankAccount; + amount: string; + reservePub: string; + debitAccountPayto: string; + }, + ) { + const url = new URL( + `taler-wire-gateway/${params.exchangeBankAccount.accountName}/admin/add-incoming`, + bank.baseUrl, + ); + await axios.post( + url.href, + { + amount: params.amount, + reserve_pub: params.reservePub, + debit_account: params.debitAccountPayto, + }, + { + auth: { + username: params.exchangeBankAccount.accountName, + password: params.exchangeBankAccount.accountPassword, + }, + }, + ); + } + + export async function confirmWithdrawalOperation( + bank: BankServiceInterface, + bankUser: BankUser, + wopi: WithdrawalOperationInfo, + ): Promise { + const url = new URL( + `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`, + bank.baseUrl, + ); + await axios.post( + url.href, + {}, + { + auth: bankUser, + }, + ); + } + + export async function abortWithdrawalOperation( + bank: BankServiceInterface, + bankUser: BankUser, + wopi: WithdrawalOperationInfo, + ): Promise { + const url = new URL( + `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/abort`, + bank.baseUrl, + ); + await axios.post( + url.href, + {}, + { + auth: bankUser, + }, + ); + } +} + +export class BankService implements BankServiceInterface { + proc: ProcessWrapper | undefined; + + static fromExistingConfig(gc: GlobalTestState): BankService { + const cfgFilename = gc.testDir + "/bank.conf"; + console.log("reading bank config from", cfgFilename); + const config = Configuration.load(cfgFilename); + const bc: BankConfig = { + allowRegistrations: config + .getYesNo("bank", "allow_registrations") + .required(), + currency: config.getString("taler", "currency").required(), + database: config.getString("bank", "database").required(), + httpPort: config.getNumber("bank", "http_port").required(), + }; + return new BankService(gc, bc, cfgFilename); + } + + static async create( + gc: GlobalTestState, + bc: BankConfig, + ): Promise { + const config = new Configuration(); + setTalerPaths(config, gc.testDir + "/talerhome"); + config.setString("taler", "currency", bc.currency); + config.setString("bank", "database", bc.database); + config.setString("bank", "http_port", `${bc.httpPort}`); + config.setString("bank", "serve", "http"); + config.setString("bank", "max_debt_bank", `${bc.currency}:999999`); + config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`); + config.setString( + "bank", + "allow_registrations", + bc.allowRegistrations ? "yes" : "no", + ); + const cfgFilename = gc.testDir + "/bank.conf"; + config.write(cfgFilename); + + await sh( + gc, + "taler-bank-manage_django", + `taler-bank-manage -c '${cfgFilename}' django migrate`, + ); + await sh( + gc, + "taler-bank-manage_django", + `taler-bank-manage -c '${cfgFilename}' django provide_accounts`, + ); + + return new BankService(gc, bc, cfgFilename); + } + + setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) { + const config = Configuration.load(this.configFile); + config.setString("bank", "suggested_exchange", e.baseUrl); + config.setString("bank", "suggested_exchange_payto", exchangePayto); + } + + get baseUrl(): string { + return `http://localhost:${this.bankConfig.httpPort}/`; + } + + async createExchangeAccount( + accountName: string, + password: string, + ): Promise { + await sh( + this.globalTestState, + "taler-bank-manage_django", + `taler-bank-manage -c '${this.configFile}' django add_bank_account ${accountName}`, + ); + await sh( + this.globalTestState, + "taler-bank-manage_django", + `taler-bank-manage -c '${this.configFile}' django changepassword_unsafe ${accountName} ${password}`, + ); + await sh( + this.globalTestState, + "taler-bank-manage_django", + `taler-bank-manage -c '${this.configFile}' django top_up ${accountName} ${this.bankConfig.currency}:100000`, + ); + return { + accountName: accountName, + accountPassword: password, + accountPaytoUri: `payto://x-taler-bank/${accountName}`, + wireGatewayApiBaseUrl: `http://localhost:${this.bankConfig.httpPort}/taler-wire-gateway/${accountName}/`, + }; + } + + get port() { + return this.bankConfig.httpPort; + } + + private constructor( + private globalTestState: GlobalTestState, + private bankConfig: BankConfig, + private configFile: string, + ) {} + + async start(): Promise { + this.proc = this.globalTestState.spawnService( + "taler-bank-manage", + ["-c", this.configFile, "serve"], + "bank", + ); + } + + async pingUntilAvailable(): Promise { + const url = `http://localhost:${this.bankConfig.httpPort}/config`; + await pingProc(this.proc, url, "bank"); + } +} + +export class FakeBankService { + proc: ProcessWrapper | undefined; + + static fromExistingConfig(gc: GlobalTestState): FakeBankService { + const cfgFilename = gc.testDir + "/bank.conf"; + console.log("reading fakebank config from", cfgFilename); + const config = Configuration.load(cfgFilename); + const bc: FakeBankConfig = { + currency: config.getString("taler", "currency").required(), + httpPort: config.getNumber("bank", "http_port").required(), + }; + return new FakeBankService(gc, bc, cfgFilename); + } + + static async create( + gc: GlobalTestState, + bc: FakeBankConfig, + ): Promise { + const config = new Configuration(); + setTalerPaths(config, gc.testDir + "/talerhome"); + config.setString("taler", "currency", bc.currency); + config.setString("bank", "http_port", `${bc.httpPort}`); + const cfgFilename = gc.testDir + "/bank.conf"; + config.write(cfgFilename); + return new FakeBankService(gc, bc, cfgFilename); + } + + get baseUrl(): string { + return `http://localhost:${this.bankConfig.httpPort}/`; + } + + get port() { + return this.bankConfig.httpPort; + } + + private constructor( + private globalTestState: GlobalTestState, + private bankConfig: FakeBankConfig, + private configFile: string, + ) {} + + async start(): Promise { + this.proc = this.globalTestState.spawnService( + "taler-fakebank-run", + ["-c", this.configFile], + "fakebank", + ); + } + + async pingUntilAvailable(): Promise { + // Fakebank doesn't have "/config", so we ping just "/". + const url = `http://localhost:${this.bankConfig.httpPort}/`; + await pingProc(this.proc, url, "bank"); + } +} + +export interface BankUser { + username: string; + password: string; + accountPaytoUri: string; +} + +export interface WithdrawalOperationInfo { + withdrawal_id: string; + taler_withdraw_uri: string; +} + +const codecForWithdrawalOperationInfo = (): Codec => + buildCodecForObject() + .property("withdrawal_id", codecForString()) + .property("taler_withdraw_uri", codecForString()) + .build("WithdrawalOperationInfo"); + +export interface ExchangeConfig { + name: string; + currency: string; + roundUnit?: string; + httpPort: number; + database: string; +} + +export interface ExchangeServiceInterface { + readonly baseUrl: string; + readonly port: number; + readonly name: string; + readonly masterPub: string; +} + +export class ExchangeService implements ExchangeServiceInterface { + static fromExistingConfig(gc: GlobalTestState, exchangeName: string) { + const cfgFilename = gc.testDir + `/exchange-${exchangeName}.conf`; + const config = Configuration.load(cfgFilename); + const ec: ExchangeConfig = { + currency: config.getString("taler", "currency").required(), + database: config.getString("exchangedb-postgres", "config").required(), + httpPort: config.getNumber("exchange", "port").required(), + name: exchangeName, + roundUnit: config.getString("taler", "currency_round_unit").required(), + }; + const privFile = config.getPath("exchange", "master_priv_file").required(); + const eddsaPriv = fs.readFileSync(privFile); + const keyPair: EddsaKeyPair = { + eddsaPriv, + eddsaPub: eddsaGetPublic(eddsaPriv), + }; + return new ExchangeService(gc, ec, cfgFilename, keyPair); + } + + private currentTimetravel: Duration | undefined; + + setTimetravel(t: Duration | undefined): void { + if (this.isRunning()) { + throw Error("can't set time travel while the exchange is running"); + } + this.currentTimetravel = t; + } + + private get timetravelArg(): string | undefined { + if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { + // Convert to microseconds + return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; + } + return undefined; + } + + /** + * Return an empty array if no time travel is set, + * and an array with the time travel command line argument + * otherwise. + */ + private get timetravelArgArr(): string[] { + const tta = this.timetravelArg; + if (tta) { + return [tta]; + } + return []; + } + + async runWirewatchOnce() { + await runCommand( + this.globalState, + `exchange-${this.name}-wirewatch-once`, + "taler-exchange-wirewatch", + [...this.timetravelArgArr, "-c", this.configFilename, "-t"], + ); + } + + async runAggregatorOnce() { + await runCommand( + this.globalState, + `exchange-${this.name}-aggregator-once`, + "taler-exchange-aggregator", + [...this.timetravelArgArr, "-c", this.configFilename, "-t"], + ); + } + + async runTransferOnce() { + await runCommand( + this.globalState, + `exchange-${this.name}-transfer-once`, + "taler-exchange-transfer", + [...this.timetravelArgArr, "-c", this.configFilename, "-t"], + ); + } + + changeConfig(f: (config: Configuration) => void) { + const config = Configuration.load(this.configFilename); + f(config); + config.write(this.configFilename); + } + + static create(gc: GlobalTestState, e: ExchangeConfig) { + const config = new Configuration(); + config.setString("taler", "currency", e.currency); + config.setString( + "taler", + "currency_round_unit", + e.roundUnit ?? `${e.currency}:0.01`, + ); + setTalerPaths(config, gc.testDir + "/talerhome"); + config.setString( + "exchange", + "revocation_dir", + "${TALER_DATA_HOME}/exchange/revocations", + ); + config.setString("exchange", "max_keys_caching", "forever"); + config.setString("exchange", "db", "postgres"); + config.setString( + "exchange-offline", + "master_priv_file", + "${TALER_DATA_HOME}/exchange/offline-keys/master.priv", + ); + config.setString("exchange", "serve", "tcp"); + config.setString("exchange", "port", `${e.httpPort}`); + + config.setString("exchangedb-postgres", "config", e.database); + + config.setString("taler-exchange-secmod-eddsa", "lookahead_sign", "20 s"); + config.setString("taler-exchange-secmod-rsa", "lookahead_sign", "20 s"); + + const exchangeMasterKey = createEddsaKeyPair(); + + config.setString( + "exchange", + "master_public_key", + encodeCrock(exchangeMasterKey.eddsaPub), + ); + + const masterPrivFile = config + .getPath("exchange-offline", "master_priv_file") + .required(); + + fs.mkdirSync(path.dirname(masterPrivFile), { recursive: true }); + + fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv)); + + const cfgFilename = gc.testDir + `/exchange-${e.name}.conf`; + config.write(cfgFilename); + return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey); + } + + addOfferedCoins(offeredCoins: ((curr: string) => CoinConfig)[]) { + const config = Configuration.load(this.configFilename); + offeredCoins.forEach((cc) => + setCoin(config, cc(this.exchangeConfig.currency)), + ); + config.write(this.configFilename); + } + + addCoinConfigList(ccs: CoinConfig[]) { + const config = Configuration.load(this.configFilename); + ccs.forEach((cc) => setCoin(config, cc)); + config.write(this.configFilename); + } + + get masterPub() { + return encodeCrock(this.keyPair.eddsaPub); + } + + get port() { + return this.exchangeConfig.httpPort; + } + + async addBankAccount( + localName: string, + exchangeBankAccount: HarnessExchangeBankAccount, + ): Promise { + const config = Configuration.load(this.configFilename); + config.setString( + `exchange-account-${localName}`, + "wire_response", + `\${TALER_DATA_HOME}/exchange/account-${localName}.json`, + ); + config.setString( + `exchange-account-${localName}`, + "payto_uri", + exchangeBankAccount.accountPaytoUri, + ); + config.setString(`exchange-account-${localName}`, "enable_credit", "yes"); + config.setString(`exchange-account-${localName}`, "enable_debit", "yes"); + config.setString( + `exchange-accountcredentials-${localName}`, + "wire_gateway_url", + exchangeBankAccount.wireGatewayApiBaseUrl, + ); + config.setString( + `exchange-accountcredentials-${localName}`, + "wire_gateway_auth_method", + "basic", + ); + config.setString( + `exchange-accountcredentials-${localName}`, + "username", + exchangeBankAccount.accountName, + ); + config.setString( + `exchange-accountcredentials-${localName}`, + "password", + exchangeBankAccount.accountPassword, + ); + config.write(this.configFilename); + } + + exchangeHttpProc: ProcessWrapper | undefined; + exchangeWirewatchProc: ProcessWrapper | undefined; + + helperCryptoRsaProc: ProcessWrapper | undefined; + helperCryptoEddsaProc: ProcessWrapper | undefined; + + constructor( + private globalState: GlobalTestState, + private exchangeConfig: ExchangeConfig, + private configFilename: string, + private keyPair: EddsaKeyPair, + ) {} + + get name() { + return this.exchangeConfig.name; + } + + get baseUrl() { + return `http://localhost:${this.exchangeConfig.httpPort}/`; + } + + isRunning(): boolean { + return !!this.exchangeWirewatchProc || !!this.exchangeHttpProc; + } + + async stop(): Promise { + const wirewatch = this.exchangeWirewatchProc; + if (wirewatch) { + wirewatch.proc.kill("SIGTERM"); + await wirewatch.wait(); + this.exchangeWirewatchProc = undefined; + } + const httpd = this.exchangeHttpProc; + if (httpd) { + httpd.proc.kill("SIGTERM"); + await httpd.wait(); + this.exchangeHttpProc = undefined; + } + const cryptoRsa = this.helperCryptoRsaProc; + if (cryptoRsa) { + cryptoRsa.proc.kill("SIGTERM"); + await cryptoRsa.wait(); + this.helperCryptoRsaProc = undefined; + } + const cryptoEddsa = this.helperCryptoEddsaProc; + if (cryptoEddsa) { + cryptoEddsa.proc.kill("SIGTERM"); + await cryptoEddsa.wait(); + this.helperCryptoRsaProc = undefined; + } + } + + /** + * Update keys signing the keys generated by the security module + * with the offline signing key. + */ + async keyup(): Promise { + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + ["-c", this.configFilename, "download", "sign", "upload"], + ); + + const accounts: string[] = []; + const accountTargetTypes: Set = new Set(); + + const config = Configuration.load(this.configFilename); + for (const sectionName of config.getSectionNames()) { + if (sectionName.startsWith("EXCHANGE-ACCOUNT-")) { + const paytoUri = config.getString(sectionName, "payto_uri").required(); + const p = parsePaytoUri(paytoUri); + if (!p) { + throw Error(`invalid payto uri in exchange config: ${paytoUri}`); + } + accountTargetTypes.add(p?.targetType); + accounts.push(paytoUri); + } + } + + console.log("configuring bank accounts", accounts); + + for (const acc of accounts) { + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + ["-c", this.configFilename, "enable-account", acc, "upload"], + ); + } + + const year = new Date().getFullYear(); + for (const accTargetType of accountTargetTypes.values()) { + for (let i = year; i < year + 5; i++) { + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + [ + "-c", + this.configFilename, + "wire-fee", + `${i}`, + accTargetType, + `${this.exchangeConfig.currency}:0.01`, + `${this.exchangeConfig.currency}:0.01`, + "upload", + ], + ); + } + } + } + + async revokeDenomination(denomPubHash: string) { + if (!this.isRunning()) { + throw Error("exchange must be running when revoking denominations"); + } + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + [ + "-c", + this.configFilename, + "revoke-denomination", + denomPubHash, + "upload", + ], + ); + } + + async purgeSecmodKeys(): Promise { + const cfg = Configuration.load(this.configFilename); + const rsaKeydir = cfg + .getPath("taler-exchange-secmod-rsa", "KEY_DIR") + .required(); + const eddsaKeydir = cfg + .getPath("taler-exchange-secmod-eddsa", "KEY_DIR") + .required(); + // Be *VERY* careful when changing this, or you will accidentally delete user data. + await sh(this.globalState, "rm-secmod-keys", `rm -rf ${rsaKeydir}/COIN_*`); + await sh(this.globalState, "rm-secmod-keys", `rm ${eddsaKeydir}/*`); + } + + async purgeDatabase(): Promise { + await sh( + this.globalState, + "exchange-dbinit", + `taler-exchange-dbinit -r -c "${this.configFilename}"`, + ); + } + + async start(): Promise { + if (this.isRunning()) { + throw Error("exchange is already running"); + } + await sh( + this.globalState, + "exchange-dbinit", + `taler-exchange-dbinit -c "${this.configFilename}"`, + ); + + this.helperCryptoEddsaProc = this.globalState.spawnService( + "taler-exchange-secmod-eddsa", + ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], + `exchange-crypto-eddsa-${this.name}`, + ); + + this.helperCryptoRsaProc = this.globalState.spawnService( + "taler-exchange-secmod-rsa", + ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], + `exchange-crypto-rsa-${this.name}`, + ); + + this.exchangeWirewatchProc = this.globalState.spawnService( + "taler-exchange-wirewatch", + ["-c", this.configFilename, ...this.timetravelArgArr], + `exchange-wirewatch-${this.name}`, + ); + + this.exchangeHttpProc = this.globalState.spawnService( + "taler-exchange-httpd", + ["-c", this.configFilename, ...this.timetravelArgArr], + `exchange-httpd-${this.name}`, + ); + + await this.pingUntilAvailable(); + await this.keyup(); + } + + async pingUntilAvailable(): Promise { + // We request /management/keys, since /keys can block + // when we didn't do the key setup yet. + const url = `http://localhost:${this.exchangeConfig.httpPort}/management/keys`; + await pingProc(this.exchangeHttpProc, url, `exchange (${this.name})`); + } +} + +export interface MerchantConfig { + name: string; + currency: string; + httpPort: number; + database: string; +} + +export interface PrivateOrderStatusQuery { + instance?: string; + orderId: string; + sessionId?: string; +} + +export interface MerchantServiceInterface { + makeInstanceBaseUrl(instanceName?: string): string; + readonly port: number; + readonly name: string; +} + +export class MerchantApiClient { + constructor( + private baseUrl: string, + public readonly auth: MerchantAuthConfiguration, + ) {} + + async changeAuth(auth: MerchantAuthConfiguration): Promise { + const url = new URL("private/auth", this.baseUrl); + await axios.post(url.href, auth, { + headers: this.makeAuthHeader(), + }); + } + + async deleteInstance(instanceId: string) { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + await axios.delete(url.href, { + headers: this.makeAuthHeader(), + }); + } + + async createInstance(req: MerchantInstanceConfig): Promise { + const url = new URL("management/instances", this.baseUrl); + await axios.post(url.href, req, { + headers: this.makeAuthHeader(), + }); + } + + async getInstances(): Promise { + const url = new URL("management/instances", this.baseUrl); + const resp = await axios.get(url.href, { + headers: this.makeAuthHeader(), + }); + return resp.data; + } + + async getInstanceFullDetails(instanceId: string): Promise { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + try { + const resp = await axios.get(url.href, { + headers: this.makeAuthHeader(), + }); + return resp.data; + } catch (e) { + throw e; + } + } + + makeAuthHeader(): Record { + switch (this.auth.method) { + case "external": + return {}; + case "token": + return { + Authorization: `Bearer ${this.auth.token}`, + }; + } + } +} + +/** + * FIXME: This should be deprecated in favor of MerchantApiClient + */ +export namespace MerchantPrivateApi { + export async function createOrder( + merchantService: MerchantServiceInterface, + instanceName: string, + req: PostOrderRequest, + withAuthorization: WithAuthorization = {}, + ): Promise { + const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); + let url = new URL("private/orders", baseUrl); + const resp = await axios.post(url.href, req, { + headers: withAuthorization, + }); + return codecForPostOrderResponse().decode(resp.data); + } + + export async function queryPrivateOrderStatus( + merchantService: MerchantServiceInterface, + query: PrivateOrderStatusQuery, + withAuthorization: WithAuthorization = {}, + ): Promise { + const reqUrl = new URL( + `private/orders/${query.orderId}`, + merchantService.makeInstanceBaseUrl(query.instance), + ); + if (query.sessionId) { + reqUrl.searchParams.set("session_id", query.sessionId); + } + const resp = await axios.get(reqUrl.href, { headers: withAuthorization }); + return codecForMerchantOrderPrivateStatusResponse().decode(resp.data); + } + + export async function giveRefund( + merchantService: MerchantServiceInterface, + r: { + instance: string; + orderId: string; + amount: string; + justification: string; + }, + ): Promise<{ talerRefundUri: string }> { + const reqUrl = new URL( + `private/orders/${r.orderId}/refund`, + merchantService.makeInstanceBaseUrl(r.instance), + ); + const resp = await axios.post(reqUrl.href, { + refund: r.amount, + reason: r.justification, + }); + return { + talerRefundUri: resp.data.taler_refund_uri, + }; + } + + export async function createTippingReserve( + merchantService: MerchantServiceInterface, + instance: string, + req: CreateMerchantTippingReserveRequest, + ): Promise { + const reqUrl = new URL( + `private/reserves`, + merchantService.makeInstanceBaseUrl(instance), + ); + const resp = await axios.post(reqUrl.href, req); + // FIXME: validate + return resp.data; + } + + export async function queryTippingReserves( + merchantService: MerchantServiceInterface, + instance: string, + ): Promise { + const reqUrl = new URL( + `private/reserves`, + merchantService.makeInstanceBaseUrl(instance), + ); + const resp = await axios.get(reqUrl.href); + // FIXME: validate + return resp.data; + } + + export async function giveTip( + merchantService: MerchantServiceInterface, + instance: string, + req: TipCreateRequest, + ): Promise { + const reqUrl = new URL( + `private/tips`, + merchantService.makeInstanceBaseUrl(instance), + ); + const resp = await axios.post(reqUrl.href, req); + // FIXME: validate + return resp.data; + } +} + +export interface CreateMerchantTippingReserveRequest { + // Amount that the merchant promises to put into the reserve + initial_balance: AmountString; + + // Exchange the merchant intends to use for tipping + exchange_url: string; + + // Desired wire method, for example "iban" or "x-taler-bank" + wire_method: string; +} + +export interface CreateMerchantTippingReserveConfirmation { + // Public key identifying the reserve + reserve_pub: string; + + // Wire account of the exchange where to transfer the funds + payto_uri: string; +} + +export class MerchantService implements MerchantServiceInterface { + static fromExistingConfig(gc: GlobalTestState, name: string) { + const cfgFilename = gc.testDir + `/merchant-${name}.conf`; + const config = Configuration.load(cfgFilename); + const mc: MerchantConfig = { + currency: config.getString("taler", "currency").required(), + database: config.getString("merchantdb-postgres", "config").required(), + httpPort: config.getNumber("merchant", "port").required(), + name, + }; + return new MerchantService(gc, mc, cfgFilename); + } + + proc: ProcessWrapper | undefined; + + constructor( + private globalState: GlobalTestState, + private merchantConfig: MerchantConfig, + private configFilename: string, + ) {} + + private currentTimetravel: Duration | undefined; + + private isRunning(): boolean { + return !!this.proc; + } + + setTimetravel(t: Duration | undefined): void { + if (this.isRunning()) { + throw Error("can't set time travel while the exchange is running"); + } + this.currentTimetravel = t; + } + + private get timetravelArg(): string | undefined { + if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { + // Convert to microseconds + return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; + } + return undefined; + } + + /** + * Return an empty array if no time travel is set, + * and an array with the time travel command line argument + * otherwise. + */ + private get timetravelArgArr(): string[] { + const tta = this.timetravelArg; + if (tta) { + return [tta]; + } + return []; + } + + get port(): number { + return this.merchantConfig.httpPort; + } + + get name(): string { + return this.merchantConfig.name; + } + + async stop(): Promise { + const httpd = this.proc; + if (httpd) { + httpd.proc.kill("SIGTERM"); + await httpd.wait(); + this.proc = undefined; + } + } + + async start(): Promise { + await exec(`taler-merchant-dbinit -c "${this.configFilename}"`); + + this.proc = this.globalState.spawnService( + "taler-merchant-httpd", + ["-LDEBUG", "-c", this.configFilename, ...this.timetravelArgArr], + `merchant-${this.merchantConfig.name}`, + ); + } + + static async create( + gc: GlobalTestState, + mc: MerchantConfig, + ): Promise { + const config = new Configuration(); + config.setString("taler", "currency", mc.currency); + + const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`; + setTalerPaths(config, gc.testDir + "/talerhome"); + config.setString("merchant", "serve", "tcp"); + config.setString("merchant", "port", `${mc.httpPort}`); + config.setString( + "merchant", + "keyfile", + "${TALER_DATA_HOME}/merchant/merchant.priv", + ); + config.setString("merchantdb-postgres", "config", mc.database); + config.write(cfgFilename); + + return new MerchantService(gc, mc, cfgFilename); + } + + addExchange(e: ExchangeServiceInterface): void { + const config = Configuration.load(this.configFilename); + config.setString( + `merchant-exchange-${e.name}`, + "exchange_base_url", + e.baseUrl, + ); + config.setString( + `merchant-exchange-${e.name}`, + "currency", + this.merchantConfig.currency, + ); + config.setString(`merchant-exchange-${e.name}`, "master_key", e.masterPub); + config.write(this.configFilename); + } + + async addDefaultInstance(): Promise { + return await this.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + auth: { + method: "external", + }, + }); + } + + async addInstance( + instanceConfig: PartialMerchantInstanceConfig, + ): Promise { + if (!this.proc) { + throw Error("merchant must be running to add instance"); + } + console.log("adding instance"); + const url = `http://localhost:${this.merchantConfig.httpPort}/management/instances`; + const auth = instanceConfig.auth ?? { method: "external" }; + await axios.post(url, { + auth, + payto_uris: instanceConfig.paytoUris, + id: instanceConfig.id, + name: instanceConfig.name, + address: instanceConfig.address ?? {}, + jurisdiction: instanceConfig.jurisdiction ?? {}, + default_max_wire_fee: + instanceConfig.defaultMaxWireFee ?? + `${this.merchantConfig.currency}:1.0`, + default_wire_fee_amortization: + instanceConfig.defaultWireFeeAmortization ?? 3, + default_max_deposit_fee: + instanceConfig.defaultMaxDepositFee ?? + `${this.merchantConfig.currency}:1.0`, + default_wire_transfer_delay: instanceConfig.defaultWireTransferDelay ?? { + d_ms: "forever", + }, + default_pay_delay: instanceConfig.defaultPayDelay ?? { d_ms: "forever" }, + }); + } + + makeInstanceBaseUrl(instanceName?: string): string { + if (instanceName === undefined || instanceName === "default") { + return `http://localhost:${this.merchantConfig.httpPort}/`; + } else { + return `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/`; + } + } + + async pingUntilAvailable(): Promise { + const url = `http://localhost:${this.merchantConfig.httpPort}/config`; + await pingProc(this.proc, url, `merchant (${this.merchantConfig.name})`); + } +} + +export interface MerchantAuthConfiguration { + method: "external" | "token"; + token?: string; +} + +export interface PartialMerchantInstanceConfig { + auth?: MerchantAuthConfiguration; + id: string; + name: string; + paytoUris: string[]; + address?: unknown; + jurisdiction?: unknown; + defaultMaxWireFee?: string; + defaultMaxDepositFee?: string; + defaultWireFeeAmortization?: number; + defaultWireTransferDelay?: Duration; + defaultPayDelay?: Duration; +} + +export interface MerchantInstanceConfig { + auth: MerchantAuthConfiguration; + id: string; + name: string; + payto_uris: string[]; + address: unknown; + jurisdiction: unknown; + default_max_wire_fee: string; + default_max_deposit_fee: string; + default_wire_fee_amortization: number; + default_wire_transfer_delay: Duration; + default_pay_delay: Duration; +} + +type TestStatus = "pass" | "fail" | "skip"; + +export interface TestRunResult { + /** + * Name of the test. + */ + name: string; + + /** + * How long did the test run? + */ + timeSec: number; + + status: TestStatus; + + reason?: string; +} + +export async function runTestWithState( + gc: GlobalTestState, + testMain: (t: GlobalTestState) => Promise, + testName: string, + linger: boolean = false, +): Promise { + const startMs = new Date().getTime(); + + const p = openPromise(); + let status: TestStatus; + + const handleSignal = (s: string) => { + console.warn( + `**** received fatal process event, terminating test ${testName}`, + ); + gc.shutdownSync(); + process.exit(1); + }; + + process.on("SIGINT", handleSignal); + process.on("SIGTERM", handleSignal); + process.on("unhandledRejection", handleSignal); + process.on("uncaughtException", handleSignal); + + try { + console.log("running test in directory", gc.testDir); + await Promise.race([testMain(gc), p.promise]); + status = "pass"; + if (linger) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: true, + }); + await new Promise((resolve, reject) => { + rl.question("Press enter to shut down test.", () => { + resolve(); + }); + }); + rl.close(); + } + } catch (e) { + console.error("FATAL: test failed with exception", e); + status = "fail"; + } finally { + await gc.shutdown(); + } + const afterMs = new Date().getTime(); + return { + name: testName, + timeSec: (afterMs - startMs) / 1000, + status, + }; +} + +function shellWrap(s: string) { + return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"; +} + +export class WalletCli { + private currentTimetravel: Duration | undefined; + private _client: WalletCoreApiClient; + + setTimetravel(d: Duration | undefined) { + this.currentTimetravel = d; + } + + private get timetravelArg(): string | undefined { + if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { + // Convert to microseconds + return `--timetravel=${this.currentTimetravel.d_ms * 1000}`; + } + return undefined; + } + + constructor( + private globalTestState: GlobalTestState, + private name: string = "default", + ) { + const self = this; + this._client = { + async call(op: any, payload: any): Promise { + console.log("calling wallet with timetravel arg", self.timetravelArg); + const resp = await sh( + self.globalTestState, + `wallet-${self.name}`, + `taler-wallet-cli ${ + self.timetravelArg ?? "" + } --no-throttle --wallet-db '${self.dbfile}' api '${op}' ${shellWrap( + JSON.stringify(payload), + )}`, + ); + console.log(resp); + const ar = JSON.parse(resp) as CoreApiResponse; + if (ar.type === "error") { + throw new OperationFailedError(ar.error); + } else { + return ar.result; + } + }, + }; + } + + get dbfile(): string { + return this.globalTestState.testDir + `/walletdb-${this.name}.json`; + } + + deleteDatabase() { + fs.unlinkSync(this.dbfile); + } + + private get timetravelArgArr(): string[] { + const tta = this.timetravelArg; + if (tta) { + return [tta]; + } + return []; + } + + get client(): WalletCoreApiClient { + return this._client; + } + + async runUntilDone(args: { maxRetries?: number } = {}): Promise { + await runCommand( + this.globalTestState, + `wallet-${this.name}`, + "taler-wallet-cli", + [ + "--no-throttle", + ...this.timetravelArgArr, + "--wallet-db", + this.dbfile, + "run-until-done", + ...(args.maxRetries ? ["--max-retries", `${args.maxRetries}`] : []), + ], + ); + } + + async runPending(): Promise { + await runCommand( + this.globalTestState, + `wallet-${this.name}`, + "taler-wallet-cli", + [ + "--no-throttle", + ...this.timetravelArgArr, + "--wallet-db", + this.dbfile, + "run-pending", + ], + ); + } +} diff --git a/packages/taler-wallet-cli/src/harness/helpers.ts b/packages/taler-wallet-cli/src/harness/helpers.ts new file mode 100644 index 000000000..3b4e1643f --- /dev/null +++ b/packages/taler-wallet-cli/src/harness/helpers.ts @@ -0,0 +1,406 @@ +/* + 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 { + FaultInjectedExchangeService, + FaultInjectedMerchantService, +} from "./faultInjection"; +import { CoinConfig, defaultCoinConfig } from "./denomStructures"; +import { + AmountString, + Duration, + ContractTerms, + PreparePayResultType, + ConfirmPayResultType, +} from "@gnu-taler/taler-util"; +import { + DbInfo, + BankService, + ExchangeService, + MerchantService, + WalletCli, + GlobalTestState, + setupDb, + ExchangeServiceInterface, + BankApi, + BankAccessApi, + MerchantServiceInterface, + MerchantPrivateApi, + HarnessExchangeBankAccount, + WithAuthorization, +} from "./harness.js"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; + +export interface SimpleTestEnvironment { + commonDb: DbInfo; + bank: BankService; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + merchant: MerchantService; + wallet: WalletCli; +} + +export function getRandomIban(countryCode: string): string { + return `${countryCode}715001051796${(Math.random() * 100000000) + .toString() + .substring(0, 6)}`; +} + +export function getRandomString(): string { + return Math.random().toString(36).substring(2); +} + +/** + * Run a test case with a simple TESTKUDOS Taler environment, consisting + * of one exchange, one bank and one merchant. + */ +export async function createSimpleTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), +): 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", + ); + exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + 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: [`payto://x-taler-bank/merchant-default`], + }); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: ["payto://x-taler-bank/minst1"], + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + return { + commonDb: db, + exchange, + merchant, + wallet, + bank, + exchangeBankAccount, + }; +} + +export interface FaultyMerchantTestEnvironment { + commonDb: DbInfo; + bank: BankService; + exchange: ExchangeService; + faultyExchange: FaultInjectedExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + merchant: MerchantService; + faultyMerchant: FaultInjectedMerchantService; + wallet: WalletCli; +} + +/** + * 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); + + 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.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: ["payto://x-taler-bank/minst1"], + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + return { + commonDb: db, + exchange, + merchant, + wallet, + bank, + exchangeBankAccount, + faultyMerchant, + faultyExchange, + }; +} + +/** + * Withdraw balance. + */ +export async function startWithdrawViaBank( + t: GlobalTestState, + p: { + wallet: WalletCli; + bank: BankService; + exchange: ExchangeServiceInterface; + amount: AmountString; + }, +): Promise { + const { wallet, bank, exchange, amount } = p; + + const user = await BankApi.createRandomBankUser(bank); + const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount); + + // Hand it to the wallet + + await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { + talerWithdrawUri: wop.taler_withdraw_uri, + }); + + await wallet.runPending(); + + // Confirm it + + await BankApi.confirmWithdrawalOperation(bank, user, wop); + + // Withdraw + + await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + }); +} + +/** + * Withdraw balance. + */ +export async function withdrawViaBank( + t: GlobalTestState, + p: { + wallet: WalletCli; + bank: BankService; + exchange: ExchangeServiceInterface; + amount: AmountString; + }, +): Promise { + const { wallet } = p; + + await startWithdrawViaBank(t, p); + + await wallet.runUntilDone(); + + // Check balance + + await wallet.client.call(WalletApiOperation.GetBalances, {}); +} + +export async function applyTimeTravel( + timetravelDuration: Duration, + s: { + exchange?: ExchangeService; + merchant?: MerchantService; + wallet?: WalletCli; + }, +): Promise { + if (s.exchange) { + await s.exchange.stop(); + s.exchange.setTimetravel(timetravelDuration); + await s.exchange.start(); + await s.exchange.pingUntilAvailable(); + } + + if (s.merchant) { + await s.merchant.stop(); + s.merchant.setTimetravel(timetravelDuration); + await s.merchant.start(); + await s.merchant.pingUntilAvailable(); + } + + if (s.wallet) { + s.wallet.setTimetravel(timetravelDuration); + } +} + +/** + * Make a simple payment and check that it succeeded. + */ +export async function makeTestPayment( + t: GlobalTestState, + args: { + merchant: MerchantServiceInterface; + wallet: WalletCli; + order: Partial; + instance?: string; + }, + auth: WithAuthorization = {}, +): Promise { + // Set up order. + + const { wallet, merchant } = args; + const instance = args.instance ?? "default"; + + const orderResp = await MerchantPrivateApi.createOrder( + merchant, + instance, + { + order: args.order, + }, + auth, + ); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + }, + auth, + ); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const preparePayResult = await wallet.client.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderStatus.taler_pay_uri, + }, + ); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { + proposalId: preparePayResult.proposalId, + }); + + t.assertTrue(r2.type === ConfirmPayResultType.Done); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + instance, + }, + auth, + ); + + t.assertTrue(orderStatus.order_status === "paid"); +} diff --git a/packages/taler-wallet-cli/src/harness/libeufin.ts b/packages/taler-wallet-cli/src/harness/libeufin.ts new file mode 100644 index 000000000..11447b389 --- /dev/null +++ b/packages/taler-wallet-cli/src/harness/libeufin.ts @@ -0,0 +1,1676 @@ +/* + This file is part of GNU Taler + (C) 2021 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 axios from "axios"; +import { URL } from "@gnu-taler/taler-util"; +import { getRandomIban, getRandomString } from "../harness/helpers.js"; +import { + GlobalTestState, + DbInfo, + pingProc, + ProcessWrapper, + runCommand, + setupDb, + sh, +} from "../harness/harness.js"; + +export interface LibeufinSandboxServiceInterface { + baseUrl: string; +} + +export interface LibeufinNexusServiceInterface { + baseUrl: string; +} + +export interface LibeufinServices { + libeufinSandbox: LibeufinSandboxService; + libeufinNexus: LibeufinNexusService; + commonDb: DbInfo; +} + +export interface LibeufinSandboxConfig { + httpPort: number; + databaseJdbcUri: string; +} + +export interface LibeufinNexusConfig { + httpPort: number; + databaseJdbcUri: string; +} + +export interface DeleteBankConnectionRequest { + bankConnectionId: string; +} + +interface LibeufinNexusMoneyMovement { + amount: string; + creditDebitIndicator: string; + details: { + debtor: { + name: string; + }; + debtorAccount: { + iban: string; + }; + debtorAgent: { + bic: string; + }; + creditor: { + name: string; + }; + creditorAccount: { + iban: string; + }; + creditorAgent: { + bic: string; + }; + endToEndId: string; + unstructuredRemittanceInformation: string; + }; +} + +interface LibeufinNexusBatches { + batchTransactions: Array; +} + +interface LibeufinNexusTransaction { + amount: string; + creditDebitIndicator: string; + status: string; + bankTransactionCode: string; + valueDate: string; + bookingDate: string; + accountServicerRef: string; + batches: Array; +} + +interface LibeufinNexusTransactions { + transactions: Array; +} + +export interface LibeufinCliDetails { + nexusUrl: string; + sandboxUrl: string; + nexusDatabaseUri: string; + sandboxDatabaseUri: string; + user: LibeufinNexusUser; +} + +export interface LibeufinEbicsSubscriberDetails { + hostId: string; + partnerId: string; + userId: string; +} + +export interface LibeufinEbicsConnectionDetails { + subscriberDetails: LibeufinEbicsSubscriberDetails; + ebicsUrl: string; + connectionName: string; +} + +export interface LibeufinBankAccountDetails { + currency: string; + iban: string; + bic: string; + personName: string; + accountName: string; +} + +export interface LibeufinNexusUser { + username: string; + password: string; +} + +export interface LibeufinBackupFileDetails { + passphrase: string; + outputFile: string; + connectionName: string; +} + +export interface LibeufinKeyLetterDetails { + outputFile: string; + connectionName: string; +} + +export interface LibeufinBankAccountImportDetails { + offeredBankAccountName: string; + nexusBankAccountName: string; + connectionName: string; +} + +export interface BankAccountInfo { + iban: string; + bic: string; + name: string; + currency: string; + label: string; +} + +export interface LibeufinPreparedPaymentDetails { + creditorIban: string; + creditorBic: string; + creditorName: string; + subject: string; + amount: string; + currency: string; + nexusBankAccountName: string; +} + +export interface LibeufinSandboxAddIncomingRequest { + creditorIban: string; + creditorBic: string; + creditorName: string; + debtorIban: string; + debtorBic: string; + debtorName: string; + subject: string; + amount: string; + currency: string; + uid: string; + direction: string; +} + +export class LibeufinSandboxService implements LibeufinSandboxServiceInterface { + static async create( + gc: GlobalTestState, + sandboxConfig: LibeufinSandboxConfig, + ): Promise { + return new LibeufinSandboxService(gc, sandboxConfig); + } + + sandboxProc: ProcessWrapper | undefined; + globalTestState: GlobalTestState; + + constructor( + gc: GlobalTestState, + private sandboxConfig: LibeufinSandboxConfig, + ) { + this.globalTestState = gc; + } + + get baseUrl(): string { + return `http://localhost:${this.sandboxConfig.httpPort}/`; + } + + async start(): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-sandbox-config", + "libeufin-sandbox config localhost", + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, + }, + ); + this.sandboxProc = this.globalTestState.spawnService( + "libeufin-sandbox", + ["serve", "--port", `${this.sandboxConfig.httpPort}`], + "libeufin-sandbox", + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, + LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", + }, + ); + } + + async c53tick(): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-sandbox-c53tick", + "libeufin-sandbox camt053tick", + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, + }, + ); + return stdout; + } + + async makeTransaction( + debit: string, + credit: string, + amount: string, // $currency:x.y + subject: string,): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-sandbox-maketransfer", + `libeufin-sandbox make-transaction --debit-account=${debit} --credit-account=${credit} ${amount} "${subject}"`, + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, + }, + ); + return stdout; + } + + async pingUntilAvailable(): Promise { + const url = this.baseUrl; + await pingProc(this.sandboxProc, url, "libeufin-sandbox"); + } +} + +export class LibeufinNexusService { + static async create( + gc: GlobalTestState, + nexusConfig: LibeufinNexusConfig, + ): Promise { + return new LibeufinNexusService(gc, nexusConfig); + } + + nexusProc: ProcessWrapper | undefined; + globalTestState: GlobalTestState; + + constructor(gc: GlobalTestState, private nexusConfig: LibeufinNexusConfig) { + this.globalTestState = gc; + } + + get baseUrl(): string { + return `http://localhost:${this.nexusConfig.httpPort}/`; + } + + async start(): Promise { + await runCommand( + this.globalTestState, + "libeufin-nexus-superuser", + "libeufin-nexus", + ["superuser", "admin", "--password", "test"], + { + ...process.env, + LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, + }, + ); + + this.nexusProc = this.globalTestState.spawnService( + "libeufin-nexus", + ["serve", "--port", `${this.nexusConfig.httpPort}`], + "libeufin-nexus", + { + ...process.env, + LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, + }, + ); + } + + async pingUntilAvailable(): Promise { + const url = `${this.baseUrl}config`; + await pingProc(this.nexusProc, url, "libeufin-nexus"); + } + + async createNexusSuperuser(details: LibeufinNexusUser): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-nexus", + `libeufin-nexus superuser ${details.username} --password=${details.password}`, + { + ...process.env, + LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, + }, + ); + console.log(stdout); + } +} + +export interface CreateEbicsSubscriberRequest { + hostID: string; + userID: string; + partnerID: string; + systemID?: string; +} + +export interface TwgAddIncomingRequest { + amount: string; + reserve_pub: string; + debit_account: string; +} + +interface CreateEbicsBankAccountRequest { + subscriber: { + hostID: string; + partnerID: string; + userID: string; + systemID?: string; + }; + // IBAN + iban: string; + // BIC + bic: string; + // human name + name: string; + currency: string; + label: string; +} + +export interface SimulateIncomingTransactionRequest { + debtorIban: string; + debtorBic: string; + debtorName: string; + + /** + * Subject / unstructured remittance info. + */ + subject: string; + + /** + * Decimal amount without currency. + */ + amount: string; +} + +/** + * The bundle aims at minimizing the amount of input + * data that is required to initialize a new user + Ebics + * connection. + */ +export class NexusUserBundle { + userReq: CreateNexusUserRequest; + connReq: CreateEbicsBankConnectionRequest; + anastasisReq: CreateAnastasisFacadeRequest; + twgReq: CreateTalerWireGatewayFacadeRequest; + twgTransferPermission: PostNexusPermissionRequest; + twgHistoryPermission: PostNexusPermissionRequest; + twgAddIncomingPermission: PostNexusPermissionRequest; + localAccountName: string; + remoteAccountName: string; + + constructor(salt: string, ebicsURL: string) { + this.userReq = { + username: `username-${salt}`, + password: `password-${salt}`, + }; + + this.connReq = { + name: `connection-${salt}`, + ebicsURL: ebicsURL, + hostID: `ebicshost,${salt}`, + partnerID: `ebicspartner,${salt}`, + userID: `ebicsuser,${salt}`, + }; + + this.twgReq = { + currency: "EUR", + name: `twg-${salt}`, + reserveTransferLevel: "report", + accountName: `local-account-${salt}`, + connectionName: `connection-${salt}`, + }; + this.anastasisReq = { + currency: "EUR", + name: `anastasis-${salt}`, + reserveTransferLevel: "report", + accountName: `local-account-${salt}`, + connectionName: `connection-${salt}`, + }; + this.remoteAccountName = `remote-account-${salt}`; + this.localAccountName = `local-account-${salt}`; + this.twgTransferPermission = { + action: "grant", + permission: { + subjectId: `username-${salt}`, + subjectType: "user", + resourceType: "facade", + resourceId: `twg-${salt}`, + permissionName: "facade.talerWireGateway.transfer", + }, + }; + this.twgHistoryPermission = { + action: "grant", + permission: { + subjectId: `username-${salt}`, + subjectType: "user", + resourceType: "facade", + resourceId: `twg-${salt}`, + permissionName: "facade.talerWireGateway.history", + }, + }; + } +} + +/** + * The bundle aims at minimizing the amount of input + * data that is required to initialize a new Sandbox + * customer, associating their bank account with a Ebics + * subscriber. + */ +export class SandboxUserBundle { + ebicsBankAccount: CreateEbicsBankAccountRequest; + constructor(salt: string) { + this.ebicsBankAccount = { + currency: "EUR", + bic: "BELADEBEXXX", + iban: getRandomIban("DE"), + label: `remote-account-${salt}`, + name: `Taler Exchange: ${salt}`, + subscriber: { + hostID: `ebicshost,${salt}`, + partnerID: `ebicspartner,${salt}`, + userID: `ebicsuser,${salt}`, + }, + }; + } +} + +export class LibeufinCli { + cliDetails: LibeufinCliDetails; + globalTestState: GlobalTestState; + + constructor(gc: GlobalTestState, cd: LibeufinCliDetails) { + this.globalTestState = gc; + this.cliDetails = cd; + } + + env(): any { + return { + ...process.env, + LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl, + LIBEUFIN_SANDBOX_USERNAME: "admin", + LIBEUFIN_SANDBOX_PASSWORD: "secret", + } + } + + async checkSandbox(): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-checksandbox", + "libeufin-cli sandbox check", + this.env() + ); + } + + async createEbicsHost(hostId: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createebicshost", + `libeufin-cli sandbox ebicshost create --host-id=${hostId}`, + this.env() + ); + console.log(stdout); + } + + async createEbicsSubscriber( + details: LibeufinEbicsSubscriberDetails, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createebicssubscriber", + "libeufin-cli sandbox ebicssubscriber create" + + ` --host-id=${details.hostId}` + + ` --partner-id=${details.partnerId}` + + ` --user-id=${details.userId}`, + this.env() + ); + console.log(stdout); + } + + async createEbicsBankAccount( + sd: LibeufinEbicsSubscriberDetails, + bankAccountDetails: LibeufinBankAccountDetails, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createebicsbankaccount", + "libeufin-cli sandbox ebicsbankaccount create" + + ` --currency=${bankAccountDetails.currency}` + + ` --iban=${bankAccountDetails.iban}` + + ` --bic=${bankAccountDetails.bic}` + + ` --person-name='${bankAccountDetails.personName}'` + + ` --account-name=${bankAccountDetails.accountName}` + + ` --ebics-host-id=${sd.hostId}` + + ` --ebics-partner-id=${sd.partnerId}` + + ` --ebics-user-id=${sd.userId}`, + this.env() + ); + console.log(stdout); + } + + async generateTransactions(accountName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-generatetransactions", + `libeufin-cli sandbox bankaccount generate-transactions ${accountName}`, + this.env() + ); + console.log(stdout); + } + + async showSandboxTransactions(accountName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-showsandboxtransactions", + `libeufin-cli sandbox bankaccount transactions ${accountName}`, + this.env() + ); + console.log(stdout); + } + + async createEbicsConnection( + connectionDetails: LibeufinEbicsConnectionDetails, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createebicsconnection", + `libeufin-cli connections new-ebics-connection` + + ` --ebics-url=${connectionDetails.ebicsUrl}` + + ` --host-id=${connectionDetails.subscriberDetails.hostId}` + + ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` + + ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` + + ` ${connectionDetails.connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async createBackupFile(details: LibeufinBackupFileDetails): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createbackupfile", + `libeufin-cli connections export-backup` + + ` --passphrase=${details.passphrase}` + + ` --output-file=${details.outputFile}` + + ` ${details.connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async createKeyLetter(details: LibeufinKeyLetterDetails): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createkeyletter", + `libeufin-cli connections get-key-letter` + + ` ${details.connectionName} ${details.outputFile}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async connect(connectionName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-connect", + `libeufin-cli connections connect ${connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async downloadBankAccounts(connectionName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-downloadbankaccounts", + `libeufin-cli connections download-bank-accounts ${connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async listOfferedBankAccounts(connectionName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-listofferedbankaccounts", + `libeufin-cli connections list-offered-bank-accounts ${connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async importBankAccount( + importDetails: LibeufinBankAccountImportDetails, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-importbankaccount", + "libeufin-cli connections import-bank-account" + + ` --offered-account-id=${importDetails.offeredBankAccountName}` + + ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` + + ` ${importDetails.connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async fetchTransactions(bankAccountName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-fetchtransactions", + `libeufin-cli accounts fetch-transactions ${bankAccountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async transactions(bankAccountName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-transactions", + `libeufin-cli accounts transactions ${bankAccountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async preparePayment(details: LibeufinPreparedPaymentDetails): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-preparepayment", + `libeufin-cli accounts prepare-payment` + + ` --creditor-iban=${details.creditorIban}` + + ` --creditor-bic=${details.creditorBic}` + + ` --creditor-name='${details.creditorName}'` + + ` --payment-subject='${details.subject}'` + + ` --payment-amount=${details.currency}:${details.amount}` + + ` ${details.nexusBankAccountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async submitPayment( + details: LibeufinPreparedPaymentDetails, + paymentUuid: string, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-submitpayment", + `libeufin-cli accounts submit-payment` + + ` --payment-uuid=${paymentUuid}` + + ` ${details.nexusBankAccountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async newAnastasisFacade(req: NewAnastasisFacadeReq): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-new-anastasis-facade", + `libeufin-cli facades new-anastasis-facade` + + ` --currency ${req.currency}` + + ` --facade-name ${req.facadeName}` + + ` ${req.connectionName} ${req.accountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + + async newTalerWireGatewayFacade(req: NewTalerWireGatewayReq): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-new-taler-wire-gateway-facade", + `libeufin-cli facades new-taler-wire-gateway-facade` + + ` --currency ${req.currency}` + + ` --facade-name ${req.facadeName}` + + ` ${req.connectionName} ${req.accountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } + + async listFacades(): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-facades-list", + `libeufin-cli facades list`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, + }, + ); + console.log(stdout); + } +} + +interface NewAnastasisFacadeReq { + facadeName: string; + connectionName: string; + accountName: string; + currency: string; +} + +interface NewTalerWireGatewayReq { + facadeName: string; + connectionName: string; + accountName: string; + currency: string; +} + +export namespace LibeufinSandboxApi { + + export async function rotateKeys( + libeufinSandboxService: LibeufinSandboxServiceInterface, + hostID: string, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL(`admin/ebics/hosts/${hostID}/rotate-keys`, baseUrl); + await axios.post(url.href, {}, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + export async function createEbicsHost( + libeufinSandboxService: LibeufinSandboxServiceInterface, + hostID: string, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL("admin/ebics/hosts", baseUrl); + await axios.post(url.href, { + hostID, + ebicsVersion: "2.5", + }, + { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + export async function createBankAccount( + libeufinSandboxService: LibeufinSandboxServiceInterface, + req: BankAccountInfo, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL(`admin/bank-accounts/${req.label}`, baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + export async function createEbicsSubscriber( + libeufinSandboxService: LibeufinSandboxServiceInterface, + req: CreateEbicsSubscriberRequest, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL("admin/ebics/subscribers", baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + export async function createEbicsBankAccount( + libeufinSandboxService: LibeufinSandboxServiceInterface, + req: CreateEbicsBankAccountRequest, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL("admin/ebics/bank-accounts", baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + export async function bookPayment2( + libeufinSandboxService: LibeufinSandboxService, + req: LibeufinSandboxAddIncomingRequest, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL("admin/payments", baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + export async function bookPayment( + libeufinSandboxService: LibeufinSandboxService, + creditorBundle: SandboxUserBundle, + debitorBundle: SandboxUserBundle, + subject: string, + amount: string, + currency: string, + ) { + let req: LibeufinSandboxAddIncomingRequest = { + creditorIban: creditorBundle.ebicsBankAccount.iban, + creditorBic: creditorBundle.ebicsBankAccount.bic, + creditorName: creditorBundle.ebicsBankAccount.name, + debtorIban: debitorBundle.ebicsBankAccount.iban, + debtorBic: debitorBundle.ebicsBankAccount.bic, + debtorName: debitorBundle.ebicsBankAccount.name, + subject: subject, + amount: amount, + currency: currency, + uid: getRandomString(), + direction: "CRDT", + }; + await bookPayment2(libeufinSandboxService, req); + } + + export async function simulateIncomingTransaction( + libeufinSandboxService: LibeufinSandboxServiceInterface, + accountLabel: string, + req: SimulateIncomingTransactionRequest, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL( + `admin/bank-accounts/${accountLabel}/simulate-incoming-transaction`, + baseUrl, + ); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + export async function getAccountTransactions( + libeufinSandboxService: LibeufinSandboxServiceInterface, + accountLabel: string, + ): Promise { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL( + `admin/bank-accounts/${accountLabel}/transactions`, + baseUrl, + ); + const res = await axios.get(url.href, { + auth: { + username: "admin", + password: "secret", + }, + }); + return res.data as SandboxAccountTransactions; + } + + export async function getCamt053( + libeufinSandboxService: LibeufinSandboxServiceInterface, + accountLabel: string, + ): Promise { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL("admin/payments/camt", baseUrl); + return await axios.post(url.href, { + bankaccount: accountLabel, + type: 53, + }, + { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + export async function getAccountInfoWithBalance( + libeufinSandboxService: LibeufinSandboxServiceInterface, + accountLabel: string, + ): Promise { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL( + `admin/bank-accounts/${accountLabel}`, + baseUrl, + ); + return await axios.get(url.href, { + auth: { + username: "admin", + password: "secret", + }, + }); + } +} + +export interface SandboxAccountTransactions { + payments: { + accountLabel: string; + creditorIban: string; + creditorBic?: string; + creditorName: string; + debtorIban: string; + debtorBic: string; + debtorName: string; + amount: string; + currency: string; + subject: string; + date: string; + creditDebitIndicator: "debit" | "credit"; + accountServicerReference: string; + }[]; +} + +export interface CreateEbicsBankConnectionRequest { + name: string; + ebicsURL: string; + hostID: string; + userID: string; + partnerID: string; + systemID?: string; +} + +export interface CreateAnastasisFacadeRequest { + name: string; + connectionName: string; + accountName: string; + currency: string; + reserveTransferLevel: "report" | "statement" | "notification"; +} + + +export interface CreateTalerWireGatewayFacadeRequest { + name: string; + connectionName: string; + accountName: string; + currency: string; + reserveTransferLevel: "report" | "statement" | "notification"; +} + +export interface UpdateNexusUserRequest { + newPassword: string; +} + +export interface NexusAuth { + auth: { + username: string; + password: string; + }; +} + +export interface CreateNexusUserRequest { + username: string; + password: string; +} + +export interface PostNexusTaskRequest { + name: string; + cronspec: string; + type: string; // fetch | submit + params: + | { + level: string; // report | statement | all + rangeType: string; // all | since-last | previous-days | latest + } + | {}; +} + +export interface PostNexusPermissionRequest { + action: "revoke" | "grant"; + permission: { + subjectType: string; + subjectId: string; + resourceType: string; + resourceId: string; + permissionName: string; + }; +} + +export namespace LibeufinNexusApi { + export async function getAllConnections( + nexus: LibeufinNexusServiceInterface, + ): Promise { + let url = new URL("bank-connections", nexus.baseUrl); + const res = await axios.get(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + return res; + } + + export async function deleteBankConnection( + libeufinNexusService: LibeufinNexusServiceInterface, + req: DeleteBankConnectionRequest, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("bank-connections/delete-connection", baseUrl); + return await axios.post(url.href, req, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function createEbicsBankConnection( + libeufinNexusService: LibeufinNexusServiceInterface, + req: CreateEbicsBankConnectionRequest, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("bank-connections", baseUrl); + await axios.post( + url.href, + { + source: "new", + type: "ebics", + name: req.name, + data: { + ebicsURL: req.ebicsURL, + hostID: req.hostID, + userID: req.userID, + partnerID: req.partnerID, + systemID: req.systemID, + }, + }, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function getBankAccount( + libeufinNexusService: LibeufinNexusServiceInterface, + accountName: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `bank-accounts/${accountName}`, + baseUrl, + ); + return await axios.get( + url.href, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + + export async function submitInitiatedPayment( + libeufinNexusService: LibeufinNexusServiceInterface, + accountName: string, + paymentId: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `bank-accounts/${accountName}/payment-initiations/${paymentId}/submit`, + baseUrl, + ); + await axios.post( + url.href, + {}, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function fetchAccounts( + libeufinNexusService: LibeufinNexusServiceInterface, + connectionName: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `bank-connections/${connectionName}/fetch-accounts`, + baseUrl, + ); + await axios.post( + url.href, + {}, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function importConnectionAccount( + libeufinNexusService: LibeufinNexusServiceInterface, + connectionName: string, + offeredAccountId: string, + nexusBankAccountId: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `bank-connections/${connectionName}/import-account`, + baseUrl, + ); + await axios.post( + url.href, + { + offeredAccountId, + nexusBankAccountId, + }, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function connectBankConnection( + libeufinNexusService: LibeufinNexusServiceInterface, + connectionName: string, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`bank-connections/${connectionName}/connect`, baseUrl); + await axios.post( + url.href, + {}, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function getPaymentInitiations( + libeufinNexusService: LibeufinNexusService, + accountName: string, + username: string = "admin", + password: string = "test", + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `/bank-accounts/${accountName}/payment-initiations`, + baseUrl, + ); + let response = await axios.get(url.href, { + auth: { + username: username, + password: password, + }, + }); + console.log( + `Payment initiations of: ${accountName}`, + JSON.stringify(response.data, null, 2), + ); + } + + export async function getConfig( + libeufinNexusService: LibeufinNexusService, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/config`, baseUrl); + let response = await axios.get(url.href); + } + + // Uses the Anastasis API to get a list of transactions. + export async function getAnastasisTransactions( + libeufinNexusService: LibeufinNexusService, + anastasisBaseUrl: string, + params: {}, // of the request: {delta: 5, ..} + username: string = "admin", + password: string = "test", + ): Promise { + let url = new URL("history/incoming", anastasisBaseUrl); + let response = await axios.get(url.href, { params: params, + auth: { + username: username, + password: password, + }, + }); + return response; + } + + // FIXME: this function should return some structured + // object that represents a history. + export async function getAccountTransactions( + libeufinNexusService: LibeufinNexusService, + accountName: string, + username: string = "admin", + password: string = "test", + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/bank-accounts/${accountName}/transactions`, baseUrl); + let response = await axios.get(url.href, { + auth: { + username: username, + password: password, + }, + }); + return response; + } + + export async function fetchTransactions( + libeufinNexusService: LibeufinNexusService, + accountName: string, + rangeType: string = "all", + level: string = "report", + username: string = "admin", + password: string = "test", + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `/bank-accounts/${accountName}/fetch-transactions`, + baseUrl, + ); + return await axios.post( + url.href, + { + rangeType: rangeType, + level: level, + }, + { + auth: { + username: username, + password: password, + }, + }, + ); + } + + export async function changePassword( + libeufinNexusService: LibeufinNexusServiceInterface, + username: string, + req: UpdateNexusUserRequest, + auth: NexusAuth, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/users/${username}/password`, baseUrl); + await axios.post(url.href, req, auth); + } + + export async function getUser( + libeufinNexusService: LibeufinNexusServiceInterface, + auth: NexusAuth, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/user`, baseUrl); + return await axios.get(url.href, auth); + } + + export async function createUser( + libeufinNexusService: LibeufinNexusServiceInterface, + req: CreateNexusUserRequest, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/users`, baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function getAllPermissions( + libeufinNexusService: LibeufinNexusServiceInterface, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/permissions`, baseUrl); + return await axios.get(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function postPermission( + libeufinNexusService: LibeufinNexusServiceInterface, + req: PostNexusPermissionRequest, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/permissions`, baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function getTasks( + libeufinNexusService: LibeufinNexusServiceInterface, + bankAccountName: string, + // When void, the request returns the list of all the + // tasks under this bank account. + taskName: string | void, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); + if (taskName) url = new URL(taskName, `${url}/`); + + // It's caller's responsibility to interpret the response. + return await axios.get(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function deleteTask( + libeufinNexusService: LibeufinNexusServiceInterface, + bankAccountName: string, + taskName: string, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `/bank-accounts/${bankAccountName}/schedule/${taskName}`, + baseUrl, + ); + await axios.delete(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function postTask( + libeufinNexusService: LibeufinNexusServiceInterface, + bankAccountName: string, + req: PostNexusTaskRequest, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); + return await axios.post(url.href, req, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function deleteFacade( + libeufinNexusService: LibeufinNexusServiceInterface, + facadeName: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`facades/${facadeName}`, baseUrl); + return await axios.delete(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function getAllFacades( + libeufinNexusService: LibeufinNexusServiceInterface, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("facades", baseUrl); + return await axios.get(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function createAnastasisFacade( + libeufinNexusService: LibeufinNexusServiceInterface, + req: CreateAnastasisFacadeRequest, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("facades", baseUrl); + await axios.post( + url.href, + { + name: req.name, + type: "anastasis", + config: { + bankAccount: req.accountName, + bankConnection: req.connectionName, + currency: req.currency, + reserveTransferLevel: req.reserveTransferLevel, + }, + }, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function createTwgFacade( + libeufinNexusService: LibeufinNexusServiceInterface, + req: CreateTalerWireGatewayFacadeRequest, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("facades", baseUrl); + await axios.post( + url.href, + { + name: req.name, + type: "taler-wire-gateway", + config: { + bankAccount: req.accountName, + bankConnection: req.connectionName, + currency: req.currency, + reserveTransferLevel: req.reserveTransferLevel, + }, + }, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function submitAllPaymentInitiations( + libeufinNexusService: LibeufinNexusServiceInterface, + accountId: string, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `/bank-accounts/${accountId}/submit-all-payment-initiations`, + baseUrl, + ); + await axios.post( + url.href, + {}, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } +} + +/** + * Launch Nexus and Sandbox AND creates users / facades / bank accounts / + * .. all that's required to start making banking traffic. + */ +export async function launchLibeufinServices( + t: GlobalTestState, + nexusUserBundle: NexusUserBundle[], + sandboxUserBundle: SandboxUserBundle[] = [], + withFacades: string[] = [], // takes only "twg" and/or "anastasis" +): Promise { + const db = await setupDb(t); + + const libeufinSandbox = await LibeufinSandboxService.create(t, { + httpPort: 5010, + databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, + }); + + await libeufinSandbox.start(); + await libeufinSandbox.pingUntilAvailable(); + + const libeufinNexus = await LibeufinNexusService.create(t, { + httpPort: 5011, + databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, + }); + + await libeufinNexus.start(); + await libeufinNexus.pingUntilAvailable(); + console.log("Libeufin services launched!"); + + for (let sb of sandboxUserBundle) { + await LibeufinSandboxApi.createEbicsHost( + libeufinSandbox, + sb.ebicsBankAccount.subscriber.hostID, + ); + await LibeufinSandboxApi.createEbicsSubscriber( + libeufinSandbox, + sb.ebicsBankAccount.subscriber, + ); + await LibeufinSandboxApi.createEbicsBankAccount( + libeufinSandbox, + sb.ebicsBankAccount, + ); + } + console.log("Sandbox user(s) / account(s) / subscriber(s): created"); + + for (let nb of nexusUserBundle) { + await LibeufinNexusApi.createEbicsBankConnection(libeufinNexus, nb.connReq); + await LibeufinNexusApi.connectBankConnection( + libeufinNexus, + nb.connReq.name, + ); + await LibeufinNexusApi.fetchAccounts(libeufinNexus, nb.connReq.name); + await LibeufinNexusApi.importConnectionAccount( + libeufinNexus, + nb.connReq.name, + nb.remoteAccountName, + nb.localAccountName, + ); + await LibeufinNexusApi.createUser(libeufinNexus, nb.userReq); + for (let facade of withFacades) { + switch (facade) { + case "twg": + await LibeufinNexusApi.createTwgFacade(libeufinNexus, nb.twgReq); + await LibeufinNexusApi.postPermission( + libeufinNexus, + nb.twgTransferPermission, + ); + await LibeufinNexusApi.postPermission( + libeufinNexus, + nb.twgHistoryPermission, + ); + break; + case "anastasis": + await LibeufinNexusApi.createAnastasisFacade(libeufinNexus, nb.anastasisReq); + } + } + } + console.log( + "Nexus user(s) / connection(s) / facade(s) / permission(s): created", + ); + + return { + commonDb: db, + libeufinNexus: libeufinNexus, + libeufinSandbox: libeufinSandbox, + }; +} + +/** + * Helper function that searches a payment among + * a list, as returned by Nexus. The key is just + * the payment subject. + */ +export function findNexusPayment( + key: string, + payments: LibeufinNexusTransactions, +): LibeufinNexusMoneyMovement | void { + let transactions = payments["transactions"]; + for (let i = 0; i < transactions.length; i++) { + let batches = transactions[i]["batches"]; + for (let y = 0; y < batches.length; y++) { + let movements = batches[y]["batchTransactions"]; + for (let z = 0; z < movements.length; z++) { + let movement = movements[z]; + if (movement["details"]["unstructuredRemittanceInformation"] == key) + return movement; + } + } + } +} diff --git a/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts b/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts new file mode 100644 index 000000000..a93a0ed25 --- /dev/null +++ b/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts @@ -0,0 +1,318 @@ +/* + 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 + */ + +/** + * Test harness for various GNU Taler components. + * Also provides a fault-injection proxy. + * + * @author Florian Dold + */ + +/** + * Imports. + */ +import { + ContractTerms, + Duration, + Codec, + buildCodecForObject, + codecForString, + codecOptional, + codecForConstString, + codecForBoolean, + codecForNumber, + codecForContractTerms, + codecForAny, + buildCodecForUnion, + AmountString, + Timestamp, + CoinPublicKeyString, + EddsaPublicKeyString, + codecForAmountString, +} from "@gnu-taler/taler-util"; + +export interface PostOrderRequest { + // The order must at least contain the minimal + // order detail, but can override all + order: Partial; + + // if set, the backend will then set the refund deadline to the current + // time plus the specified delay. + refund_delay?: Duration; + + // specifies the payment target preferred by the client. Can be used + // to select among the various (active) wire methods supported by the instance. + payment_target?: string; + + // FIXME: some fields are missing + + // Should a token for claiming the order be generated? + // False can make sense if the ORDER_ID is sufficiently + // high entropy to prevent adversarial claims (like it is + // if the backend auto-generates one). Default is 'true'. + create_token?: boolean; +} + +export type ClaimToken = string; + +export interface PostOrderResponse { + order_id: string; + token?: ClaimToken; +} + +export const codecForPostOrderResponse = (): Codec => + buildCodecForObject() + .property("order_id", codecForString()) + .property("token", codecOptional(codecForString())) + .build("PostOrderResponse"); + +export const codecForCheckPaymentPaidResponse = (): Codec => + buildCodecForObject() + .property("order_status_url", codecForString()) + .property("order_status", codecForConstString("paid")) + .property("refunded", codecForBoolean()) + .property("wired", codecForBoolean()) + .property("deposit_total", codecForAmountString()) + .property("exchange_ec", codecForNumber()) + .property("exchange_hc", codecForNumber()) + .property("refund_amount", codecForAmountString()) + .property("contract_terms", codecForContractTerms()) + // FIXME: specify + .property("wire_details", codecForAny()) + .property("wire_reports", codecForAny()) + .property("refund_details", codecForAny()) + .build("CheckPaymentPaidResponse"); + +export const codecForCheckPaymentUnpaidResponse = (): Codec => + buildCodecForObject() + .property("order_status", codecForConstString("unpaid")) + .property("taler_pay_uri", codecForString()) + .property("order_status_url", codecForString()) + .property("already_paid_order_id", codecOptional(codecForString())) + .build("CheckPaymentPaidResponse"); + +export const codecForCheckPaymentClaimedResponse = (): Codec => + buildCodecForObject() + .property("order_status", codecForConstString("claimed")) + .property("contract_terms", codecForContractTerms()) + .build("CheckPaymentClaimedResponse"); + +export const codecForMerchantOrderPrivateStatusResponse = (): Codec => + buildCodecForUnion() + .discriminateOn("order_status") + .alternative("paid", codecForCheckPaymentPaidResponse()) + .alternative("unpaid", codecForCheckPaymentUnpaidResponse()) + .alternative("claimed", codecForCheckPaymentClaimedResponse()) + .build("MerchantOrderPrivateStatusResponse"); + +export type MerchantOrderPrivateStatusResponse = + | CheckPaymentPaidResponse + | CheckPaymentUnpaidResponse + | CheckPaymentClaimedResponse; + +export interface CheckPaymentClaimedResponse { + // Wallet claimed the order, but didn't pay yet. + order_status: "claimed"; + + contract_terms: ContractTerms; +} + +export interface CheckPaymentPaidResponse { + // did the customer pay for this contract + order_status: "paid"; + + // Was the payment refunded (even partially) + refunded: boolean; + + // Did the exchange wire us the funds + wired: boolean; + + // Total amount the exchange deposited into our bank account + // for this contract, excluding fees. + deposit_total: AmountString; + + // Numeric error code indicating errors the exchange + // encountered tracking the wire transfer for this purchase (before + // we even got to specific coin issues). + // 0 if there were no issues. + exchange_ec: number; + + // HTTP status code returned by the exchange when we asked for + // information to track the wire transfer for this purchase. + // 0 if there were no issues. + exchange_hc: number; + + // Total amount that was refunded, 0 if refunded is false. + refund_amount: AmountString; + + // Contract terms + contract_terms: ContractTerms; + + // Ihe wire transfer status from the exchange for this order if available, otherwise empty array + wire_details: TransactionWireTransfer[]; + + // Reports about trouble obtaining wire transfer details, empty array if no trouble were encountered. + wire_reports: TransactionWireReport[]; + + // The refund details for this order. One entry per + // refunded coin; empty array if there are no refunds. + refund_details: RefundDetails[]; + + order_status_url: string; +} + +export interface CheckPaymentUnpaidResponse { + order_status: "unpaid"; + + // URI that the wallet must process to complete the payment. + taler_pay_uri: string; + + order_status_url: string; + + // Alternative order ID which was paid for already in the same session. + // Only given if the same product was purchased before in the same session. + already_paid_order_id?: string; + + // We do we NOT return the contract terms here because they may not + // exist in case the wallet did not yet claim them. +} + +export interface RefundDetails { + // Reason given for the refund + reason: string; + + // when was the refund approved + timestamp: Timestamp; + + // Total amount that was refunded (minus a refund fee). + amount: AmountString; +} + +export interface TransactionWireTransfer { + // Responsible exchange + exchange_url: string; + + // 32-byte wire transfer identifier + wtid: string; + + // execution time of the wire transfer + execution_time: Timestamp; + + // Total amount that has been wire transferred + // to the merchant + amount: AmountString; + + // Was this transfer confirmed by the merchant via the + // POST /transfers API, or is it merely claimed by the exchange? + confirmed: boolean; +} + +export interface TransactionWireReport { + // Numerical error code + code: number; + + // Human-readable error description + hint: string; + + // Numerical error code from the exchange. + exchange_ec: number; + + // HTTP status code received from the exchange. + exchange_hc: number; + + // Public key of the coin for which we got the exchange error. + coin_pub: CoinPublicKeyString; +} + +export interface TippingReserveStatus { + // Array of all known reserves (possibly empty!) + reserves: ReserveStatusEntry[]; +} + +export interface ReserveStatusEntry { + // Public key of the reserve + reserve_pub: string; + + // Timestamp when it was established + creation_time: Timestamp; + + // Timestamp when it expires + expiration_time: Timestamp; + + // Initial amount as per reserve creation call + merchant_initial_amount: AmountString; + + // Initial amount as per exchange, 0 if exchange did + // not confirm reserve creation yet. + exchange_initial_amount: AmountString; + + // Amount picked up so far. + pickup_amount: AmountString; + + // Amount approved for tips that exceeds the pickup_amount. + committed_amount: AmountString; + + // Is this reserve active (false if it was deleted but not purged) + active: boolean; +} + +export interface TipCreateConfirmation { + // Unique tip identifier for the tip that was created. + tip_id: string; + + // taler://tip URI for the tip + taler_tip_uri: string; + + // URL that will directly trigger processing + // the tip when the browser is redirected to it + tip_status_url: string; + + // when does the tip expire + tip_expiration: Timestamp; +} + +export interface TipCreateRequest { + // Amount that the customer should be tipped + amount: AmountString; + + // Justification for giving the tip + justification: string; + + // URL that the user should be directed to after tipping, + // will be included in the tip_token. + next_url: string; +} + +export interface MerchantInstancesResponse { + // List of instances that are present in the backend (see Instance) + instances: MerchantInstanceDetail[]; +} + +export interface MerchantInstanceDetail { + // Merchant name corresponding to this instance. + name: string; + + // Merchant instance this response is about ($INSTANCE) + id: string; + + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKeyString; + + // List of the payment targets supported by this instance. Clients can + // specify the desired payment target in /order requests. Note that + // front-ends do not have to support wallets selecting payment targets. + payment_targets: string[]; +} diff --git a/packages/taler-wallet-cli/src/harness/sync.ts b/packages/taler-wallet-cli/src/harness/sync.ts new file mode 100644 index 000000000..16be89eff --- /dev/null +++ b/packages/taler-wallet-cli/src/harness/sync.ts @@ -0,0 +1,118 @@ +/* + This file is part of GNU Taler + (C) 2021 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 { URL } from "@gnu-taler/taler-util"; +import * as fs from "fs"; +import * as util from "util"; +import { + GlobalTestState, + pingProc, + ProcessWrapper, +} from "../harness/harness.js"; +import { Configuration } from "@gnu-taler/taler-util"; + +const exec = util.promisify(require("child_process").exec); + +export interface SyncConfig { + /** + * Human-readable name used in the test harness logs. + */ + name: string; + + httpPort: number; + + /** + * Database connection string (only postgres is supported). + */ + database: string; + + annualFee: string; + + currency: string; + + uploadLimitMb: number; + + /** + * Fulfillment URL used for contract terms related to + * sync. + */ + fulfillmentUrl: string; + + paymentBackendUrl: string; +} + +function setSyncPaths(config: Configuration, home: string) { + config.setString("paths", "sync_home", home); + // We need to make sure that the path of taler_runtime_dir isn't too long, + // as it contains unix domain sockets (108 character limit). + const runDir = fs.mkdtempSync("/tmp/taler-test-"); + config.setString("paths", "sync_runtime_dir", runDir); + config.setString("paths", "sync_data_home", "$SYNC_HOME/.local/share/sync/"); + config.setString("paths", "sync_config_home", "$SYNC_HOME/.config/sync/"); + config.setString("paths", "sync_cache_home", "$SYNC_HOME/.config/sync/"); +} + +export class SyncService { + static async create( + gc: GlobalTestState, + sc: SyncConfig, + ): Promise { + const config = new Configuration(); + + const cfgFilename = gc.testDir + `/sync-${sc.name}.conf`; + setSyncPaths(config, gc.testDir + "/synchome"); + config.setString("taler", "currency", sc.currency); + config.setString("sync", "serve", "tcp"); + config.setString("sync", "port", `${sc.httpPort}`); + config.setString("sync", "db", "postgres"); + config.setString("syncdb-postgres", "config", sc.database); + config.setString("sync", "payment_backend_url", sc.paymentBackendUrl); + config.setString("sync", "upload_limit_mb", `${sc.uploadLimitMb}`); + config.write(cfgFilename); + + return new SyncService(gc, sc, cfgFilename); + } + + proc: ProcessWrapper | undefined; + + get baseUrl(): string { + return `http://localhost:${this.syncConfig.httpPort}/`; + } + + async start(): Promise { + await exec(`sync-dbinit -c "${this.configFilename}"`); + + this.proc = this.globalState.spawnService( + "sync-httpd", + ["-LDEBUG", "-c", this.configFilename], + `sync-${this.syncConfig.name}`, + ); + } + + async pingUntilAvailable(): Promise { + const url = new URL("config", this.baseUrl).href; + await pingProc(this.proc, url, "sync"); + } + + constructor( + private globalState: GlobalTestState, + private syncConfig: SyncConfig, + private configFilename: string, + ) {} +} diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index a5e129d92..142e98e7c 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -19,6 +19,7 @@ */ import os from "os"; import fs from "fs"; +import path from "path"; import { deepStrictEqual } from "assert"; // Polyfill for encoding which isn't present globally in older nodejs versions import { TextEncoder, TextDecoder } from "util"; @@ -56,6 +57,9 @@ import { Wallet, } from "@gnu-taler/taler-wallet-core"; import { lintExchangeDeployment } from "./lint.js"; +import { runBench1 } from "./bench1.js"; +import { runEnv1 } from "./env1.js"; +import { GlobalTestState, runTestWithState } from "./harness/harness.js"; // This module also serves as the entry point for the crypto // thread worker, and thus must expose these two handlers. @@ -634,6 +638,33 @@ const advancedCli = walletCli.subcommand("advancedArgs", "advanced", { "Subcommands for advanced operations (only use if you know what you're doing!).", }); +advancedCli + .subcommand("bench1", "bench1", { + help: "Run the 'bench1' benchmark", + }) + .requiredOption("configJson", ["--config-json"], clk.STRING) + .action(async (args) => { + let config: any; + try { + config = JSON.parse(args.bench1.configJson); + } catch (e) { + console.log("Could not parse config JSON"); + } + await runBench1(config); + }); + +advancedCli + .subcommand("env1", "env1", { + help: "Run a test environment for bench1", + }) + .action(async (args) => { + const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-")); + const testState = new GlobalTestState({ + testDir, + }); + await runTestWithState(testState, runEnv1, "env1", true); + }); + advancedCli .subcommand("withdrawFakebank", "withdraw-fakebank", { help: "Withdraw via a fakebank.", @@ -642,7 +673,7 @@ advancedCli help: "Base URL of the exchange to use", }) .requiredOption("amount", ["--amount"], clk.STRING, { - help: "Amount to withdraw (before fees)." + help: "Amount to withdraw (before fees).", }) .requiredOption("bank", ["--bank"], clk.STRING, { help: "Base URL of the Taler fakebank service.", diff --git a/packages/taler-wallet-cli/src/integrationtests/denomStructures.ts b/packages/taler-wallet-cli/src/integrationtests/denomStructures.ts deleted file mode 100644 index 5ab9aca00..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/denomStructures.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - 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 - */ - -export interface CoinConfig { - name: string; - value: string; - durationWithdraw: string; - durationSpend: string; - durationLegal: string; - feeWithdraw: string; - feeDeposit: string; - feeRefresh: string; - feeRefund: string; - rsaKeySize: number; -} - -const coinCommon = { - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, -}; - -export const coin_ct1 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_ct1`, - value: `${curr}:0.01`, - feeDeposit: `${curr}:0.00`, - feeRefresh: `${curr}:0.01`, - feeRefund: `${curr}:0.00`, - feeWithdraw: `${curr}:0.01`, -}); - -export const coin_ct10 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_ct10`, - value: `${curr}:0.10`, - feeDeposit: `${curr}:0.01`, - feeRefresh: `${curr}:0.01`, - feeRefund: `${curr}:0.00`, - feeWithdraw: `${curr}:0.01`, -}); - -export const coin_u1 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u1`, - value: `${curr}:1`, - feeDeposit: `${curr}:0.02`, - feeRefresh: `${curr}:0.02`, - feeRefund: `${curr}:0.02`, - feeWithdraw: `${curr}:0.02`, -}); - -export const coin_u2 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u2`, - value: `${curr}:2`, - feeDeposit: `${curr}:0.02`, - feeRefresh: `${curr}:0.02`, - feeRefund: `${curr}:0.02`, - feeWithdraw: `${curr}:0.02`, -}); - -export const coin_u4 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u4`, - value: `${curr}:4`, - feeDeposit: `${curr}:0.02`, - feeRefresh: `${curr}:0.02`, - feeRefund: `${curr}:0.02`, - feeWithdraw: `${curr}:0.02`, -}); - -export const coin_u8 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u8`, - value: `${curr}:8`, - feeDeposit: `${curr}:0.16`, - feeRefresh: `${curr}:0.16`, - feeRefund: `${curr}:0.16`, - feeWithdraw: `${curr}:0.16`, -}); - -const coin_u10 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u10`, - value: `${curr}:10`, - feeDeposit: `${curr}:0.2`, - feeRefresh: `${curr}:0.2`, - feeRefund: `${curr}:0.2`, - feeWithdraw: `${curr}:0.2`, -}); - -export const defaultCoinConfig = [ - coin_ct1, - coin_ct10, - coin_u1, - coin_u2, - coin_u4, - coin_u8, - coin_u10, -]; - -const coinCheapCommon = (curr: string) => ({ - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, - feeRefresh: `${curr}:0.2`, - feeRefund: `${curr}:0.2`, - feeWithdraw: `${curr}:0.2`, -}); - -export function makeNoFeeCoinConfig(curr: string): CoinConfig[] { - const cc: CoinConfig[] = []; - - for (let i = 0; i < 16; i++) { - const ct = 2 ** i; - - const unit = Math.floor(ct / 100); - const cent = ct % 100; - - cc.push({ - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, - name: `${curr}-u${i}`, - feeDeposit: `${curr}:0`, - feeRefresh: `${curr}:0`, - feeRefund: `${curr}:0`, - feeWithdraw: `${curr}:0`, - value: `${curr}:${unit}.${cent}`, - }); - } - - return cc; -} diff --git a/packages/taler-wallet-cli/src/integrationtests/faultInjection.ts b/packages/taler-wallet-cli/src/integrationtests/faultInjection.ts deleted file mode 100644 index 474482ec0..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/faultInjection.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - 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 - */ - -/** - * Fault injection proxy. - * - * @author Florian Dold - */ - -/** - * Imports - */ -import * as http from "http"; -import { URL } from "url"; -import { - GlobalTestState, - ExchangeService, - ExchangeServiceInterface, - MerchantServiceInterface, - MerchantService, -} from "./harness"; - -export interface FaultProxyConfig { - inboundPort: number; - targetPort: number; -} - -/** - * Fault injection context. Modified by fault injection functions. - */ -export interface FaultInjectionRequestContext { - requestUrl: string; - method: string; - requestHeaders: Record; - requestBody?: Buffer; - dropRequest: boolean; -} - -export interface FaultInjectionResponseContext { - request: FaultInjectionRequestContext; - statusCode: number; - responseHeaders: Record; - responseBody: Buffer | undefined; - dropResponse: boolean; -} - -export interface FaultSpec { - modifyRequest?: (ctx: FaultInjectionRequestContext) => Promise; - modifyResponse?: (ctx: FaultInjectionResponseContext) => Promise; -} - -export class FaultProxy { - constructor( - private globalTestState: GlobalTestState, - private faultProxyConfig: FaultProxyConfig, - ) {} - - private currentFaultSpecs: FaultSpec[] = []; - - start() { - const server = http.createServer((req, res) => { - const requestChunks: Buffer[] = []; - const requestUrl = `http://localhost:${this.faultProxyConfig.inboundPort}${req.url}`; - console.log("request for", new URL(requestUrl)); - req.on("data", (chunk) => { - requestChunks.push(chunk); - }); - req.on("end", async () => { - console.log("end of data"); - let requestBuffer: Buffer | undefined; - if (requestChunks.length > 0) { - requestBuffer = Buffer.concat(requestChunks); - } - console.log("full request body", requestBuffer); - - const faultReqContext: FaultInjectionRequestContext = { - dropRequest: false, - method: req.method!!, - requestHeaders: req.headers, - requestUrl, - requestBody: requestBuffer, - }; - - for (const faultSpec of this.currentFaultSpecs) { - if (faultSpec.modifyRequest) { - await faultSpec.modifyRequest(faultReqContext); - } - } - - if (faultReqContext.dropRequest) { - res.destroy(); - return; - } - - const faultedUrl = new URL(faultReqContext.requestUrl); - - const proxyRequest = http.request({ - method: faultReqContext.method, - host: "localhost", - port: this.faultProxyConfig.targetPort, - path: faultedUrl.pathname + faultedUrl.search, - headers: faultReqContext.requestHeaders, - }); - - console.log( - `proxying request to target path '${ - faultedUrl.pathname + faultedUrl.search - }'`, - ); - - if (faultReqContext.requestBody) { - proxyRequest.write(faultReqContext.requestBody); - } - proxyRequest.end(); - proxyRequest.on("response", (proxyResp) => { - console.log("gotten response from target", proxyResp.statusCode); - const respChunks: Buffer[] = []; - proxyResp.on("data", (proxyRespData) => { - respChunks.push(proxyRespData); - }); - proxyResp.on("end", async () => { - console.log("end of target response"); - let responseBuffer: Buffer | undefined; - if (respChunks.length > 0) { - responseBuffer = Buffer.concat(respChunks); - } - const faultRespContext: FaultInjectionResponseContext = { - request: faultReqContext, - dropResponse: false, - responseBody: responseBuffer, - responseHeaders: proxyResp.headers, - statusCode: proxyResp.statusCode!!, - }; - for (const faultSpec of this.currentFaultSpecs) { - const modResponse = faultSpec.modifyResponse; - if (modResponse) { - await modResponse(faultRespContext); - } - } - if (faultRespContext.dropResponse) { - req.destroy(); - return; - } - if (faultRespContext.responseBody) { - // We must accommodate for potentially changed content length - faultRespContext.responseHeaders[ - "content-length" - ] = `${faultRespContext.responseBody.byteLength}`; - } - console.log("writing response head"); - res.writeHead( - faultRespContext.statusCode, - http.STATUS_CODES[faultRespContext.statusCode], - faultRespContext.responseHeaders, - ); - if (faultRespContext.responseBody) { - res.write(faultRespContext.responseBody); - } - res.end(); - }); - }); - }); - }); - - server.listen(this.faultProxyConfig.inboundPort); - this.globalTestState.servers.push(server); - } - - addFault(f: FaultSpec) { - this.currentFaultSpecs.push(f); - } - - clearAllFaults() { - this.currentFaultSpecs = []; - } -} - -export class FaultInjectedExchangeService implements ExchangeServiceInterface { - baseUrl: string; - port: number; - faultProxy: FaultProxy; - - get name(): string { - return this.innerExchange.name; - } - - get masterPub(): string { - return this.innerExchange.masterPub; - } - - private innerExchange: ExchangeService; - - constructor( - t: GlobalTestState, - e: ExchangeService, - proxyInboundPort: number, - ) { - this.innerExchange = e; - this.faultProxy = new FaultProxy(t, { - inboundPort: proxyInboundPort, - targetPort: e.port, - }); - this.faultProxy.start(); - - const exchangeUrl = new URL(e.baseUrl); - exchangeUrl.port = `${proxyInboundPort}`; - this.baseUrl = exchangeUrl.href; - this.port = proxyInboundPort; - } -} - -export class FaultInjectedMerchantService implements MerchantServiceInterface { - baseUrl: string; - port: number; - faultProxy: FaultProxy; - - get name(): string { - return this.innerMerchant.name; - } - - private innerMerchant: MerchantService; - private inboundPort: number; - - constructor( - t: GlobalTestState, - m: MerchantService, - proxyInboundPort: number, - ) { - this.innerMerchant = m; - this.faultProxy = new FaultProxy(t, { - inboundPort: proxyInboundPort, - targetPort: m.port, - }); - this.faultProxy.start(); - this.inboundPort = proxyInboundPort; - } - - makeInstanceBaseUrl(instanceName?: string | undefined): string { - const url = new URL(this.innerMerchant.makeInstanceBaseUrl(instanceName)); - url.port = `${this.inboundPort}`; - return url.href; - } -} diff --git a/packages/taler-wallet-cli/src/integrationtests/harness.ts b/packages/taler-wallet-cli/src/integrationtests/harness.ts deleted file mode 100644 index 6644e567f..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/harness.ts +++ /dev/null @@ -1,1764 +0,0 @@ -/* - 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 - */ - -/** - * Test harness for various GNU Taler components. - * Also provides a fault-injection proxy. - * - * @author Florian Dold - */ - -/** - * Imports - */ -import * as util from "util"; -import * as fs from "fs"; -import * as path from "path"; -import * as http from "http"; -import { deepStrictEqual } from "assert"; -import { ChildProcess, spawn } from "child_process"; -import { URL } from "url"; -import axios, { AxiosError } from "axios"; -import { - codecForMerchantOrderPrivateStatusResponse, - codecForPostOrderResponse, - PostOrderRequest, - PostOrderResponse, - MerchantOrderPrivateStatusResponse, - TippingReserveStatus, - TipCreateConfirmation, - TipCreateRequest, - MerchantInstancesResponse, -} from "./merchantApiTypes"; -import { - openPromise, - OperationFailedError, - WalletCoreApiClient, -} from "@gnu-taler/taler-wallet-core"; -import { - AmountJson, - Amounts, - Configuration, - AmountString, - Codec, - buildCodecForObject, - codecForString, - Duration, - parsePaytoUri, - CoreApiResponse, - createEddsaKeyPair, - eddsaGetPublic, - EddsaKeyPair, - encodeCrock, - getRandomBytes, -} from "@gnu-taler/taler-util"; -import { CoinConfig } from "./denomStructures.js"; - -const exec = util.promisify(require("child_process").exec); - -export async function delayMs(ms: number): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(), ms); - }); -} - -export interface WithAuthorization { - Authorization?: string; -} - -interface WaitResult { - code: number | null; - signal: NodeJS.Signals | null; -} - -/** - * Run a shell command, return stdout. - */ -export async function sh( - t: GlobalTestState, - logName: string, - command: string, - env: { [index: string]: string | undefined } = process.env, -): Promise { - console.log("running command", command); - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const proc = spawn(command, { - stdio: ["inherit", "pipe", "pipe"], - shell: true, - env: env, - }); - proc.stdout.on("data", (x) => { - if (x instanceof Buffer) { - stdoutChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); - const stderrLog = fs.createWriteStream(stderrLogFileName, { - flags: "a", - }); - proc.stderr.pipe(stderrLog); - proc.on("exit", (code, signal) => { - console.log(`child process exited (${code} / ${signal})`); - if (code != 0) { - reject(Error(`Unexpected exit code ${code} for '${command}'`)); - return; - } - const b = Buffer.concat(stdoutChunks).toString("utf-8"); - resolve(b); - }); - proc.on("error", () => { - reject(Error("Child process had error")); - }); - }); -} - -function shellescape(args: string[]) { - const ret = args.map((s) => { - if (/[^A-Za-z0-9_\/:=-]/.test(s)) { - s = "'" + s.replace(/'/g, "'\\''") + "'"; - s = s.replace(/^(?:'')+/g, "").replace(/\\'''/g, "\\'"); - } - return s; - }); - return ret.join(" "); -} - -/** - * Run a shell command, return stdout. - * - * Log stderr to a log file. - */ -export async function runCommand( - t: GlobalTestState, - logName: string, - command: string, - args: string[], - env: { [index: string]: string | undefined } = process.env, -): Promise { - console.log("running command", shellescape([command, ...args])); - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const proc = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - shell: false, - env: env, - }); - proc.stdout.on("data", (x) => { - if (x instanceof Buffer) { - stdoutChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); - const stderrLog = fs.createWriteStream(stderrLogFileName, { - flags: "a", - }); - proc.stderr.pipe(stderrLog); - proc.on("exit", (code, signal) => { - console.log(`child process exited (${code} / ${signal})`); - if (code != 0) { - reject(Error(`Unexpected exit code ${code} for '${command}'`)); - return; - } - const b = Buffer.concat(stdoutChunks).toString("utf-8"); - resolve(b); - }); - proc.on("error", () => { - reject(Error("Child process had error")); - }); - }); -} - -export class ProcessWrapper { - private waitPromise: Promise; - constructor(public proc: ChildProcess) { - this.waitPromise = new Promise((resolve, reject) => { - proc.on("exit", (code, signal) => { - resolve({ code, signal }); - }); - proc.on("error", (err) => { - reject(err); - }); - }); - } - - wait(): Promise { - return this.waitPromise; - } -} - -export class GlobalTestParams { - testDir: string; -} - -export class GlobalTestState { - testDir: string; - procs: ProcessWrapper[]; - servers: http.Server[]; - inShutdown: boolean = false; - constructor(params: GlobalTestParams) { - this.testDir = params.testDir; - this.procs = []; - this.servers = []; - } - - async assertThrowsOperationErrorAsync( - block: () => Promise, - ): Promise { - try { - await block(); - } catch (e) { - if (e instanceof OperationFailedError) { - return e; - } - throw Error(`expected OperationFailedError to be thrown, but got ${e}`); - } - throw Error( - `expected OperationFailedError to be thrown, but block finished without throwing`, - ); - } - - async assertThrowsAsync(block: () => Promise): Promise { - try { - await block(); - } catch (e) { - return e; - } - throw Error( - `expected exception to be thrown, but block finished without throwing`, - ); - } - - assertAxiosError(e: any): asserts e is AxiosError { - if (!e.isAxiosError) { - throw Error("expected axios error"); - } - } - - assertTrue(b: boolean): asserts b { - if (!b) { - throw Error("test assertion failed"); - } - } - - assertDeepEqual(actual: any, expected: T): asserts actual is T { - deepStrictEqual(actual, expected); - } - - assertAmountEquals( - amtActual: string | AmountJson, - amtExpected: string | AmountJson, - ): void { - if (Amounts.cmp(amtActual, amtExpected) != 0) { - throw Error( - `test assertion failed: expected ${Amounts.stringify( - amtExpected, - )} but got ${Amounts.stringify(amtActual)}`, - ); - } - } - - assertAmountLeq(a: string | AmountJson, b: string | AmountJson): void { - if (Amounts.cmp(a, b) > 0) { - throw Error( - `test assertion failed: expected ${Amounts.stringify( - a, - )} to be less or equal (leq) than ${Amounts.stringify(b)}`, - ); - } - } - - shutdownSync(): void { - for (const s of this.servers) { - s.close(); - s.removeAllListeners(); - } - for (const p of this.procs) { - if (p.proc.exitCode == null) { - p.proc.kill("SIGTERM"); - } - } - } - - spawnService( - command: string, - args: string[], - logName: string, - env: { [index: string]: string | undefined } = process.env, - ): ProcessWrapper { - console.log( - `spawning process (${logName}): ${shellescape([command, ...args])}`, - ); - const proc = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - env: env, - }); - console.log(`spawned process (${logName}) with pid ${proc.pid}`); - proc.on("error", (err) => { - console.log(`could not start process (${command})`, err); - }); - proc.on("exit", (code, signal) => { - console.log(`process ${logName} exited`); - }); - const stderrLogFileName = this.testDir + `/${logName}-stderr.log`; - const stderrLog = fs.createWriteStream(stderrLogFileName, { - flags: "a", - }); - proc.stderr.pipe(stderrLog); - const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`; - const stdoutLog = fs.createWriteStream(stdoutLogFileName, { - flags: "a", - }); - proc.stdout.pipe(stdoutLog); - const procWrap = new ProcessWrapper(proc); - this.procs.push(procWrap); - return procWrap; - } - - async shutdown(): Promise { - if (this.inShutdown) { - return; - } - if (shouldLingerInTest()) { - console.log("refusing to shut down, lingering was requested"); - return; - } - this.inShutdown = true; - console.log("shutting down"); - for (const s of this.servers) { - s.close(); - s.removeAllListeners(); - } - for (const p of this.procs) { - if (p.proc.exitCode == null) { - console.log("killing process", p.proc.pid); - p.proc.kill("SIGTERM"); - await p.wait(); - } - } - } -} - -export function shouldLingerInTest(): boolean { - return !!process.env["TALER_TEST_LINGER"]; -} - -export interface TalerConfigSection { - options: Record; -} - -export interface TalerConfig { - sections: Record; -} - -export interface DbInfo { - /** - * Postgres connection string. - */ - connStr: string; - - dbname: string; -} - -export async function setupDb(gc: GlobalTestState): Promise { - const dbname = "taler-integrationtest"; - await exec(`dropdb "${dbname}" || true`); - await exec(`createdb "${dbname}"`); - return { - connStr: `postgres:///${dbname}`, - dbname, - }; -} - -export interface BankConfig { - currency: string; - httpPort: number; - database: string; - allowRegistrations: boolean; - maxDebt?: string; -} - -export interface FakeBankConfig { - currency: string; - httpPort: number; -} - -function setTalerPaths(config: Configuration, home: string) { - config.setString("paths", "taler_home", home); - // We need to make sure that the path of taler_runtime_dir isn't too long, - // as it contains unix domain sockets (108 character limit). - const runDir = fs.mkdtempSync("/tmp/taler-test-"); - config.setString("paths", "taler_runtime_dir", runDir); - config.setString( - "paths", - "taler_data_home", - "$TALER_HOME/.local/share/taler/", - ); - config.setString("paths", "taler_config_home", "$TALER_HOME/.config/taler/"); - config.setString("paths", "taler_cache_home", "$TALER_HOME/.config/taler/"); -} - -function setCoin(config: Configuration, c: CoinConfig) { - const s = `coin_${c.name}`; - config.setString(s, "value", c.value); - config.setString(s, "duration_withdraw", c.durationWithdraw); - config.setString(s, "duration_spend", c.durationSpend); - config.setString(s, "duration_legal", c.durationLegal); - config.setString(s, "fee_deposit", c.feeDeposit); - config.setString(s, "fee_withdraw", c.feeWithdraw); - config.setString(s, "fee_refresh", c.feeRefresh); - config.setString(s, "fee_refund", c.feeRefund); - config.setString(s, "rsa_keysize", `${c.rsaKeySize}`); -} - -/** - * Send an HTTP request until it succeeds or the - * process dies. - */ -export async function pingProc( - proc: ProcessWrapper | undefined, - url: string, - serviceName: string, -): Promise { - if (!proc || proc.proc.exitCode !== null) { - throw Error(`service process ${serviceName} not started, can't ping`); - } - while (true) { - try { - console.log(`pinging ${serviceName}`); - const resp = await axios.get(url); - console.log(`service ${serviceName} available`); - return; - } catch (e: any) { - console.log(`service ${serviceName} not ready:`, e.toString()); - await delayMs(1000); - } - if (!proc || proc.proc.exitCode !== null) { - throw Error(`service process ${serviceName} stopped unexpectedly`); - } - } -} - -export interface HarnessExchangeBankAccount { - accountName: string; - accountPassword: string; - accountPaytoUri: string; - wireGatewayApiBaseUrl: string; -} - -export interface BankServiceInterface { - readonly baseUrl: string; - readonly port: number; -} - -export enum CreditDebitIndicator { - Credit = "credit", - Debit = "debit", -} - -export interface BankAccountBalanceResponse { - balance: { - amount: AmountString; - credit_debit_indicator: CreditDebitIndicator; - }; -} - -export namespace BankAccessApi { - export async function getAccountBalance( - bank: BankServiceInterface, - bankUser: BankUser, - ): Promise { - const url = new URL(`accounts/${bankUser.username}`, bank.baseUrl); - const resp = await axios.get(url.href, { - auth: bankUser, - }); - return resp.data; - } - - export async function createWithdrawalOperation( - bank: BankServiceInterface, - bankUser: BankUser, - amount: string, - ): Promise { - const url = new URL( - `accounts/${bankUser.username}/withdrawals`, - bank.baseUrl, - ); - const resp = await axios.post( - url.href, - { - amount, - }, - { - auth: bankUser, - }, - ); - return codecForWithdrawalOperationInfo().decode(resp.data); - } -} - -export namespace BankApi { - export async function registerAccount( - bank: BankServiceInterface, - username: string, - password: string, - ): Promise { - const url = new URL("testing/register", bank.baseUrl); - await axios.post(url.href, { - username, - password, - }); - return { - password, - username, - accountPaytoUri: `payto://x-taler-bank/localhost/${username}`, - }; - } - - export async function createRandomBankUser( - bank: BankServiceInterface, - ): Promise { - const username = "user-" + encodeCrock(getRandomBytes(10)); - const password = "pw-" + encodeCrock(getRandomBytes(10)); - return await registerAccount(bank, username, password); - } - - export async function adminAddIncoming( - bank: BankServiceInterface, - params: { - exchangeBankAccount: HarnessExchangeBankAccount; - amount: string; - reservePub: string; - debitAccountPayto: string; - }, - ) { - const url = new URL( - `taler-wire-gateway/${params.exchangeBankAccount.accountName}/admin/add-incoming`, - bank.baseUrl, - ); - await axios.post( - url.href, - { - amount: params.amount, - reserve_pub: params.reservePub, - debit_account: params.debitAccountPayto, - }, - { - auth: { - username: params.exchangeBankAccount.accountName, - password: params.exchangeBankAccount.accountPassword, - }, - }, - ); - } - - export async function confirmWithdrawalOperation( - bank: BankServiceInterface, - bankUser: BankUser, - wopi: WithdrawalOperationInfo, - ): Promise { - const url = new URL( - `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`, - bank.baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: bankUser, - }, - ); - } - - export async function abortWithdrawalOperation( - bank: BankServiceInterface, - bankUser: BankUser, - wopi: WithdrawalOperationInfo, - ): Promise { - const url = new URL( - `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/abort`, - bank.baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: bankUser, - }, - ); - } -} - -export class BankService implements BankServiceInterface { - proc: ProcessWrapper | undefined; - - static fromExistingConfig(gc: GlobalTestState): BankService { - const cfgFilename = gc.testDir + "/bank.conf"; - console.log("reading bank config from", cfgFilename); - const config = Configuration.load(cfgFilename); - const bc: BankConfig = { - allowRegistrations: config - .getYesNo("bank", "allow_registrations") - .required(), - currency: config.getString("taler", "currency").required(), - database: config.getString("bank", "database").required(), - httpPort: config.getNumber("bank", "http_port").required(), - }; - return new BankService(gc, bc, cfgFilename); - } - - static async create( - gc: GlobalTestState, - bc: BankConfig, - ): Promise { - const config = new Configuration(); - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString("taler", "currency", bc.currency); - config.setString("bank", "database", bc.database); - config.setString("bank", "http_port", `${bc.httpPort}`); - config.setString("bank", "serve", "http"); - config.setString("bank", "max_debt_bank", `${bc.currency}:999999`); - config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`); - config.setString( - "bank", - "allow_registrations", - bc.allowRegistrations ? "yes" : "no", - ); - const cfgFilename = gc.testDir + "/bank.conf"; - config.write(cfgFilename); - - await sh( - gc, - "taler-bank-manage_django", - `taler-bank-manage -c '${cfgFilename}' django migrate`, - ); - await sh( - gc, - "taler-bank-manage_django", - `taler-bank-manage -c '${cfgFilename}' django provide_accounts`, - ); - - return new BankService(gc, bc, cfgFilename); - } - - setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) { - const config = Configuration.load(this.configFile); - config.setString("bank", "suggested_exchange", e.baseUrl); - config.setString("bank", "suggested_exchange_payto", exchangePayto); - } - - get baseUrl(): string { - return `http://localhost:${this.bankConfig.httpPort}/`; - } - - async createExchangeAccount( - accountName: string, - password: string, - ): Promise { - await sh( - this.globalTestState, - "taler-bank-manage_django", - `taler-bank-manage -c '${this.configFile}' django add_bank_account ${accountName}`, - ); - await sh( - this.globalTestState, - "taler-bank-manage_django", - `taler-bank-manage -c '${this.configFile}' django changepassword_unsafe ${accountName} ${password}`, - ); - await sh( - this.globalTestState, - "taler-bank-manage_django", - `taler-bank-manage -c '${this.configFile}' django top_up ${accountName} ${this.bankConfig.currency}:100000`, - ); - return { - accountName: accountName, - accountPassword: password, - accountPaytoUri: `payto://x-taler-bank/${accountName}`, - wireGatewayApiBaseUrl: `http://localhost:${this.bankConfig.httpPort}/taler-wire-gateway/${accountName}/`, - }; - } - - get port() { - return this.bankConfig.httpPort; - } - - private constructor( - private globalTestState: GlobalTestState, - private bankConfig: BankConfig, - private configFile: string, - ) {} - - async start(): Promise { - this.proc = this.globalTestState.spawnService( - "taler-bank-manage", - ["-c", this.configFile, "serve"], - "bank", - ); - } - - async pingUntilAvailable(): Promise { - const url = `http://localhost:${this.bankConfig.httpPort}/config`; - await pingProc(this.proc, url, "bank"); - } -} - -export class FakeBankService { - proc: ProcessWrapper | undefined; - - static fromExistingConfig(gc: GlobalTestState): FakeBankService { - const cfgFilename = gc.testDir + "/bank.conf"; - console.log("reading fakebank config from", cfgFilename); - const config = Configuration.load(cfgFilename); - const bc: FakeBankConfig = { - currency: config.getString("taler", "currency").required(), - httpPort: config.getNumber("bank", "http_port").required(), - }; - return new FakeBankService(gc, bc, cfgFilename); - } - - static async create( - gc: GlobalTestState, - bc: FakeBankConfig, - ): Promise { - const config = new Configuration(); - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString("taler", "currency", bc.currency); - config.setString("bank", "http_port", `${bc.httpPort}`); - const cfgFilename = gc.testDir + "/bank.conf"; - config.write(cfgFilename); - return new FakeBankService(gc, bc, cfgFilename); - } - - get baseUrl(): string { - return `http://localhost:${this.bankConfig.httpPort}/`; - } - - get port() { - return this.bankConfig.httpPort; - } - - private constructor( - private globalTestState: GlobalTestState, - private bankConfig: FakeBankConfig, - private configFile: string, - ) {} - - async start(): Promise { - this.proc = this.globalTestState.spawnService( - "taler-fakebank-run", - ["-c", this.configFile], - "fakebank", - ); - } - - async pingUntilAvailable(): Promise { - // Fakebank doesn't have "/config", so we ping just "/". - const url = `http://localhost:${this.bankConfig.httpPort}/`; - await pingProc(this.proc, url, "bank"); - } -} - -export interface BankUser { - username: string; - password: string; - accountPaytoUri: string; -} - -export interface WithdrawalOperationInfo { - withdrawal_id: string; - taler_withdraw_uri: string; -} - -const codecForWithdrawalOperationInfo = (): Codec => - buildCodecForObject() - .property("withdrawal_id", codecForString()) - .property("taler_withdraw_uri", codecForString()) - .build("WithdrawalOperationInfo"); - -export interface ExchangeConfig { - name: string; - currency: string; - roundUnit?: string; - httpPort: number; - database: string; -} - -export interface ExchangeServiceInterface { - readonly baseUrl: string; - readonly port: number; - readonly name: string; - readonly masterPub: string; -} - -export class ExchangeService implements ExchangeServiceInterface { - static fromExistingConfig(gc: GlobalTestState, exchangeName: string) { - const cfgFilename = gc.testDir + `/exchange-${exchangeName}.conf`; - const config = Configuration.load(cfgFilename); - const ec: ExchangeConfig = { - currency: config.getString("taler", "currency").required(), - database: config.getString("exchangedb-postgres", "config").required(), - httpPort: config.getNumber("exchange", "port").required(), - name: exchangeName, - roundUnit: config.getString("taler", "currency_round_unit").required(), - }; - const privFile = config.getPath("exchange", "master_priv_file").required(); - const eddsaPriv = fs.readFileSync(privFile); - const keyPair: EddsaKeyPair = { - eddsaPriv, - eddsaPub: eddsaGetPublic(eddsaPriv), - }; - return new ExchangeService(gc, ec, cfgFilename, keyPair); - } - - private currentTimetravel: Duration | undefined; - - setTimetravel(t: Duration | undefined): void { - if (this.isRunning()) { - throw Error("can't set time travel while the exchange is running"); - } - this.currentTimetravel = t; - } - - private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { - // Convert to microseconds - return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; - } - return undefined; - } - - /** - * Return an empty array if no time travel is set, - * and an array with the time travel command line argument - * otherwise. - */ - private get timetravelArgArr(): string[] { - const tta = this.timetravelArg; - if (tta) { - return [tta]; - } - return []; - } - - async runWirewatchOnce() { - await runCommand( - this.globalState, - `exchange-${this.name}-wirewatch-once`, - "taler-exchange-wirewatch", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); - } - - async runAggregatorOnce() { - await runCommand( - this.globalState, - `exchange-${this.name}-aggregator-once`, - "taler-exchange-aggregator", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); - } - - async runTransferOnce() { - await runCommand( - this.globalState, - `exchange-${this.name}-transfer-once`, - "taler-exchange-transfer", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); - } - - changeConfig(f: (config: Configuration) => void) { - const config = Configuration.load(this.configFilename); - f(config); - config.write(this.configFilename); - } - - static create(gc: GlobalTestState, e: ExchangeConfig) { - const config = new Configuration(); - config.setString("taler", "currency", e.currency); - config.setString( - "taler", - "currency_round_unit", - e.roundUnit ?? `${e.currency}:0.01`, - ); - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString( - "exchange", - "revocation_dir", - "${TALER_DATA_HOME}/exchange/revocations", - ); - config.setString("exchange", "max_keys_caching", "forever"); - config.setString("exchange", "db", "postgres"); - config.setString( - "exchange-offline", - "master_priv_file", - "${TALER_DATA_HOME}/exchange/offline-keys/master.priv", - ); - config.setString("exchange", "serve", "tcp"); - config.setString("exchange", "port", `${e.httpPort}`); - - config.setString("exchangedb-postgres", "config", e.database); - - config.setString("taler-exchange-secmod-eddsa", "lookahead_sign", "20 s"); - config.setString("taler-exchange-secmod-rsa", "lookahead_sign", "20 s"); - - const exchangeMasterKey = createEddsaKeyPair(); - - config.setString( - "exchange", - "master_public_key", - encodeCrock(exchangeMasterKey.eddsaPub), - ); - - const masterPrivFile = config - .getPath("exchange-offline", "master_priv_file") - .required(); - - fs.mkdirSync(path.dirname(masterPrivFile), { recursive: true }); - - fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv)); - - const cfgFilename = gc.testDir + `/exchange-${e.name}.conf`; - config.write(cfgFilename); - return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey); - } - - addOfferedCoins(offeredCoins: ((curr: string) => CoinConfig)[]) { - const config = Configuration.load(this.configFilename); - offeredCoins.forEach((cc) => - setCoin(config, cc(this.exchangeConfig.currency)), - ); - config.write(this.configFilename); - } - - addCoinConfigList(ccs: CoinConfig[]) { - const config = Configuration.load(this.configFilename); - ccs.forEach((cc) => setCoin(config, cc)); - config.write(this.configFilename); - } - - get masterPub() { - return encodeCrock(this.keyPair.eddsaPub); - } - - get port() { - return this.exchangeConfig.httpPort; - } - - async addBankAccount( - localName: string, - exchangeBankAccount: HarnessExchangeBankAccount, - ): Promise { - const config = Configuration.load(this.configFilename); - config.setString( - `exchange-account-${localName}`, - "wire_response", - `\${TALER_DATA_HOME}/exchange/account-${localName}.json`, - ); - config.setString( - `exchange-account-${localName}`, - "payto_uri", - exchangeBankAccount.accountPaytoUri, - ); - config.setString(`exchange-account-${localName}`, "enable_credit", "yes"); - config.setString(`exchange-account-${localName}`, "enable_debit", "yes"); - config.setString( - `exchange-accountcredentials-${localName}`, - "wire_gateway_url", - exchangeBankAccount.wireGatewayApiBaseUrl, - ); - config.setString( - `exchange-accountcredentials-${localName}`, - "wire_gateway_auth_method", - "basic", - ); - config.setString( - `exchange-accountcredentials-${localName}`, - "username", - exchangeBankAccount.accountName, - ); - config.setString( - `exchange-accountcredentials-${localName}`, - "password", - exchangeBankAccount.accountPassword, - ); - config.write(this.configFilename); - } - - exchangeHttpProc: ProcessWrapper | undefined; - exchangeWirewatchProc: ProcessWrapper | undefined; - - helperCryptoRsaProc: ProcessWrapper | undefined; - helperCryptoEddsaProc: ProcessWrapper | undefined; - - constructor( - private globalState: GlobalTestState, - private exchangeConfig: ExchangeConfig, - private configFilename: string, - private keyPair: EddsaKeyPair, - ) {} - - get name() { - return this.exchangeConfig.name; - } - - get baseUrl() { - return `http://localhost:${this.exchangeConfig.httpPort}/`; - } - - isRunning(): boolean { - return !!this.exchangeWirewatchProc || !!this.exchangeHttpProc; - } - - async stop(): Promise { - const wirewatch = this.exchangeWirewatchProc; - if (wirewatch) { - wirewatch.proc.kill("SIGTERM"); - await wirewatch.wait(); - this.exchangeWirewatchProc = undefined; - } - const httpd = this.exchangeHttpProc; - if (httpd) { - httpd.proc.kill("SIGTERM"); - await httpd.wait(); - this.exchangeHttpProc = undefined; - } - const cryptoRsa = this.helperCryptoRsaProc; - if (cryptoRsa) { - cryptoRsa.proc.kill("SIGTERM"); - await cryptoRsa.wait(); - this.helperCryptoRsaProc = undefined; - } - const cryptoEddsa = this.helperCryptoEddsaProc; - if (cryptoEddsa) { - cryptoEddsa.proc.kill("SIGTERM"); - await cryptoEddsa.wait(); - this.helperCryptoRsaProc = undefined; - } - } - - /** - * Update keys signing the keys generated by the security module - * with the offline signing key. - */ - async keyup(): Promise { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - ["-c", this.configFilename, "download", "sign", "upload"], - ); - - const accounts: string[] = []; - const accountTargetTypes: Set = new Set(); - - const config = Configuration.load(this.configFilename); - for (const sectionName of config.getSectionNames()) { - if (sectionName.startsWith("EXCHANGE-ACCOUNT-")) { - const paytoUri = config.getString(sectionName, "payto_uri").required(); - const p = parsePaytoUri(paytoUri); - if (!p) { - throw Error(`invalid payto uri in exchange config: ${paytoUri}`); - } - accountTargetTypes.add(p?.targetType); - accounts.push(paytoUri); - } - } - - console.log("configuring bank accounts", accounts); - - for (const acc of accounts) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - ["-c", this.configFilename, "enable-account", acc, "upload"], - ); - } - - const year = new Date().getFullYear(); - for (const accTargetType of accountTargetTypes.values()) { - for (let i = year; i < year + 5; i++) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - "wire-fee", - `${i}`, - accTargetType, - `${this.exchangeConfig.currency}:0.01`, - `${this.exchangeConfig.currency}:0.01`, - "upload", - ], - ); - } - } - } - - async revokeDenomination(denomPubHash: string) { - if (!this.isRunning()) { - throw Error("exchange must be running when revoking denominations"); - } - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - "revoke-denomination", - denomPubHash, - "upload", - ], - ); - } - - async purgeSecmodKeys(): Promise { - const cfg = Configuration.load(this.configFilename); - const rsaKeydir = cfg - .getPath("taler-exchange-secmod-rsa", "KEY_DIR") - .required(); - const eddsaKeydir = cfg - .getPath("taler-exchange-secmod-eddsa", "KEY_DIR") - .required(); - // Be *VERY* careful when changing this, or you will accidentally delete user data. - await sh(this.globalState, "rm-secmod-keys", `rm -rf ${rsaKeydir}/COIN_*`); - await sh(this.globalState, "rm-secmod-keys", `rm ${eddsaKeydir}/*`); - } - - async purgeDatabase(): Promise { - await sh( - this.globalState, - "exchange-dbinit", - `taler-exchange-dbinit -r -c "${this.configFilename}"`, - ); - } - - async start(): Promise { - if (this.isRunning()) { - throw Error("exchange is already running"); - } - await sh( - this.globalState, - "exchange-dbinit", - `taler-exchange-dbinit -c "${this.configFilename}"`, - ); - - this.helperCryptoEddsaProc = this.globalState.spawnService( - "taler-exchange-secmod-eddsa", - ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], - `exchange-crypto-eddsa-${this.name}`, - ); - - this.helperCryptoRsaProc = this.globalState.spawnService( - "taler-exchange-secmod-rsa", - ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], - `exchange-crypto-rsa-${this.name}`, - ); - - this.exchangeWirewatchProc = this.globalState.spawnService( - "taler-exchange-wirewatch", - ["-c", this.configFilename, ...this.timetravelArgArr], - `exchange-wirewatch-${this.name}`, - ); - - this.exchangeHttpProc = this.globalState.spawnService( - "taler-exchange-httpd", - ["-c", this.configFilename, ...this.timetravelArgArr], - `exchange-httpd-${this.name}`, - ); - - await this.pingUntilAvailable(); - await this.keyup(); - } - - async pingUntilAvailable(): Promise { - // We request /management/keys, since /keys can block - // when we didn't do the key setup yet. - const url = `http://localhost:${this.exchangeConfig.httpPort}/management/keys`; - await pingProc(this.exchangeHttpProc, url, `exchange (${this.name})`); - } -} - -export interface MerchantConfig { - name: string; - currency: string; - httpPort: number; - database: string; -} - -export interface PrivateOrderStatusQuery { - instance?: string; - orderId: string; - sessionId?: string; -} - -export interface MerchantServiceInterface { - makeInstanceBaseUrl(instanceName?: string): string; - readonly port: number; - readonly name: string; -} - -export class MerchantApiClient { - constructor( - private baseUrl: string, - public readonly auth: MerchantAuthConfiguration, - ) {} - - async changeAuth(auth: MerchantAuthConfiguration): Promise { - const url = new URL("private/auth", this.baseUrl); - await axios.post(url.href, auth, { - headers: this.makeAuthHeader(), - }); - } - - async deleteInstance(instanceId: string) { - const url = new URL(`management/instances/${instanceId}`, this.baseUrl); - await axios.delete(url.href, { - headers: this.makeAuthHeader(), - }); - } - - async createInstance(req: MerchantInstanceConfig): Promise { - const url = new URL("management/instances", this.baseUrl); - await axios.post(url.href, req, { - headers: this.makeAuthHeader(), - }); - } - - async getInstances(): Promise { - const url = new URL("management/instances", this.baseUrl); - const resp = await axios.get(url.href, { - headers: this.makeAuthHeader(), - }); - return resp.data; - } - - async getInstanceFullDetails(instanceId: string): Promise { - const url = new URL(`management/instances/${instanceId}`, this.baseUrl); - try { - const resp = await axios.get(url.href, { - headers: this.makeAuthHeader(), - }); - return resp.data; - } catch (e) { - throw e; - } - } - - makeAuthHeader(): Record { - switch (this.auth.method) { - case "external": - return {}; - case "token": - return { - Authorization: `Bearer ${this.auth.token}`, - }; - } - } -} - -/** - * FIXME: This should be deprecated in favor of MerchantApiClient - */ -export namespace MerchantPrivateApi { - export async function createOrder( - merchantService: MerchantServiceInterface, - instanceName: string, - req: PostOrderRequest, - withAuthorization: WithAuthorization = {}, - ): Promise { - const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); - let url = new URL("private/orders", baseUrl); - const resp = await axios.post(url.href, req, { - headers: withAuthorization, - }); - return codecForPostOrderResponse().decode(resp.data); - } - - export async function queryPrivateOrderStatus( - merchantService: MerchantServiceInterface, - query: PrivateOrderStatusQuery, - withAuthorization: WithAuthorization = {}, - ): Promise { - const reqUrl = new URL( - `private/orders/${query.orderId}`, - merchantService.makeInstanceBaseUrl(query.instance), - ); - if (query.sessionId) { - reqUrl.searchParams.set("session_id", query.sessionId); - } - const resp = await axios.get(reqUrl.href, { headers: withAuthorization }); - return codecForMerchantOrderPrivateStatusResponse().decode(resp.data); - } - - export async function giveRefund( - merchantService: MerchantServiceInterface, - r: { - instance: string; - orderId: string; - amount: string; - justification: string; - }, - ): Promise<{ talerRefundUri: string }> { - const reqUrl = new URL( - `private/orders/${r.orderId}/refund`, - merchantService.makeInstanceBaseUrl(r.instance), - ); - const resp = await axios.post(reqUrl.href, { - refund: r.amount, - reason: r.justification, - }); - return { - talerRefundUri: resp.data.taler_refund_uri, - }; - } - - export async function createTippingReserve( - merchantService: MerchantServiceInterface, - instance: string, - req: CreateMerchantTippingReserveRequest, - ): Promise { - const reqUrl = new URL( - `private/reserves`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await axios.post(reqUrl.href, req); - // FIXME: validate - return resp.data; - } - - export async function queryTippingReserves( - merchantService: MerchantServiceInterface, - instance: string, - ): Promise { - const reqUrl = new URL( - `private/reserves`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await axios.get(reqUrl.href); - // FIXME: validate - return resp.data; - } - - export async function giveTip( - merchantService: MerchantServiceInterface, - instance: string, - req: TipCreateRequest, - ): Promise { - const reqUrl = new URL( - `private/tips`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await axios.post(reqUrl.href, req); - // FIXME: validate - return resp.data; - } -} - -export interface CreateMerchantTippingReserveRequest { - // Amount that the merchant promises to put into the reserve - initial_balance: AmountString; - - // Exchange the merchant intends to use for tipping - exchange_url: string; - - // Desired wire method, for example "iban" or "x-taler-bank" - wire_method: string; -} - -export interface CreateMerchantTippingReserveConfirmation { - // Public key identifying the reserve - reserve_pub: string; - - // Wire account of the exchange where to transfer the funds - payto_uri: string; -} - -export class MerchantService implements MerchantServiceInterface { - static fromExistingConfig(gc: GlobalTestState, name: string) { - const cfgFilename = gc.testDir + `/merchant-${name}.conf`; - const config = Configuration.load(cfgFilename); - const mc: MerchantConfig = { - currency: config.getString("taler", "currency").required(), - database: config.getString("merchantdb-postgres", "config").required(), - httpPort: config.getNumber("merchant", "port").required(), - name, - }; - return new MerchantService(gc, mc, cfgFilename); - } - - proc: ProcessWrapper | undefined; - - constructor( - private globalState: GlobalTestState, - private merchantConfig: MerchantConfig, - private configFilename: string, - ) {} - - private currentTimetravel: Duration | undefined; - - private isRunning(): boolean { - return !!this.proc; - } - - setTimetravel(t: Duration | undefined): void { - if (this.isRunning()) { - throw Error("can't set time travel while the exchange is running"); - } - this.currentTimetravel = t; - } - - private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { - // Convert to microseconds - return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; - } - return undefined; - } - - /** - * Return an empty array if no time travel is set, - * and an array with the time travel command line argument - * otherwise. - */ - private get timetravelArgArr(): string[] { - const tta = this.timetravelArg; - if (tta) { - return [tta]; - } - return []; - } - - get port(): number { - return this.merchantConfig.httpPort; - } - - get name(): string { - return this.merchantConfig.name; - } - - async stop(): Promise { - const httpd = this.proc; - if (httpd) { - httpd.proc.kill("SIGTERM"); - await httpd.wait(); - this.proc = undefined; - } - } - - async start(): Promise { - await exec(`taler-merchant-dbinit -c "${this.configFilename}"`); - - this.proc = this.globalState.spawnService( - "taler-merchant-httpd", - ["-LDEBUG", "-c", this.configFilename, ...this.timetravelArgArr], - `merchant-${this.merchantConfig.name}`, - ); - } - - static async create( - gc: GlobalTestState, - mc: MerchantConfig, - ): Promise { - const config = new Configuration(); - config.setString("taler", "currency", mc.currency); - - const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`; - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString("merchant", "serve", "tcp"); - config.setString("merchant", "port", `${mc.httpPort}`); - config.setString( - "merchant", - "keyfile", - "${TALER_DATA_HOME}/merchant/merchant.priv", - ); - config.setString("merchantdb-postgres", "config", mc.database); - config.write(cfgFilename); - - return new MerchantService(gc, mc, cfgFilename); - } - - addExchange(e: ExchangeServiceInterface): void { - const config = Configuration.load(this.configFilename); - config.setString( - `merchant-exchange-${e.name}`, - "exchange_base_url", - e.baseUrl, - ); - config.setString( - `merchant-exchange-${e.name}`, - "currency", - this.merchantConfig.currency, - ); - config.setString(`merchant-exchange-${e.name}`, "master_key", e.masterPub); - config.write(this.configFilename); - } - - async addDefaultInstance(): Promise { - return await this.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - auth: { - method: "external", - }, - }); - } - - async addInstance( - instanceConfig: PartialMerchantInstanceConfig, - ): Promise { - if (!this.proc) { - throw Error("merchant must be running to add instance"); - } - console.log("adding instance"); - const url = `http://localhost:${this.merchantConfig.httpPort}/management/instances`; - const auth = instanceConfig.auth ?? { method: "external" }; - await axios.post(url, { - auth, - payto_uris: instanceConfig.paytoUris, - id: instanceConfig.id, - name: instanceConfig.name, - address: instanceConfig.address ?? {}, - jurisdiction: instanceConfig.jurisdiction ?? {}, - default_max_wire_fee: - instanceConfig.defaultMaxWireFee ?? - `${this.merchantConfig.currency}:1.0`, - default_wire_fee_amortization: - instanceConfig.defaultWireFeeAmortization ?? 3, - default_max_deposit_fee: - instanceConfig.defaultMaxDepositFee ?? - `${this.merchantConfig.currency}:1.0`, - default_wire_transfer_delay: instanceConfig.defaultWireTransferDelay ?? { - d_ms: "forever", - }, - default_pay_delay: instanceConfig.defaultPayDelay ?? { d_ms: "forever" }, - }); - } - - makeInstanceBaseUrl(instanceName?: string): string { - if (instanceName === undefined || instanceName === "default") { - return `http://localhost:${this.merchantConfig.httpPort}/`; - } else { - return `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/`; - } - } - - async pingUntilAvailable(): Promise { - const url = `http://localhost:${this.merchantConfig.httpPort}/config`; - await pingProc(this.proc, url, `merchant (${this.merchantConfig.name})`); - } -} - -export interface MerchantAuthConfiguration { - method: "external" | "token"; - token?: string; -} - -export interface PartialMerchantInstanceConfig { - auth?: MerchantAuthConfiguration; - id: string; - name: string; - paytoUris: string[]; - address?: unknown; - jurisdiction?: unknown; - defaultMaxWireFee?: string; - defaultMaxDepositFee?: string; - defaultWireFeeAmortization?: number; - defaultWireTransferDelay?: Duration; - defaultPayDelay?: Duration; -} - -export interface MerchantInstanceConfig { - auth: MerchantAuthConfiguration; - id: string; - name: string; - payto_uris: string[]; - address: unknown; - jurisdiction: unknown; - default_max_wire_fee: string; - default_max_deposit_fee: string; - default_wire_fee_amortization: number; - default_wire_transfer_delay: Duration; - default_pay_delay: Duration; -} - -type TestStatus = "pass" | "fail" | "skip"; - -export interface TestRunResult { - /** - * Name of the test. - */ - name: string; - - /** - * How long did the test run? - */ - timeSec: number; - - status: TestStatus; - - reason?: string; -} - -export async function runTestWithState( - gc: GlobalTestState, - testMain: (t: GlobalTestState) => Promise, - testName: string, -): Promise { - const startMs = new Date().getTime(); - - const p = openPromise(); - let status: TestStatus; - - const handleSignal = (s: string) => { - console.warn( - `**** received fatal process event, terminating test ${testName}`, - ); - gc.shutdownSync(); - process.exit(1); - }; - - process.on("SIGINT", handleSignal); - process.on("SIGTERM", handleSignal); - process.on("unhandledRejection", handleSignal); - process.on("uncaughtException", handleSignal); - - try { - console.log("running test in directory", gc.testDir); - await Promise.race([testMain(gc), p.promise]); - status = "pass"; - } catch (e) { - console.error("FATAL: test failed with exception", e); - status = "fail"; - } finally { - await gc.shutdown(); - } - const afterMs = new Date().getTime(); - return { - name: testName, - timeSec: (afterMs - startMs) / 1000, - status, - }; -} - -function shellWrap(s: string) { - return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"; -} - -export class WalletCli { - private currentTimetravel: Duration | undefined; - private _client: WalletCoreApiClient; - - setTimetravel(d: Duration | undefined) { - this.currentTimetravel = d; - } - - private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { - // Convert to microseconds - return `--timetravel=${this.currentTimetravel.d_ms * 1000}`; - } - return undefined; - } - - constructor( - private globalTestState: GlobalTestState, - private name: string = "default", - ) { - const self = this; - this._client = { - async call(op: any, payload: any): Promise { - console.log("calling wallet with timetravel arg", self.timetravelArg); - const resp = await sh( - self.globalTestState, - `wallet-${self.name}`, - `taler-wallet-cli ${ - self.timetravelArg ?? "" - } --no-throttle --wallet-db '${self.dbfile}' api '${op}' ${shellWrap( - JSON.stringify(payload), - )}`, - ); - console.log(resp); - const ar = JSON.parse(resp) as CoreApiResponse; - if (ar.type === "error") { - throw new OperationFailedError(ar.error); - } else { - return ar.result; - } - }, - }; - } - - get dbfile(): string { - return this.globalTestState.testDir + `/walletdb-${this.name}.json`; - } - - deleteDatabase() { - fs.unlinkSync(this.dbfile); - } - - private get timetravelArgArr(): string[] { - const tta = this.timetravelArg; - if (tta) { - return [tta]; - } - return []; - } - - get client(): WalletCoreApiClient { - return this._client; - } - - async runUntilDone(args: { maxRetries?: number } = {}): Promise { - await runCommand( - this.globalTestState, - `wallet-${this.name}`, - "taler-wallet-cli", - [ - "--no-throttle", - ...this.timetravelArgArr, - "--wallet-db", - this.dbfile, - "run-until-done", - ...(args.maxRetries ? ["--max-retries", `${args.maxRetries}`] : []), - ], - ); - } - - async runPending(): Promise { - await runCommand( - this.globalTestState, - `wallet-${this.name}`, - "taler-wallet-cli", - [ - "--no-throttle", - ...this.timetravelArgArr, - "--wallet-db", - this.dbfile, - "run-pending", - ], - ); - } -} diff --git a/packages/taler-wallet-cli/src/integrationtests/helpers.ts b/packages/taler-wallet-cli/src/integrationtests/helpers.ts deleted file mode 100644 index 3b4e1643f..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/helpers.ts +++ /dev/null @@ -1,406 +0,0 @@ -/* - 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 { - FaultInjectedExchangeService, - FaultInjectedMerchantService, -} from "./faultInjection"; -import { CoinConfig, defaultCoinConfig } from "./denomStructures"; -import { - AmountString, - Duration, - ContractTerms, - PreparePayResultType, - ConfirmPayResultType, -} from "@gnu-taler/taler-util"; -import { - DbInfo, - BankService, - ExchangeService, - MerchantService, - WalletCli, - GlobalTestState, - setupDb, - ExchangeServiceInterface, - BankApi, - BankAccessApi, - MerchantServiceInterface, - MerchantPrivateApi, - HarnessExchangeBankAccount, - WithAuthorization, -} from "./harness.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -export interface SimpleTestEnvironment { - commonDb: DbInfo; - bank: BankService; - exchange: ExchangeService; - exchangeBankAccount: HarnessExchangeBankAccount; - merchant: MerchantService; - wallet: WalletCli; -} - -export function getRandomIban(countryCode: string): string { - return `${countryCode}715001051796${(Math.random() * 100000000) - .toString() - .substring(0, 6)}`; -} - -export function getRandomString(): string { - return Math.random().toString(36).substring(2); -} - -/** - * Run a test case with a simple TESTKUDOS Taler environment, consisting - * of one exchange, one bank and one merchant. - */ -export async function createSimpleTestkudosEnvironment( - t: GlobalTestState, - coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), -): 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", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - 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: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - }; -} - -export interface FaultyMerchantTestEnvironment { - commonDb: DbInfo; - bank: BankService; - exchange: ExchangeService; - faultyExchange: FaultInjectedExchangeService; - exchangeBankAccount: HarnessExchangeBankAccount; - merchant: MerchantService; - faultyMerchant: FaultInjectedMerchantService; - wallet: WalletCli; -} - -/** - * 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); - - 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.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - faultyMerchant, - faultyExchange, - }; -} - -/** - * Withdraw balance. - */ -export async function startWithdrawViaBank( - t: GlobalTestState, - p: { - wallet: WalletCli; - bank: BankService; - exchange: ExchangeServiceInterface; - amount: AmountString; - }, -): Promise { - const { wallet, bank, exchange, amount } = p; - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount); - - // Hand it to the wallet - - await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - - await wallet.runPending(); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bank, user, wop); - - // Withdraw - - await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }); -} - -/** - * Withdraw balance. - */ -export async function withdrawViaBank( - t: GlobalTestState, - p: { - wallet: WalletCli; - bank: BankService; - exchange: ExchangeServiceInterface; - amount: AmountString; - }, -): Promise { - const { wallet } = p; - - await startWithdrawViaBank(t, p); - - await wallet.runUntilDone(); - - // Check balance - - await wallet.client.call(WalletApiOperation.GetBalances, {}); -} - -export async function applyTimeTravel( - timetravelDuration: Duration, - s: { - exchange?: ExchangeService; - merchant?: MerchantService; - wallet?: WalletCli; - }, -): Promise { - if (s.exchange) { - await s.exchange.stop(); - s.exchange.setTimetravel(timetravelDuration); - await s.exchange.start(); - await s.exchange.pingUntilAvailable(); - } - - if (s.merchant) { - await s.merchant.stop(); - s.merchant.setTimetravel(timetravelDuration); - await s.merchant.start(); - await s.merchant.pingUntilAvailable(); - } - - if (s.wallet) { - s.wallet.setTimetravel(timetravelDuration); - } -} - -/** - * Make a simple payment and check that it succeeded. - */ -export async function makeTestPayment( - t: GlobalTestState, - args: { - merchant: MerchantServiceInterface; - wallet: WalletCli; - order: Partial; - instance?: string; - }, - auth: WithAuthorization = {}, -): Promise { - // Set up order. - - const { wallet, merchant } = args; - const instance = args.instance ?? "default"; - - const orderResp = await MerchantPrivateApi.createOrder( - merchant, - instance, - { - order: args.order, - }, - auth, - ); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - auth, - ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - - t.assertTrue(r2.type === ConfirmPayResultType.Done); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - instance, - }, - auth, - ); - - t.assertTrue(orderStatus.order_status === "paid"); -} diff --git a/packages/taler-wallet-cli/src/integrationtests/libeufin.ts b/packages/taler-wallet-cli/src/integrationtests/libeufin.ts deleted file mode 100644 index 2ee98952a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/libeufin.ts +++ /dev/null @@ -1,1676 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 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 axios from "axios"; -import { URL } from "@gnu-taler/taler-util"; -import { getRandomIban, getRandomString } from "./helpers"; -import { - GlobalTestState, - DbInfo, - pingProc, - ProcessWrapper, - runCommand, - setupDb, - sh, -} from "./harness"; - -export interface LibeufinSandboxServiceInterface { - baseUrl: string; -} - -export interface LibeufinNexusServiceInterface { - baseUrl: string; -} - -export interface LibeufinServices { - libeufinSandbox: LibeufinSandboxService; - libeufinNexus: LibeufinNexusService; - commonDb: DbInfo; -} - -export interface LibeufinSandboxConfig { - httpPort: number; - databaseJdbcUri: string; -} - -export interface LibeufinNexusConfig { - httpPort: number; - databaseJdbcUri: string; -} - -export interface DeleteBankConnectionRequest { - bankConnectionId: string; -} - -interface LibeufinNexusMoneyMovement { - amount: string; - creditDebitIndicator: string; - details: { - debtor: { - name: string; - }; - debtorAccount: { - iban: string; - }; - debtorAgent: { - bic: string; - }; - creditor: { - name: string; - }; - creditorAccount: { - iban: string; - }; - creditorAgent: { - bic: string; - }; - endToEndId: string; - unstructuredRemittanceInformation: string; - }; -} - -interface LibeufinNexusBatches { - batchTransactions: Array; -} - -interface LibeufinNexusTransaction { - amount: string; - creditDebitIndicator: string; - status: string; - bankTransactionCode: string; - valueDate: string; - bookingDate: string; - accountServicerRef: string; - batches: Array; -} - -interface LibeufinNexusTransactions { - transactions: Array; -} - -export interface LibeufinCliDetails { - nexusUrl: string; - sandboxUrl: string; - nexusDatabaseUri: string; - sandboxDatabaseUri: string; - user: LibeufinNexusUser; -} - -export interface LibeufinEbicsSubscriberDetails { - hostId: string; - partnerId: string; - userId: string; -} - -export interface LibeufinEbicsConnectionDetails { - subscriberDetails: LibeufinEbicsSubscriberDetails; - ebicsUrl: string; - connectionName: string; -} - -export interface LibeufinBankAccountDetails { - currency: string; - iban: string; - bic: string; - personName: string; - accountName: string; -} - -export interface LibeufinNexusUser { - username: string; - password: string; -} - -export interface LibeufinBackupFileDetails { - passphrase: string; - outputFile: string; - connectionName: string; -} - -export interface LibeufinKeyLetterDetails { - outputFile: string; - connectionName: string; -} - -export interface LibeufinBankAccountImportDetails { - offeredBankAccountName: string; - nexusBankAccountName: string; - connectionName: string; -} - -export interface BankAccountInfo { - iban: string; - bic: string; - name: string; - currency: string; - label: string; -} - -export interface LibeufinPreparedPaymentDetails { - creditorIban: string; - creditorBic: string; - creditorName: string; - subject: string; - amount: string; - currency: string; - nexusBankAccountName: string; -} - -export interface LibeufinSandboxAddIncomingRequest { - creditorIban: string; - creditorBic: string; - creditorName: string; - debtorIban: string; - debtorBic: string; - debtorName: string; - subject: string; - amount: string; - currency: string; - uid: string; - direction: string; -} - -export class LibeufinSandboxService implements LibeufinSandboxServiceInterface { - static async create( - gc: GlobalTestState, - sandboxConfig: LibeufinSandboxConfig, - ): Promise { - return new LibeufinSandboxService(gc, sandboxConfig); - } - - sandboxProc: ProcessWrapper | undefined; - globalTestState: GlobalTestState; - - constructor( - gc: GlobalTestState, - private sandboxConfig: LibeufinSandboxConfig, - ) { - this.globalTestState = gc; - } - - get baseUrl(): string { - return `http://localhost:${this.sandboxConfig.httpPort}/`; - } - - async start(): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-sandbox-config", - "libeufin-sandbox config localhost", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - }, - ); - this.sandboxProc = this.globalTestState.spawnService( - "libeufin-sandbox", - ["serve", "--port", `${this.sandboxConfig.httpPort}`], - "libeufin-sandbox", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", - }, - ); - } - - async c53tick(): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-sandbox-c53tick", - "libeufin-sandbox camt053tick", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - }, - ); - return stdout; - } - - async makeTransaction( - debit: string, - credit: string, - amount: string, // $currency:x.y - subject: string,): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-sandbox-maketransfer", - `libeufin-sandbox make-transaction --debit-account=${debit} --credit-account=${credit} ${amount} "${subject}"`, - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - }, - ); - return stdout; - } - - async pingUntilAvailable(): Promise { - const url = this.baseUrl; - await pingProc(this.sandboxProc, url, "libeufin-sandbox"); - } -} - -export class LibeufinNexusService { - static async create( - gc: GlobalTestState, - nexusConfig: LibeufinNexusConfig, - ): Promise { - return new LibeufinNexusService(gc, nexusConfig); - } - - nexusProc: ProcessWrapper | undefined; - globalTestState: GlobalTestState; - - constructor(gc: GlobalTestState, private nexusConfig: LibeufinNexusConfig) { - this.globalTestState = gc; - } - - get baseUrl(): string { - return `http://localhost:${this.nexusConfig.httpPort}/`; - } - - async start(): Promise { - await runCommand( - this.globalTestState, - "libeufin-nexus-superuser", - "libeufin-nexus", - ["superuser", "admin", "--password", "test"], - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, - }, - ); - - this.nexusProc = this.globalTestState.spawnService( - "libeufin-nexus", - ["serve", "--port", `${this.nexusConfig.httpPort}`], - "libeufin-nexus", - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, - }, - ); - } - - async pingUntilAvailable(): Promise { - const url = `${this.baseUrl}config`; - await pingProc(this.nexusProc, url, "libeufin-nexus"); - } - - async createNexusSuperuser(details: LibeufinNexusUser): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-nexus", - `libeufin-nexus superuser ${details.username} --password=${details.password}`, - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, - }, - ); - console.log(stdout); - } -} - -export interface CreateEbicsSubscriberRequest { - hostID: string; - userID: string; - partnerID: string; - systemID?: string; -} - -export interface TwgAddIncomingRequest { - amount: string; - reserve_pub: string; - debit_account: string; -} - -interface CreateEbicsBankAccountRequest { - subscriber: { - hostID: string; - partnerID: string; - userID: string; - systemID?: string; - }; - // IBAN - iban: string; - // BIC - bic: string; - // human name - name: string; - currency: string; - label: string; -} - -export interface SimulateIncomingTransactionRequest { - debtorIban: string; - debtorBic: string; - debtorName: string; - - /** - * Subject / unstructured remittance info. - */ - subject: string; - - /** - * Decimal amount without currency. - */ - amount: string; -} - -/** - * The bundle aims at minimizing the amount of input - * data that is required to initialize a new user + Ebics - * connection. - */ -export class NexusUserBundle { - userReq: CreateNexusUserRequest; - connReq: CreateEbicsBankConnectionRequest; - anastasisReq: CreateAnastasisFacadeRequest; - twgReq: CreateTalerWireGatewayFacadeRequest; - twgTransferPermission: PostNexusPermissionRequest; - twgHistoryPermission: PostNexusPermissionRequest; - twgAddIncomingPermission: PostNexusPermissionRequest; - localAccountName: string; - remoteAccountName: string; - - constructor(salt: string, ebicsURL: string) { - this.userReq = { - username: `username-${salt}`, - password: `password-${salt}`, - }; - - this.connReq = { - name: `connection-${salt}`, - ebicsURL: ebicsURL, - hostID: `ebicshost,${salt}`, - partnerID: `ebicspartner,${salt}`, - userID: `ebicsuser,${salt}`, - }; - - this.twgReq = { - currency: "EUR", - name: `twg-${salt}`, - reserveTransferLevel: "report", - accountName: `local-account-${salt}`, - connectionName: `connection-${salt}`, - }; - this.anastasisReq = { - currency: "EUR", - name: `anastasis-${salt}`, - reserveTransferLevel: "report", - accountName: `local-account-${salt}`, - connectionName: `connection-${salt}`, - }; - this.remoteAccountName = `remote-account-${salt}`; - this.localAccountName = `local-account-${salt}`; - this.twgTransferPermission = { - action: "grant", - permission: { - subjectId: `username-${salt}`, - subjectType: "user", - resourceType: "facade", - resourceId: `twg-${salt}`, - permissionName: "facade.talerWireGateway.transfer", - }, - }; - this.twgHistoryPermission = { - action: "grant", - permission: { - subjectId: `username-${salt}`, - subjectType: "user", - resourceType: "facade", - resourceId: `twg-${salt}`, - permissionName: "facade.talerWireGateway.history", - }, - }; - } -} - -/** - * The bundle aims at minimizing the amount of input - * data that is required to initialize a new Sandbox - * customer, associating their bank account with a Ebics - * subscriber. - */ -export class SandboxUserBundle { - ebicsBankAccount: CreateEbicsBankAccountRequest; - constructor(salt: string) { - this.ebicsBankAccount = { - currency: "EUR", - bic: "BELADEBEXXX", - iban: getRandomIban("DE"), - label: `remote-account-${salt}`, - name: `Taler Exchange: ${salt}`, - subscriber: { - hostID: `ebicshost,${salt}`, - partnerID: `ebicspartner,${salt}`, - userID: `ebicsuser,${salt}`, - }, - }; - } -} - -export class LibeufinCli { - cliDetails: LibeufinCliDetails; - globalTestState: GlobalTestState; - - constructor(gc: GlobalTestState, cd: LibeufinCliDetails) { - this.globalTestState = gc; - this.cliDetails = cd; - } - - env(): any { - return { - ...process.env, - LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl, - LIBEUFIN_SANDBOX_USERNAME: "admin", - LIBEUFIN_SANDBOX_PASSWORD: "secret", - } - } - - async checkSandbox(): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-checksandbox", - "libeufin-cli sandbox check", - this.env() - ); - } - - async createEbicsHost(hostId: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicshost", - `libeufin-cli sandbox ebicshost create --host-id=${hostId}`, - this.env() - ); - console.log(stdout); - } - - async createEbicsSubscriber( - details: LibeufinEbicsSubscriberDetails, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicssubscriber", - "libeufin-cli sandbox ebicssubscriber create" + - ` --host-id=${details.hostId}` + - ` --partner-id=${details.partnerId}` + - ` --user-id=${details.userId}`, - this.env() - ); - console.log(stdout); - } - - async createEbicsBankAccount( - sd: LibeufinEbicsSubscriberDetails, - bankAccountDetails: LibeufinBankAccountDetails, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicsbankaccount", - "libeufin-cli sandbox ebicsbankaccount create" + - ` --currency=${bankAccountDetails.currency}` + - ` --iban=${bankAccountDetails.iban}` + - ` --bic=${bankAccountDetails.bic}` + - ` --person-name='${bankAccountDetails.personName}'` + - ` --account-name=${bankAccountDetails.accountName}` + - ` --ebics-host-id=${sd.hostId}` + - ` --ebics-partner-id=${sd.partnerId}` + - ` --ebics-user-id=${sd.userId}`, - this.env() - ); - console.log(stdout); - } - - async generateTransactions(accountName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-generatetransactions", - `libeufin-cli sandbox bankaccount generate-transactions ${accountName}`, - this.env() - ); - console.log(stdout); - } - - async showSandboxTransactions(accountName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-showsandboxtransactions", - `libeufin-cli sandbox bankaccount transactions ${accountName}`, - this.env() - ); - console.log(stdout); - } - - async createEbicsConnection( - connectionDetails: LibeufinEbicsConnectionDetails, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicsconnection", - `libeufin-cli connections new-ebics-connection` + - ` --ebics-url=${connectionDetails.ebicsUrl}` + - ` --host-id=${connectionDetails.subscriberDetails.hostId}` + - ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` + - ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` + - ` ${connectionDetails.connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async createBackupFile(details: LibeufinBackupFileDetails): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createbackupfile", - `libeufin-cli connections export-backup` + - ` --passphrase=${details.passphrase}` + - ` --output-file=${details.outputFile}` + - ` ${details.connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async createKeyLetter(details: LibeufinKeyLetterDetails): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createkeyletter", - `libeufin-cli connections get-key-letter` + - ` ${details.connectionName} ${details.outputFile}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async connect(connectionName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-connect", - `libeufin-cli connections connect ${connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async downloadBankAccounts(connectionName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-downloadbankaccounts", - `libeufin-cli connections download-bank-accounts ${connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async listOfferedBankAccounts(connectionName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-listofferedbankaccounts", - `libeufin-cli connections list-offered-bank-accounts ${connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async importBankAccount( - importDetails: LibeufinBankAccountImportDetails, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-importbankaccount", - "libeufin-cli connections import-bank-account" + - ` --offered-account-id=${importDetails.offeredBankAccountName}` + - ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` + - ` ${importDetails.connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async fetchTransactions(bankAccountName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-fetchtransactions", - `libeufin-cli accounts fetch-transactions ${bankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async transactions(bankAccountName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-transactions", - `libeufin-cli accounts transactions ${bankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async preparePayment(details: LibeufinPreparedPaymentDetails): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-preparepayment", - `libeufin-cli accounts prepare-payment` + - ` --creditor-iban=${details.creditorIban}` + - ` --creditor-bic=${details.creditorBic}` + - ` --creditor-name='${details.creditorName}'` + - ` --payment-subject='${details.subject}'` + - ` --payment-amount=${details.currency}:${details.amount}` + - ` ${details.nexusBankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async submitPayment( - details: LibeufinPreparedPaymentDetails, - paymentUuid: string, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-submitpayment", - `libeufin-cli accounts submit-payment` + - ` --payment-uuid=${paymentUuid}` + - ` ${details.nexusBankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async newAnastasisFacade(req: NewAnastasisFacadeReq): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-new-anastasis-facade", - `libeufin-cli facades new-anastasis-facade` + - ` --currency ${req.currency}` + - ` --facade-name ${req.facadeName}` + - ` ${req.connectionName} ${req.accountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - - async newTalerWireGatewayFacade(req: NewTalerWireGatewayReq): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-new-taler-wire-gateway-facade", - `libeufin-cli facades new-taler-wire-gateway-facade` + - ` --currency ${req.currency}` + - ` --facade-name ${req.facadeName}` + - ` ${req.connectionName} ${req.accountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async listFacades(): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-facades-list", - `libeufin-cli facades list`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } -} - -interface NewAnastasisFacadeReq { - facadeName: string; - connectionName: string; - accountName: string; - currency: string; -} - -interface NewTalerWireGatewayReq { - facadeName: string; - connectionName: string; - accountName: string; - currency: string; -} - -export namespace LibeufinSandboxApi { - - export async function rotateKeys( - libeufinSandboxService: LibeufinSandboxServiceInterface, - hostID: string, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL(`admin/ebics/hosts/${hostID}/rotate-keys`, baseUrl); - await axios.post(url.href, {}, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - export async function createEbicsHost( - libeufinSandboxService: LibeufinSandboxServiceInterface, - hostID: string, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/ebics/hosts", baseUrl); - await axios.post(url.href, { - hostID, - ebicsVersion: "2.5", - }, - { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function createBankAccount( - libeufinSandboxService: LibeufinSandboxServiceInterface, - req: BankAccountInfo, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL(`admin/bank-accounts/${req.label}`, baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function createEbicsSubscriber( - libeufinSandboxService: LibeufinSandboxServiceInterface, - req: CreateEbicsSubscriberRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/ebics/subscribers", baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function createEbicsBankAccount( - libeufinSandboxService: LibeufinSandboxServiceInterface, - req: CreateEbicsBankAccountRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/ebics/bank-accounts", baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function bookPayment2( - libeufinSandboxService: LibeufinSandboxService, - req: LibeufinSandboxAddIncomingRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/payments", baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function bookPayment( - libeufinSandboxService: LibeufinSandboxService, - creditorBundle: SandboxUserBundle, - debitorBundle: SandboxUserBundle, - subject: string, - amount: string, - currency: string, - ) { - let req: LibeufinSandboxAddIncomingRequest = { - creditorIban: creditorBundle.ebicsBankAccount.iban, - creditorBic: creditorBundle.ebicsBankAccount.bic, - creditorName: creditorBundle.ebicsBankAccount.name, - debtorIban: debitorBundle.ebicsBankAccount.iban, - debtorBic: debitorBundle.ebicsBankAccount.bic, - debtorName: debitorBundle.ebicsBankAccount.name, - subject: subject, - amount: amount, - currency: currency, - uid: getRandomString(), - direction: "CRDT", - }; - await bookPayment2(libeufinSandboxService, req); - } - - export async function simulateIncomingTransaction( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - req: SimulateIncomingTransactionRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL( - `admin/bank-accounts/${accountLabel}/simulate-incoming-transaction`, - baseUrl, - ); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function getAccountTransactions( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ): Promise { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL( - `admin/bank-accounts/${accountLabel}/transactions`, - baseUrl, - ); - const res = await axios.get(url.href, { - auth: { - username: "admin", - password: "secret", - }, - }); - return res.data as SandboxAccountTransactions; - } - - export async function getCamt053( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ): Promise { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/payments/camt", baseUrl); - return await axios.post(url.href, { - bankaccount: accountLabel, - type: 53, - }, - { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function getAccountInfoWithBalance( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ): Promise { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL( - `admin/bank-accounts/${accountLabel}`, - baseUrl, - ); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "secret", - }, - }); - } -} - -export interface SandboxAccountTransactions { - payments: { - accountLabel: string; - creditorIban: string; - creditorBic?: string; - creditorName: string; - debtorIban: string; - debtorBic: string; - debtorName: string; - amount: string; - currency: string; - subject: string; - date: string; - creditDebitIndicator: "debit" | "credit"; - accountServicerReference: string; - }[]; -} - -export interface CreateEbicsBankConnectionRequest { - name: string; - ebicsURL: string; - hostID: string; - userID: string; - partnerID: string; - systemID?: string; -} - -export interface CreateAnastasisFacadeRequest { - name: string; - connectionName: string; - accountName: string; - currency: string; - reserveTransferLevel: "report" | "statement" | "notification"; -} - - -export interface CreateTalerWireGatewayFacadeRequest { - name: string; - connectionName: string; - accountName: string; - currency: string; - reserveTransferLevel: "report" | "statement" | "notification"; -} - -export interface UpdateNexusUserRequest { - newPassword: string; -} - -export interface NexusAuth { - auth: { - username: string; - password: string; - }; -} - -export interface CreateNexusUserRequest { - username: string; - password: string; -} - -export interface PostNexusTaskRequest { - name: string; - cronspec: string; - type: string; // fetch | submit - params: - | { - level: string; // report | statement | all - rangeType: string; // all | since-last | previous-days | latest - } - | {}; -} - -export interface PostNexusPermissionRequest { - action: "revoke" | "grant"; - permission: { - subjectType: string; - subjectId: string; - resourceType: string; - resourceId: string; - permissionName: string; - }; -} - -export namespace LibeufinNexusApi { - export async function getAllConnections( - nexus: LibeufinNexusServiceInterface, - ): Promise { - let url = new URL("bank-connections", nexus.baseUrl); - const res = await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - return res; - } - - export async function deleteBankConnection( - libeufinNexusService: LibeufinNexusServiceInterface, - req: DeleteBankConnectionRequest, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("bank-connections/delete-connection", baseUrl); - return await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function createEbicsBankConnection( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateEbicsBankConnectionRequest, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("bank-connections", baseUrl); - await axios.post( - url.href, - { - source: "new", - type: "ebics", - name: req.name, - data: { - ebicsURL: req.ebicsURL, - hostID: req.hostID, - userID: req.userID, - partnerID: req.partnerID, - systemID: req.systemID, - }, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function getBankAccount( - libeufinNexusService: LibeufinNexusServiceInterface, - accountName: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-accounts/${accountName}`, - baseUrl, - ); - return await axios.get( - url.href, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - - export async function submitInitiatedPayment( - libeufinNexusService: LibeufinNexusServiceInterface, - accountName: string, - paymentId: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-accounts/${accountName}/payment-initiations/${paymentId}/submit`, - baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function fetchAccounts( - libeufinNexusService: LibeufinNexusServiceInterface, - connectionName: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-connections/${connectionName}/fetch-accounts`, - baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function importConnectionAccount( - libeufinNexusService: LibeufinNexusServiceInterface, - connectionName: string, - offeredAccountId: string, - nexusBankAccountId: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-connections/${connectionName}/import-account`, - baseUrl, - ); - await axios.post( - url.href, - { - offeredAccountId, - nexusBankAccountId, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function connectBankConnection( - libeufinNexusService: LibeufinNexusServiceInterface, - connectionName: string, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`bank-connections/${connectionName}/connect`, baseUrl); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function getPaymentInitiations( - libeufinNexusService: LibeufinNexusService, - accountName: string, - username: string = "admin", - password: string = "test", - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${accountName}/payment-initiations`, - baseUrl, - ); - let response = await axios.get(url.href, { - auth: { - username: username, - password: password, - }, - }); - console.log( - `Payment initiations of: ${accountName}`, - JSON.stringify(response.data, null, 2), - ); - } - - export async function getConfig( - libeufinNexusService: LibeufinNexusService, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/config`, baseUrl); - let response = await axios.get(url.href); - } - - // Uses the Anastasis API to get a list of transactions. - export async function getAnastasisTransactions( - libeufinNexusService: LibeufinNexusService, - anastasisBaseUrl: string, - params: {}, // of the request: {delta: 5, ..} - username: string = "admin", - password: string = "test", - ): Promise { - let url = new URL("history/incoming", anastasisBaseUrl); - let response = await axios.get(url.href, { params: params, - auth: { - username: username, - password: password, - }, - }); - return response; - } - - // FIXME: this function should return some structured - // object that represents a history. - export async function getAccountTransactions( - libeufinNexusService: LibeufinNexusService, - accountName: string, - username: string = "admin", - password: string = "test", - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/bank-accounts/${accountName}/transactions`, baseUrl); - let response = await axios.get(url.href, { - auth: { - username: username, - password: password, - }, - }); - return response; - } - - export async function fetchTransactions( - libeufinNexusService: LibeufinNexusService, - accountName: string, - rangeType: string = "all", - level: string = "report", - username: string = "admin", - password: string = "test", - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${accountName}/fetch-transactions`, - baseUrl, - ); - return await axios.post( - url.href, - { - rangeType: rangeType, - level: level, - }, - { - auth: { - username: username, - password: password, - }, - }, - ); - } - - export async function changePassword( - libeufinNexusService: LibeufinNexusServiceInterface, - username: string, - req: UpdateNexusUserRequest, - auth: NexusAuth, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/users/${username}/password`, baseUrl); - await axios.post(url.href, req, auth); - } - - export async function getUser( - libeufinNexusService: LibeufinNexusServiceInterface, - auth: NexusAuth, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/user`, baseUrl); - return await axios.get(url.href, auth); - } - - export async function createUser( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateNexusUserRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/users`, baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function getAllPermissions( - libeufinNexusService: LibeufinNexusServiceInterface, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/permissions`, baseUrl); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function postPermission( - libeufinNexusService: LibeufinNexusServiceInterface, - req: PostNexusPermissionRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/permissions`, baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function getTasks( - libeufinNexusService: LibeufinNexusServiceInterface, - bankAccountName: string, - // When void, the request returns the list of all the - // tasks under this bank account. - taskName: string | void, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); - if (taskName) url = new URL(taskName, `${url}/`); - - // It's caller's responsibility to interpret the response. - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function deleteTask( - libeufinNexusService: LibeufinNexusServiceInterface, - bankAccountName: string, - taskName: string, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${bankAccountName}/schedule/${taskName}`, - baseUrl, - ); - await axios.delete(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function postTask( - libeufinNexusService: LibeufinNexusServiceInterface, - bankAccountName: string, - req: PostNexusTaskRequest, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); - return await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function deleteFacade( - libeufinNexusService: LibeufinNexusServiceInterface, - facadeName: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`facades/${facadeName}`, baseUrl); - return await axios.delete(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function getAllFacades( - libeufinNexusService: LibeufinNexusServiceInterface, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("facades", baseUrl); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function createAnastasisFacade( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateAnastasisFacadeRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("facades", baseUrl); - await axios.post( - url.href, - { - name: req.name, - type: "anastasis", - config: { - bankAccount: req.accountName, - bankConnection: req.connectionName, - currency: req.currency, - reserveTransferLevel: req.reserveTransferLevel, - }, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function createTwgFacade( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateTalerWireGatewayFacadeRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("facades", baseUrl); - await axios.post( - url.href, - { - name: req.name, - type: "taler-wire-gateway", - config: { - bankAccount: req.accountName, - bankConnection: req.connectionName, - currency: req.currency, - reserveTransferLevel: req.reserveTransferLevel, - }, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function submitAllPaymentInitiations( - libeufinNexusService: LibeufinNexusServiceInterface, - accountId: string, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${accountId}/submit-all-payment-initiations`, - baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } -} - -/** - * Launch Nexus and Sandbox AND creates users / facades / bank accounts / - * .. all that's required to start making banking traffic. - */ -export async function launchLibeufinServices( - t: GlobalTestState, - nexusUserBundle: NexusUserBundle[], - sandboxUserBundle: SandboxUserBundle[] = [], - withFacades: string[] = [], // takes only "twg" and/or "anastasis" -): Promise { - const db = await setupDb(t); - - const libeufinSandbox = await LibeufinSandboxService.create(t, { - httpPort: 5010, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - }); - - await libeufinSandbox.start(); - await libeufinSandbox.pingUntilAvailable(); - - const libeufinNexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - - await libeufinNexus.start(); - await libeufinNexus.pingUntilAvailable(); - console.log("Libeufin services launched!"); - - for (let sb of sandboxUserBundle) { - await LibeufinSandboxApi.createEbicsHost( - libeufinSandbox, - sb.ebicsBankAccount.subscriber.hostID, - ); - await LibeufinSandboxApi.createEbicsSubscriber( - libeufinSandbox, - sb.ebicsBankAccount.subscriber, - ); - await LibeufinSandboxApi.createEbicsBankAccount( - libeufinSandbox, - sb.ebicsBankAccount, - ); - } - console.log("Sandbox user(s) / account(s) / subscriber(s): created"); - - for (let nb of nexusUserBundle) { - await LibeufinNexusApi.createEbicsBankConnection(libeufinNexus, nb.connReq); - await LibeufinNexusApi.connectBankConnection( - libeufinNexus, - nb.connReq.name, - ); - await LibeufinNexusApi.fetchAccounts(libeufinNexus, nb.connReq.name); - await LibeufinNexusApi.importConnectionAccount( - libeufinNexus, - nb.connReq.name, - nb.remoteAccountName, - nb.localAccountName, - ); - await LibeufinNexusApi.createUser(libeufinNexus, nb.userReq); - for (let facade of withFacades) { - switch (facade) { - case "twg": - await LibeufinNexusApi.createTwgFacade(libeufinNexus, nb.twgReq); - await LibeufinNexusApi.postPermission( - libeufinNexus, - nb.twgTransferPermission, - ); - await LibeufinNexusApi.postPermission( - libeufinNexus, - nb.twgHistoryPermission, - ); - break; - case "anastasis": - await LibeufinNexusApi.createAnastasisFacade(libeufinNexus, nb.anastasisReq); - } - } - } - console.log( - "Nexus user(s) / connection(s) / facade(s) / permission(s): created", - ); - - return { - commonDb: db, - libeufinNexus: libeufinNexus, - libeufinSandbox: libeufinSandbox, - }; -} - -/** - * Helper function that searches a payment among - * a list, as returned by Nexus. The key is just - * the payment subject. - */ -export function findNexusPayment( - key: string, - payments: LibeufinNexusTransactions, -): LibeufinNexusMoneyMovement | void { - let transactions = payments["transactions"]; - for (let i = 0; i < transactions.length; i++) { - let batches = transactions[i]["batches"]; - for (let y = 0; y < batches.length; y++) { - let movements = batches[y]["batchTransactions"]; - for (let z = 0; z < movements.length; z++) { - let movement = movements[z]; - if (movement["details"]["unstructuredRemittanceInformation"] == key) - return movement; - } - } - } -} diff --git a/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts b/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts deleted file mode 100644 index a93a0ed25..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts +++ /dev/null @@ -1,318 +0,0 @@ -/* - 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 - */ - -/** - * Test harness for various GNU Taler components. - * Also provides a fault-injection proxy. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { - ContractTerms, - Duration, - Codec, - buildCodecForObject, - codecForString, - codecOptional, - codecForConstString, - codecForBoolean, - codecForNumber, - codecForContractTerms, - codecForAny, - buildCodecForUnion, - AmountString, - Timestamp, - CoinPublicKeyString, - EddsaPublicKeyString, - codecForAmountString, -} from "@gnu-taler/taler-util"; - -export interface PostOrderRequest { - // The order must at least contain the minimal - // order detail, but can override all - order: Partial; - - // if set, the backend will then set the refund deadline to the current - // time plus the specified delay. - refund_delay?: Duration; - - // specifies the payment target preferred by the client. Can be used - // to select among the various (active) wire methods supported by the instance. - payment_target?: string; - - // FIXME: some fields are missing - - // Should a token for claiming the order be generated? - // False can make sense if the ORDER_ID is sufficiently - // high entropy to prevent adversarial claims (like it is - // if the backend auto-generates one). Default is 'true'. - create_token?: boolean; -} - -export type ClaimToken = string; - -export interface PostOrderResponse { - order_id: string; - token?: ClaimToken; -} - -export const codecForPostOrderResponse = (): Codec => - buildCodecForObject() - .property("order_id", codecForString()) - .property("token", codecOptional(codecForString())) - .build("PostOrderResponse"); - -export const codecForCheckPaymentPaidResponse = (): Codec => - buildCodecForObject() - .property("order_status_url", codecForString()) - .property("order_status", codecForConstString("paid")) - .property("refunded", codecForBoolean()) - .property("wired", codecForBoolean()) - .property("deposit_total", codecForAmountString()) - .property("exchange_ec", codecForNumber()) - .property("exchange_hc", codecForNumber()) - .property("refund_amount", codecForAmountString()) - .property("contract_terms", codecForContractTerms()) - // FIXME: specify - .property("wire_details", codecForAny()) - .property("wire_reports", codecForAny()) - .property("refund_details", codecForAny()) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentUnpaidResponse = (): Codec => - buildCodecForObject() - .property("order_status", codecForConstString("unpaid")) - .property("taler_pay_uri", codecForString()) - .property("order_status_url", codecForString()) - .property("already_paid_order_id", codecOptional(codecForString())) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentClaimedResponse = (): Codec => - buildCodecForObject() - .property("order_status", codecForConstString("claimed")) - .property("contract_terms", codecForContractTerms()) - .build("CheckPaymentClaimedResponse"); - -export const codecForMerchantOrderPrivateStatusResponse = (): Codec => - buildCodecForUnion() - .discriminateOn("order_status") - .alternative("paid", codecForCheckPaymentPaidResponse()) - .alternative("unpaid", codecForCheckPaymentUnpaidResponse()) - .alternative("claimed", codecForCheckPaymentClaimedResponse()) - .build("MerchantOrderPrivateStatusResponse"); - -export type MerchantOrderPrivateStatusResponse = - | CheckPaymentPaidResponse - | CheckPaymentUnpaidResponse - | CheckPaymentClaimedResponse; - -export interface CheckPaymentClaimedResponse { - // Wallet claimed the order, but didn't pay yet. - order_status: "claimed"; - - contract_terms: ContractTerms; -} - -export interface CheckPaymentPaidResponse { - // did the customer pay for this contract - order_status: "paid"; - - // Was the payment refunded (even partially) - refunded: boolean; - - // Did the exchange wire us the funds - wired: boolean; - - // Total amount the exchange deposited into our bank account - // for this contract, excluding fees. - deposit_total: AmountString; - - // Numeric error code indicating errors the exchange - // encountered tracking the wire transfer for this purchase (before - // we even got to specific coin issues). - // 0 if there were no issues. - exchange_ec: number; - - // HTTP status code returned by the exchange when we asked for - // information to track the wire transfer for this purchase. - // 0 if there were no issues. - exchange_hc: number; - - // Total amount that was refunded, 0 if refunded is false. - refund_amount: AmountString; - - // Contract terms - contract_terms: ContractTerms; - - // Ihe wire transfer status from the exchange for this order if available, otherwise empty array - wire_details: TransactionWireTransfer[]; - - // Reports about trouble obtaining wire transfer details, empty array if no trouble were encountered. - wire_reports: TransactionWireReport[]; - - // The refund details for this order. One entry per - // refunded coin; empty array if there are no refunds. - refund_details: RefundDetails[]; - - order_status_url: string; -} - -export interface CheckPaymentUnpaidResponse { - order_status: "unpaid"; - - // URI that the wallet must process to complete the payment. - taler_pay_uri: string; - - order_status_url: string; - - // Alternative order ID which was paid for already in the same session. - // Only given if the same product was purchased before in the same session. - already_paid_order_id?: string; - - // We do we NOT return the contract terms here because they may not - // exist in case the wallet did not yet claim them. -} - -export interface RefundDetails { - // Reason given for the refund - reason: string; - - // when was the refund approved - timestamp: Timestamp; - - // Total amount that was refunded (minus a refund fee). - amount: AmountString; -} - -export interface TransactionWireTransfer { - // Responsible exchange - exchange_url: string; - - // 32-byte wire transfer identifier - wtid: string; - - // execution time of the wire transfer - execution_time: Timestamp; - - // Total amount that has been wire transferred - // to the merchant - amount: AmountString; - - // Was this transfer confirmed by the merchant via the - // POST /transfers API, or is it merely claimed by the exchange? - confirmed: boolean; -} - -export interface TransactionWireReport { - // Numerical error code - code: number; - - // Human-readable error description - hint: string; - - // Numerical error code from the exchange. - exchange_ec: number; - - // HTTP status code received from the exchange. - exchange_hc: number; - - // Public key of the coin for which we got the exchange error. - coin_pub: CoinPublicKeyString; -} - -export interface TippingReserveStatus { - // Array of all known reserves (possibly empty!) - reserves: ReserveStatusEntry[]; -} - -export interface ReserveStatusEntry { - // Public key of the reserve - reserve_pub: string; - - // Timestamp when it was established - creation_time: Timestamp; - - // Timestamp when it expires - expiration_time: Timestamp; - - // Initial amount as per reserve creation call - merchant_initial_amount: AmountString; - - // Initial amount as per exchange, 0 if exchange did - // not confirm reserve creation yet. - exchange_initial_amount: AmountString; - - // Amount picked up so far. - pickup_amount: AmountString; - - // Amount approved for tips that exceeds the pickup_amount. - committed_amount: AmountString; - - // Is this reserve active (false if it was deleted but not purged) - active: boolean; -} - -export interface TipCreateConfirmation { - // Unique tip identifier for the tip that was created. - tip_id: string; - - // taler://tip URI for the tip - taler_tip_uri: string; - - // URL that will directly trigger processing - // the tip when the browser is redirected to it - tip_status_url: string; - - // when does the tip expire - tip_expiration: Timestamp; -} - -export interface TipCreateRequest { - // Amount that the customer should be tipped - amount: AmountString; - - // Justification for giving the tip - justification: string; - - // URL that the user should be directed to after tipping, - // will be included in the tip_token. - next_url: string; -} - -export interface MerchantInstancesResponse { - // List of instances that are present in the backend (see Instance) - instances: MerchantInstanceDetail[]; -} - -export interface MerchantInstanceDetail { - // Merchant name corresponding to this instance. - name: string; - - // Merchant instance this response is about ($INSTANCE) - id: string; - - // Public key of the merchant/instance, in Crockford Base32 encoding. - merchant_pub: EddsaPublicKeyString; - - // List of the payment targets supported by this instance. Clients can - // specify the desired payment target in /order requests. Note that - // front-ends do not have to support wallets selecting payment targets. - payment_targets: string[]; -} diff --git a/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts b/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts index e3c2af8e6..ea05de8e9 100644 --- a/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts +++ b/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. diff --git a/packages/taler-wallet-cli/src/integrationtests/sync.ts b/packages/taler-wallet-cli/src/integrationtests/sync.ts deleted file mode 100644 index fccff715f..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/sync.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 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 { URL } from "@gnu-taler/taler-util"; -import * as fs from "fs"; -import * as util from "util"; -import { - GlobalTestState, - pingProc, - ProcessWrapper, -} from "./harness"; -import { Configuration } from "@gnu-taler/taler-util"; - -const exec = util.promisify(require("child_process").exec); - -export interface SyncConfig { - /** - * Human-readable name used in the test harness logs. - */ - name: string; - - httpPort: number; - - /** - * Database connection string (only postgres is supported). - */ - database: string; - - annualFee: string; - - currency: string; - - uploadLimitMb: number; - - /** - * Fulfillment URL used for contract terms related to - * sync. - */ - fulfillmentUrl: string; - - paymentBackendUrl: string; -} - -function setSyncPaths(config: Configuration, home: string) { - config.setString("paths", "sync_home", home); - // We need to make sure that the path of taler_runtime_dir isn't too long, - // as it contains unix domain sockets (108 character limit). - const runDir = fs.mkdtempSync("/tmp/taler-test-"); - config.setString("paths", "sync_runtime_dir", runDir); - config.setString("paths", "sync_data_home", "$SYNC_HOME/.local/share/sync/"); - config.setString("paths", "sync_config_home", "$SYNC_HOME/.config/sync/"); - config.setString("paths", "sync_cache_home", "$SYNC_HOME/.config/sync/"); -} - -export class SyncService { - static async create( - gc: GlobalTestState, - sc: SyncConfig, - ): Promise { - const config = new Configuration(); - - const cfgFilename = gc.testDir + `/sync-${sc.name}.conf`; - setSyncPaths(config, gc.testDir + "/synchome"); - config.setString("taler", "currency", sc.currency); - config.setString("sync", "serve", "tcp"); - config.setString("sync", "port", `${sc.httpPort}`); - config.setString("sync", "db", "postgres"); - config.setString("syncdb-postgres", "config", sc.database); - config.setString("sync", "payment_backend_url", sc.paymentBackendUrl); - config.setString("sync", "upload_limit_mb", `${sc.uploadLimitMb}`); - config.write(cfgFilename); - - return new SyncService(gc, sc, cfgFilename); - } - - proc: ProcessWrapper | undefined; - - get baseUrl(): string { - return `http://localhost:${this.syncConfig.httpPort}/`; - } - - async start(): Promise { - await exec(`sync-dbinit -c "${this.configFilename}"`); - - this.proc = this.globalState.spawnService( - "sync-httpd", - ["-LDEBUG", "-c", this.configFilename], - `sync-${this.syncConfig.name}`, - ); - } - - async pingUntilAvailable(): Promise { - const url = new URL("config", this.baseUrl).href; - await pingProc(this.proc, url, "sync"); - } - - constructor( - private globalState: GlobalTestState, - private syncConfig: SyncConfig, - private configFilename: string, - ) {} -} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts index d6d0e2dce..0f8af05e5 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts @@ -27,9 +27,9 @@ import { BankApi, BankAccessApi, CreditDebitIndicator, -} from "./harness"; +} from "../harness/harness.js"; import { createEddsaKeyPair, encodeCrock } from "@gnu-taler/taler-util"; -import { defaultCoinConfig } from "./denomStructures"; +import { defaultCoinConfig } from "../harness/denomStructures"; /** * Run test for basic, bank-integrated withdrawal. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts b/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts index 46882d5c4..a509e3b19 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; import { URL } from "url"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts index 430a1ac93..28cca0758 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts @@ -27,8 +27,8 @@ import { WalletApiOperation, } from "@gnu-taler/taler-wallet-core"; import { makeEventId } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; export async function runDenomUnofferedTest(t: GlobalTestState) { // Set up test environment diff --git a/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts b/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts index 156661e46..f33c8338b 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts @@ -18,8 +18,8 @@ * Imports. */ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal and payment. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts index 9cbdbd34c..8a5d563ce 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts @@ -26,7 +26,7 @@ import { MerchantService, BankApi, BankAccessApi, -} from "./harness"; +} from "../harness/harness.js"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { ExchangesListRespose, @@ -36,8 +36,8 @@ import { import { FaultInjectedExchangeService, FaultInjectionResponseContext, -} from "./faultInjection"; -import { defaultCoinConfig } from "./denomStructures"; +} from "../harness/faultInjection"; +import { defaultCoinConfig } from "../harness/denomStructures"; /** * Test if the wallet handles outdated exchange versions correct.y diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts index 50065c0df..56684f70a 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts @@ -31,7 +31,7 @@ import { readSuccessResponseJsonOrThrow, WalletApiOperation, } from "@gnu-taler/taler-wallet-core"; -import { makeNoFeeCoinConfig } from "./denomStructures"; +import { makeNoFeeCoinConfig } from "../harness/denomStructures"; import { BankService, ExchangeService, @@ -40,8 +40,8 @@ import { MerchantService, setupDb, WalletCli, -} from "./harness"; -import { startWithdrawViaBank, withdrawViaBank } from "./helpers"; +} from "../harness/harness.js"; +import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js"; async function applyTimeTravel( timetravelDuration: Duration, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts b/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts index ae8cf0e17..025e12226 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts @@ -25,12 +25,12 @@ import { MerchantService, setupDb, WalletCli, -} from "./harness"; +} from "../harness/harness.js"; import { withdrawViaBank, makeTestPayment, SimpleTestEnvironment, -} from "./helpers"; +} from "../harness/helpers.js"; /** * Run a test case with a simple TESTKUDOS Taler environment, consisting diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts index 8e079caa4..839ad5fa7 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { NexusUserBundle, LibeufinNexusApi, @@ -25,7 +25,7 @@ import { LibeufinSandboxService, LibeufinSandboxApi, findNexusPayment, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Run basic test with LibEuFin. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts index f8bee7f16..f1d507c03 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { NexusUserBundle, LibeufinNexusApi, @@ -25,7 +25,7 @@ import { LibeufinSandboxService, LibeufinSandboxApi, findNexusPayment, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Run basic test with LibEuFin. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts index 1917c0c11..b106cf304 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts @@ -19,13 +19,13 @@ */ import axios from "axios"; import { URL } from "@gnu-taler/taler-util"; -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { SandboxUserBundle, NexusUserBundle, launchLibeufinServices, LibeufinNexusApi, -} from "./libeufin"; +} from "../harness/libeufin"; export async function runLibeufinApiFacadeBadRequestTest(t: GlobalTestState) { diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts index b0e569146..c49d49712 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts @@ -17,13 +17,13 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { SandboxUserBundle, NexusUserBundle, launchLibeufinServices, LibeufinNexusApi, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Run basic test with LibEuFin. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts index abb843c94..e64f459a0 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts @@ -17,12 +17,12 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { NexusUserBundle, LibeufinNexusApi, LibeufinNexusService, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Run basic test with LibEuFin. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts index ef8a1f2b5..f5df4cfa3 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { NexusUserBundle, LibeufinNexusApi, @@ -25,7 +25,7 @@ import { LibeufinSandboxService, LibeufinSandboxApi, findNexusPayment, -} from "./libeufin"; +} from "../harness/libeufin"; // This test only checks that LibEuFin doesn't fail when // it generates Camt statements - no assertions take place. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts index f9676c58c..a90644926 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { NexusUserBundle, LibeufinNexusApi, @@ -25,7 +25,7 @@ import { LibeufinSandboxService, LibeufinSandboxApi, findNexusPayment, -} from "./libeufin"; +} from "../harness/libeufin"; export async function runLibeufinApiSandboxTransactionsTest(t: GlobalTestState) { diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts index d543bc4ab..3863c5711 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { GlobalTestState, setupDb } from "./harness"; +import { GlobalTestState, setupDb } from "../harness/harness.js"; import { SandboxUserBundle, NexusUserBundle, @@ -25,7 +25,7 @@ import { LibeufinSandboxApi, LibeufinNexusApi, LibeufinNexusService, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Test Nexus scheduling API. It creates a task, check whether it shows @@ -72,7 +72,7 @@ export async function runLibeufinApiSchedulingTest(t: GlobalTestState) { user01nexus.localAccountName, "test-task", ); - } catch (err) { + } catch (err: any) { t.assertTrue(err.response.status == 404); } @@ -100,7 +100,7 @@ export async function runLibeufinApiSchedulingTest(t: GlobalTestState) { user01nexus.localAccountName, "test-task", ); - } catch (err) { + } catch (err: any) { t.assertTrue(err.response.status == 404); } } diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts index b53db4212..edf66690b 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; -import { LibeufinNexusApi, LibeufinNexusService } from "./libeufin"; +import { GlobalTestState } from "../harness/harness.js"; +import { LibeufinNexusApi, LibeufinNexusService } from "../harness/libeufin"; /** * Run basic test with LibEuFin. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts index 3da5850cf..786e61832 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts @@ -17,13 +17,13 @@ /** * Imports. */ -import { GlobalTestState, delayMs } from "./harness"; +import { GlobalTestState, delayMs } from "../harness/harness.js"; import { NexusUserBundle, LibeufinNexusApi, LibeufinNexusService, LibeufinSandboxService, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Testing how Nexus reacts when the Sandbox is unreachable. @@ -65,7 +65,7 @@ export async function runLibeufinBadGatewayTest(t: GlobalTestState) { libeufinNexus, user01nexus.connReq.name, ); - } catch(e) { + } catch(e: any) { t.assertTrue(e.response.status == 502); return; } diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts index b284d7299..9e1842d03 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts @@ -19,7 +19,7 @@ */ import { CoreApiResponse } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { CoinConfig, defaultCoinConfig } from "./denomStructures"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures"; import { DbInfo, HarnessExchangeBankAccount, @@ -28,14 +28,14 @@ import { MerchantService, setupDb, WalletCli, -} from "./harness"; -import { makeTestPayment } from "./helpers"; +} from "../harness/harness.js"; +import { makeTestPayment } from "../harness/helpers.js"; import { LibeufinNexusApi, LibeufinNexusService, LibeufinSandboxApi, LibeufinSandboxService, -} from "./libeufin"; +} from "../harness/libeufin"; const exchangeIban = "DE71500105179674997361"; const customerIban = "DE84500105176881385584"; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts index e45f0a239..5a995fb69 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts @@ -17,14 +17,14 @@ /** * Imports. */ -import { GlobalTestState, delayMs } from "./harness"; +import { GlobalTestState, delayMs } from "../harness/harness.js"; import { SandboxUserBundle, NexusUserBundle, launchLibeufinServices, LibeufinSandboxApi, LibeufinNexusApi, -} from "./libeufin"; +} from "../harness/libeufin"; /** * This test checks how the C52 and C53 coordinate. It'll test diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts index 143870128..0bbd4fd28 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts @@ -17,14 +17,14 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { SandboxUserBundle, NexusUserBundle, launchLibeufinServices, LibeufinNexusApi, LibeufinSandboxApi, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Testing the Anastasis API, offered by the Anastasis facade. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts index 8e527804c..5dc31f0bf 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts @@ -17,14 +17,14 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { SandboxUserBundle, NexusUserBundle, launchLibeufinServices, LibeufinSandboxApi, LibeufinNexusApi, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Run basic test with LibEuFin. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts index c00a102d3..23d76081f 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts @@ -17,14 +17,14 @@ /** * Imports. */ -import { GlobalTestState, delayMs } from "./harness"; +import { GlobalTestState, delayMs } from "../harness/harness.js"; import { SandboxUserBundle, NexusUserBundle, launchLibeufinServices, LibeufinSandboxApi, LibeufinNexusApi, -} from "./libeufin"; +} from "../harness/libeufin"; /** * This test checks how the C52 and C53 coordinate. It'll test diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts index 234a7bae8..39517f247 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts @@ -17,14 +17,14 @@ /** * Imports. */ -import { GlobalTestState, delayMs } from "./harness"; +import { GlobalTestState, delayMs } from "../harness/harness.js"; import { SandboxUserBundle, NexusUserBundle, launchLibeufinServices, LibeufinSandboxApi, LibeufinNexusApi, -} from "./libeufin"; +} from "../harness/libeufin"; /** * User 01 expects a refund from user 02, and expectedly user 03 diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts index 5d5370d02..d91ae88bb 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts @@ -17,14 +17,14 @@ /** * Imports. */ -import { GlobalTestState, delayMs } from "./harness"; +import { GlobalTestState, delayMs } from "../harness/harness.js"; import { SandboxUserBundle, NexusUserBundle, launchLibeufinServices, LibeufinSandboxApi, LibeufinNexusApi, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Run basic test with LibEuFin. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts index 503468990..5560f091a 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts @@ -17,7 +17,7 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { NexusUserBundle, LibeufinNexusApi, @@ -25,7 +25,7 @@ import { LibeufinSandboxService, LibeufinSandboxApi, findNexusPayment, -} from "./libeufin"; +} from "../harness/libeufin"; export async function runLibeufinSandboxWireTransferCliTest(t: GlobalTestState) { diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts index eee1b8935..71a1e8c4b 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts @@ -17,12 +17,12 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { LibeufinNexusService, LibeufinSandboxService, LibeufinCli, -} from "./libeufin"; +} from "../harness/libeufin"; /** * Run basic test with LibEuFin. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts index 4cf9c39b4..8e8f966b9 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts @@ -25,12 +25,12 @@ import { MerchantService, setupDb, WalletCli, -} from "./harness"; +} from "../harness/harness.js"; import { withdrawViaBank, createFaultInjectedMerchantTestkudosEnvironment, FaultyMerchantTestEnvironment, -} from "./helpers"; +} from "../harness/helpers.js"; import { PreparePayResultType, codecForMerchantOrderStatusUnpaid, @@ -41,8 +41,8 @@ import { FaultInjectedExchangeService, FaultInjectedMerchantService, FaultInjectionRequestContext, -} from "./faultInjection"; -import { defaultCoinConfig } from "./denomStructures"; +} from "../harness/faultInjection"; +import { defaultCoinConfig } from "../harness/denomStructures"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { URL } from "url"; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts index 28f729692..589c79120 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts @@ -25,7 +25,7 @@ import { MerchantApiClient, MerchantService, setupDb, -} from "./harness"; +} from "../harness/harness.js"; /** * Test instance deletion and authentication for it diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts index c2f7c5179..fc5e7305a 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts @@ -24,7 +24,7 @@ import { MerchantApiClient, MerchantService, setupDb, -} from "./harness"; +} from "../harness/harness.js"; /** * Do basic checks on instance management and authentication. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts index da45b4661..46af87922 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts @@ -25,7 +25,7 @@ import { MerchantApiClient, MerchantService, setupDb, -} from "./harness"; +} from "../harness/harness.js"; /** * Do basic checks on instance management and authentication. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts index 6516327c2..556d9074e 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; import { PreparePayResultType, codecForMerchantOrderStatusUnpaid, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts index dc7863874..466b1efbd 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts @@ -24,8 +24,8 @@ import { MerchantServiceInterface, WalletCli, ExchangeServiceInterface, -} from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +} from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; import { URL, durationFromSpec, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts index 867af99d5..70edaaf0c 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts @@ -32,11 +32,11 @@ import { MerchantPrivateApi, MerchantService, WalletCli, -} from "./harness"; +} from "../harness/harness.js"; import { createSimpleTestkudosEnvironment, withdrawViaBank, -} from "./helpers.js"; +} from "../harness/helpers.js"; const httpLib = new NodeHttpLib(); diff --git a/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts b/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts index 709ad1061..0fa9ec81d 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts @@ -27,12 +27,12 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { FaultInjectionRequestContext, FaultInjectionResponseContext, -} from "./faultInjection"; -import { GlobalTestState, MerchantPrivateApi, setupDb } from "./harness"; +} from "../harness/faultInjection"; +import { GlobalTestState, MerchantPrivateApi, setupDb } from "../harness/harness.js"; import { createFaultInjectedMerchantTestkudosEnvironment, withdrawViaBank, -} from "./helpers"; +} from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts b/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts index 64645dce2..2d291ddd3 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts @@ -17,11 +17,11 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; import { withdrawViaBank, createFaultInjectedMerchantTestkudosEnvironment, -} from "./helpers"; +} from "../harness/helpers.js"; import { PreparePayResultType, codecForMerchantOrderStatusUnpaid, @@ -29,7 +29,7 @@ import { URL, } from "@gnu-taler/taler-util"; import axios from "axios"; -import { FaultInjectionRequestContext } from "./faultInjection"; +import { FaultInjectionRequestContext } from "../harness/faultInjection"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; /** diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts index 9620db6d5..ba3bd8e0a 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi, WalletCli } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi, WalletCli } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; import { PreparePayResultType } from "@gnu-taler/taler-util"; import { TalerErrorCode } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts index 57ad6a4ff..2be01d919 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts @@ -31,14 +31,14 @@ import { MerchantPrivateApi, BankApi, BankAccessApi, -} from "./harness"; +} from "../harness/harness.js"; import { FaultInjectedExchangeService, FaultInjectionRequestContext, FaultInjectionResponseContext, -} from "./faultInjection"; +} from "../harness/faultInjection"; import { CoreApiResponse } from "@gnu-taler/taler-util"; -import { defaultCoinConfig } from "./denomStructures"; +import { defaultCoinConfig } from "../harness/denomStructures"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; /** diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts index 49ffadc93..3bdd6bef3 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts @@ -17,12 +17,12 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironment, withdrawViaBank, makeTestPayment, -} from "./helpers"; +} from "../harness/helpers.js"; /** * Run test for payment with a contract that has forgettable fields. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts index 58c951b68..9378465a0 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; import { PreparePayResultType } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts index f545d5861..754c3a0e8 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts @@ -25,9 +25,9 @@ import { MerchantService, WalletCli, MerchantPrivateApi, -} from "./harness"; -import { withdrawViaBank } from "./helpers"; -import { coin_ct10, coin_u1 } from "./denomStructures"; +} from "../harness/harness.js"; +import { withdrawViaBank } from "../harness/helpers.js"; +import { coin_ct10, coin_u1 } from "../harness/denomStructures"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; async function setupTest( diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts index 0dabc9ca5..1d419fd9a 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts @@ -22,10 +22,10 @@ import { BankApi, WalletCli, BankAccessApi -} from "./harness"; +} from "../harness/harness.js"; import { makeTestPayment, -} from "./helpers"; +} from "../harness/helpers.js"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; /** diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts index b171ff66a..75d44d495 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts @@ -17,16 +17,16 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; import { withdrawViaBank, createFaultInjectedMerchantTestkudosEnvironment, -} from "./helpers"; +} from "../harness/helpers.js"; import axios from "axios"; import { FaultInjectionRequestContext, FaultInjectionResponseContext, -} from "./faultInjection"; +} from "../harness/faultInjection"; import { codecForMerchantOrderStatusUnpaid, ConfirmPayResultType, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts index 771ca27e8..c38b8b382 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts @@ -18,12 +18,12 @@ * Imports. */ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironment, withdrawViaBank, makeTestPayment, -} from "./helpers"; +} from "../harness/helpers.js"; /** * Run test for a payment for a "free" order with diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment.ts index 3512ff046..967d491be 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment.ts @@ -17,12 +17,12 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironment, withdrawViaBank, makeTestPayment, -} from "./helpers"; +} from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal and payment. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts b/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts index 04eee79e3..a8e3b3e95 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; import { PreparePayResultType, codecForMerchantOrderStatusUnpaid, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts index f1e79f4b9..230fc942d 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; import { durationFromSpec } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts index b4276248e..acb74b3d3 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts @@ -17,12 +17,12 @@ /** * Imports. */ -import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; import { createSimpleTestkudosEnvironment, withdrawViaBank, applyTimeTravel, -} from "./helpers"; +} from "../harness/helpers.js"; import { durationFromSpec, timestampAddDuration, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts index 11e1226d1..47c2293e2 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, delayMs, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, delayMs, MerchantPrivateApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; import { TransactionType, Amounts, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund.ts index 1808f7d73..f11771922 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-refund.ts @@ -19,8 +19,8 @@ */ import { durationFromSpec } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts b/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts index fc1ffb267..276c532b5 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts @@ -18,7 +18,7 @@ * Imports. */ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { CoinConfig } from "./denomStructures"; +import { CoinConfig } from "../harness/denomStructures"; import { GlobalTestState, ExchangeService, @@ -27,12 +27,12 @@ import { setupDb, BankService, delayMs, -} from "./harness"; +} from "../harness/harness.js"; import { withdrawViaBank, makeTestPayment, SimpleTestEnvironment, -} from "./helpers"; +} from "../harness/helpers.js"; async function revokeAllWalletCoins(req: { wallet: WalletCli; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts index bad821198..e20d8bdad 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts @@ -27,7 +27,7 @@ import { PendingOperationsResponse, WalletApiOperation, } from "@gnu-taler/taler-wallet-core"; -import { makeNoFeeCoinConfig } from "./denomStructures"; +import { makeNoFeeCoinConfig } from "../harness/denomStructures"; import { BankService, ExchangeService, @@ -36,8 +36,8 @@ import { MerchantService, setupDb, WalletCli, -} from "./harness"; -import { startWithdrawViaBank, withdrawViaBank } from "./helpers"; +} from "../harness/harness.js"; +import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js"; async function applyTimeTravel( timetravelDuration: Duration, diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts index b9e45c862..2ff857057 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts @@ -17,12 +17,12 @@ /** * Imports. */ -import { GlobalTestState } from "./harness"; +import { GlobalTestState } from "../harness/harness.js"; import { createSimpleTestkudosEnvironment, withdrawViaBank, startWithdrawViaBank, -} from "./helpers"; +} from "../harness/helpers.js"; import { Duration, TransactionType } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts index 2421b462f..c6a7f8402 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts @@ -18,8 +18,8 @@ * Imports. */ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi, BankApi } from "./harness"; -import { createSimpleTestkudosEnvironment } from "./helpers"; +import { GlobalTestState, MerchantPrivateApi, BankApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts index 7debfe6b6..23e01e5e1 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts @@ -18,9 +18,9 @@ * Imports. */ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, WalletCli } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; -import { SyncService } from "./sync"; +import { GlobalTestState, WalletCli } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; +import { SyncService } from "../harness/sync"; /** * Run test for basic, bank-integrated withdrawal. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts index ab2687fc3..8c20dcc2b 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts @@ -19,13 +19,13 @@ */ import { PreparePayResultType } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, WalletCli, MerchantPrivateApi } from "./harness"; +import { GlobalTestState, WalletCli, MerchantPrivateApi } from "../harness/harness.js"; import { createSimpleTestkudosEnvironment, makeTestPayment, withdrawViaBank, -} from "./helpers"; -import { SyncService } from "./sync"; +} from "../harness/helpers.js"; +import { SyncService } from "../harness/sync"; /** * Run test for basic, bank-integrated withdrawal. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts index 2499e65a0..c21a7279b 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts @@ -24,7 +24,7 @@ */ import { Amounts } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { CoinConfig, defaultCoinConfig } from "./denomStructures.js"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { BankService, ExchangeService, @@ -32,8 +32,8 @@ import { MerchantService, setupDb, WalletCli, -} from "./harness.js"; -import { SimpleTestEnvironment } from "./helpers.js"; +} from "../harness/harness.js"; +import { SimpleTestEnvironment } from "../harness/helpers.js"; const merchantAuthToken = "secret-token:sandbox"; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts index 896b1e877..fe719ea62 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts @@ -19,8 +19,8 @@ */ import { TalerErrorCode } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, BankApi, BankAccessApi } from "./harness"; -import { createSimpleTestkudosEnvironment } from "./helpers"; +import { GlobalTestState, BankApi, BankAccessApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; /** * Run test for basic, bank-integrated withdrawal. diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts index 4a02b2708..35969c78f 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, BankApi, BankAccessApi } from "./harness"; -import { createSimpleTestkudosEnvironment } from "./helpers"; +import { GlobalTestState, BankApi, BankAccessApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; import { codecForBalancesResponse } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts index bfe29b322..97beba1bf 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts @@ -24,10 +24,10 @@ import { setupDb, ExchangeService, FakeBankService, -} from "./harness"; -import { createSimpleTestkudosEnvironment } from "./helpers"; +} from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { CoinConfig, defaultCoinConfig } from "./denomStructures.js"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { URL } from "@gnu-taler/taler-util"; /** diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts index fe8fd3c56..b93d1b500 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts @@ -17,8 +17,8 @@ /** * Imports. */ -import { GlobalTestState, BankApi } from "./harness"; -import { createSimpleTestkudosEnvironment } from "./helpers"; +import { GlobalTestState, BankApi } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; /** diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts index bcb0dd271..d985ed67f 100644 --- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts +++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts @@ -22,7 +22,7 @@ import { runTestWithState, shouldLingerInTest, TestRunResult, -} from "./harness"; +} from "../harness/harness.js"; import { runPaymentTest } from "./test-payment"; import { runPaymentDemoTest } from "./test-payment-on-demo"; import * as fs from "fs"; diff --git a/packages/taler-wallet-cli/src/lint.ts b/packages/taler-wallet-cli/src/lint.ts index 0fed68c34..2b888ccf4 100644 --- a/packages/taler-wallet-cli/src/lint.ts +++ b/packages/taler-wallet-cli/src/lint.ts @@ -43,7 +43,7 @@ import { } from "@gnu-taler/taler-wallet-core"; import { URL } from "url"; import { spawn } from "child_process"; -import { delayMs } from "./integrationtests/harness.js"; +import { delayMs } from "./harness/harness.js"; interface BasicConf { mainCurrency: string; diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index c5bf2c8c0..991c03ee2 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -118,6 +118,10 @@ export enum WalletApiOperation { } export type WalletOperations = { + [WalletApiOperation.InitWallet]: { + request: {}; + response: {}; + }; [WalletApiOperation.WithdrawFakebank]: { request: WithdrawFakebankRequest; response: {}; -- cgit v1.2.3