From a5681579fbddb001f5b7118fe705c6643581c722 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 12 Jan 2021 20:04:16 +0100 Subject: make integration tests part of taler-wallet-cli --- packages/taler-integrationtests/package.json | 25 - packages/taler-integrationtests/scenario | 26 - .../taler-integrationtests/src/denomStructures.ts | 151 -- .../taler-integrationtests/src/faultInjection.ts | 263 --- packages/taler-integrationtests/src/harness.ts | 1749 -------------------- packages/taler-integrationtests/src/helpers.ts | 370 ----- .../taler-integrationtests/src/merchantApiTypes.ts | 304 ---- .../src/scenario-prompt-payment.ts | 60 - .../src/scenario-rerun-payment-multiple.ts | 128 -- .../taler-integrationtests/src/test-bank-api.ts | 137 -- .../taler-integrationtests/src/test-claim-loop.ts | 83 - .../src/test-exchange-management.ts | 250 --- .../src/test-fee-regression.ts | 206 --- .../src/test-merchant-longpolling.ts | 132 -- .../src/test-merchant-refund-api.ts | 286 ---- .../taler-integrationtests/src/test-pay-abort.ts | 202 --- .../taler-integrationtests/src/test-pay-paid.ts | 206 --- .../src/test-payment-claim.ts | 109 -- .../src/test-payment-fault.ts | 210 --- .../src/test-payment-idempotency.ts | 103 -- .../src/test-payment-multiple.ts | 161 -- .../src/test-payment-transient.ts | 172 -- .../taler-integrationtests/src/test-payment.ts | 53 - .../src/test-paywall-flow.ts | 233 --- .../taler-integrationtests/src/test-refund-auto.ts | 100 -- .../taler-integrationtests/src/test-refund-gone.ts | 127 -- .../src/test-refund-incremental.ts | 191 --- packages/taler-integrationtests/src/test-refund.ts | 103 -- .../taler-integrationtests/src/test-revocation.ts | 122 -- .../src/test-timetravel-autorefresh.ts | 204 --- .../src/test-timetravel-withdraw.ts | 90 - .../taler-integrationtests/src/test-tipping.ts | 128 -- .../src/test-wallettesting.ts | 87 - .../src/test-withdrawal-abort-bank.ts | 67 - .../src/test-withdrawal-bank-integrated.ts | 71 - .../src/test-withdrawal-manual.ts | 78 - packages/taler-integrationtests/testrunner | 77 - packages/taler-integrationtests/tsconfig.json | 32 - packages/taler-wallet-cli/package.json | 2 + packages/taler-wallet-cli/src/index.ts | 20 + .../src/integrationtests/denomStructures.ts | 151 ++ .../src/integrationtests/faultInjection.ts | 263 +++ .../src/integrationtests/harness.ts | 1718 +++++++++++++++++++ .../src/integrationtests/helpers.ts | 370 +++++ .../src/integrationtests/merchantApiTypes.ts | 304 ++++ .../integrationtests/scenario-prompt-payment.ts | 60 + .../src/integrationtests/test-bank-api.ts | 136 ++ .../src/integrationtests/test-claim-loop.ts | 81 + .../integrationtests/test-exchange-management.ts | 249 +++ .../src/integrationtests/test-fee-regression.ts | 204 +++ .../integrationtests/test-merchant-longpolling.ts | 132 ++ .../integrationtests/test-merchant-refund-api.ts | 284 ++++ .../src/integrationtests/test-pay-abort.ts | 199 +++ .../src/integrationtests/test-pay-paid.ts | 206 +++ .../src/integrationtests/test-payment-claim.ts | 104 ++ .../src/integrationtests/test-payment-fault.ts | 209 +++ .../integrationtests/test-payment-idempotency.ts | 103 ++ .../src/integrationtests/test-payment-multiple.ts | 160 ++ .../src/integrationtests/test-payment-transient.ts | 172 ++ .../src/integrationtests/test-payment.ts | 53 + .../src/integrationtests/test-paywall-flow.ts | 233 +++ .../src/integrationtests/test-refund-auto.ts | 100 ++ .../src/integrationtests/test-refund-gone.ts | 127 ++ .../integrationtests/test-refund-incremental.ts | 186 +++ .../src/integrationtests/test-refund.ts | 103 ++ .../src/integrationtests/test-revocation.ts | 120 ++ .../test-timetravel-autorefresh.ts | 203 +++ .../integrationtests/test-timetravel-withdraw.ts | 90 + .../src/integrationtests/test-tipping.ts | 127 ++ .../src/integrationtests/test-wallettesting.ts | 87 + .../integrationtests/test-withdrawal-abort-bank.ts | 67 + .../test-withdrawal-bank-integrated.ts | 71 + .../src/integrationtests/test-withdrawal-manual.ts | 78 + .../src/integrationtests/testrunner.ts | 176 ++ 74 files changed, 6948 insertions(+), 7096 deletions(-) delete mode 100644 packages/taler-integrationtests/package.json delete mode 100755 packages/taler-integrationtests/scenario delete mode 100644 packages/taler-integrationtests/src/denomStructures.ts delete mode 100644 packages/taler-integrationtests/src/faultInjection.ts delete mode 100644 packages/taler-integrationtests/src/harness.ts delete mode 100644 packages/taler-integrationtests/src/helpers.ts delete mode 100644 packages/taler-integrationtests/src/merchantApiTypes.ts delete mode 100644 packages/taler-integrationtests/src/scenario-prompt-payment.ts delete mode 100644 packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts delete mode 100644 packages/taler-integrationtests/src/test-bank-api.ts delete mode 100644 packages/taler-integrationtests/src/test-claim-loop.ts delete mode 100644 packages/taler-integrationtests/src/test-exchange-management.ts delete mode 100644 packages/taler-integrationtests/src/test-fee-regression.ts delete mode 100644 packages/taler-integrationtests/src/test-merchant-longpolling.ts delete mode 100644 packages/taler-integrationtests/src/test-merchant-refund-api.ts delete mode 100644 packages/taler-integrationtests/src/test-pay-abort.ts delete mode 100644 packages/taler-integrationtests/src/test-pay-paid.ts delete mode 100644 packages/taler-integrationtests/src/test-payment-claim.ts delete mode 100644 packages/taler-integrationtests/src/test-payment-fault.ts delete mode 100644 packages/taler-integrationtests/src/test-payment-idempotency.ts delete mode 100644 packages/taler-integrationtests/src/test-payment-multiple.ts delete mode 100644 packages/taler-integrationtests/src/test-payment-transient.ts delete mode 100644 packages/taler-integrationtests/src/test-payment.ts delete mode 100644 packages/taler-integrationtests/src/test-paywall-flow.ts delete mode 100644 packages/taler-integrationtests/src/test-refund-auto.ts delete mode 100644 packages/taler-integrationtests/src/test-refund-gone.ts delete mode 100644 packages/taler-integrationtests/src/test-refund-incremental.ts delete mode 100644 packages/taler-integrationtests/src/test-refund.ts delete mode 100644 packages/taler-integrationtests/src/test-revocation.ts delete mode 100644 packages/taler-integrationtests/src/test-timetravel-autorefresh.ts delete mode 100644 packages/taler-integrationtests/src/test-timetravel-withdraw.ts delete mode 100644 packages/taler-integrationtests/src/test-tipping.ts delete mode 100644 packages/taler-integrationtests/src/test-wallettesting.ts delete mode 100644 packages/taler-integrationtests/src/test-withdrawal-abort-bank.ts delete mode 100644 packages/taler-integrationtests/src/test-withdrawal-bank-integrated.ts delete mode 100644 packages/taler-integrationtests/src/test-withdrawal-manual.ts delete mode 100755 packages/taler-integrationtests/testrunner delete mode 100644 packages/taler-integrationtests/tsconfig.json create mode 100644 packages/taler-wallet-cli/src/integrationtests/denomStructures.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/faultInjection.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/harness.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/helpers.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-refund.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-revocation.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-tipping.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts create mode 100644 packages/taler-wallet-cli/src/integrationtests/testrunner.ts (limited to 'packages') diff --git a/packages/taler-integrationtests/package.json b/packages/taler-integrationtests/package.json deleted file mode 100644 index 7da65b144..000000000 --- a/packages/taler-integrationtests/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "taler-integrationtests", - "version": "0.0.1", - "description": "Integration tests and fault injection for GNU Taler components", - "main": "index.js", - "scripts": { - "compile": "tsc -b", - "pretty": "prettier --write src" - }, - "author": "Florian Dold ", - "license": "AGPL-3.0-or-later", - "devDependencies": { - "esm": "^3.2.25", - "nyc": "^15.1.0", - "prettier": "^2.1.2", - "source-map-support": "^0.5.19", - "ts-node": "^9.0.0", - "typescript": "^4.0.5" - }, - "dependencies": { - "axios": "^0.21.0", - "taler-wallet-core": "workspace:*", - "tslib": "^2.0.3" - } -} diff --git a/packages/taler-integrationtests/scenario b/packages/taler-integrationtests/scenario deleted file mode 100755 index 9bef68ffa..000000000 --- a/packages/taler-integrationtests/scenario +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -# Simple test runner for the wallet integration tests. -# -# Usage: $0 TESTGLOB -# -# The TESTGLOB can be used to select which test cases to execute - -set -eu - -if [ "$#" -ne 1 ]; then - echo "Usage: $0 SCENARIO" - exit 1 -fi - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" - -cd $DIR - -./node_modules/.bin/tsc -b - -export ESM_OPTIONS='{"sourceMap": true}' - -file=lib/scenario-$1.js - -exec node -r source-map-support/register -r esm $file diff --git a/packages/taler-integrationtests/src/denomStructures.ts b/packages/taler-integrationtests/src/denomStructures.ts deleted file mode 100644 index 5ab9aca00..000000000 --- a/packages/taler-integrationtests/src/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-integrationtests/src/faultInjection.ts b/packages/taler-integrationtests/src/faultInjection.ts deleted file mode 100644 index a2d4836d9..000000000 --- a/packages/taler-integrationtests/src/faultInjection.ts +++ /dev/null @@ -1,263 +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, - BankService, - ExchangeServiceInterface, - MerchantServiceInterface, - MerchantService, - PrivateOrderStatusQuery, -} from "./harness"; -import { - PostOrderRequest, - PostOrderResponse, - MerchantOrderPrivateStatusResponse, -} from "./merchantApiTypes"; - -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) => void; - modifyResponse?: (ctx: FaultInjectionResponseContext) => void; -} - -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", () => { - 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) { - 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", () => { - 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) { - modResponse(faultRespContext); - } - } - if (faultRespContext.dropResponse) { - req.destroy(); - return; - } - if (faultRespContext.responseBody) { - // We must accomodate 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-integrationtests/src/harness.ts b/packages/taler-integrationtests/src/harness.ts deleted file mode 100644 index 58bcf2cf4..000000000 --- a/packages/taler-integrationtests/src/harness.ts +++ /dev/null @@ -1,1749 +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 os from "os"; -import * as http from "http"; -import { deepStrictEqual } from "assert"; -import { ChildProcess, spawn } from "child_process"; -import { - Configuration, - AmountJson, - Amounts, - Codec, - buildCodecForObject, - codecForString, - Duration, - CoreApiResponse, - PreparePayResult, - PreparePayRequest, - codecForPreparePayResult, - OperationFailedError, - AddExchangeRequest, - ExchangesListRespose, - codecForExchangesListResponse, - GetWithdrawalDetailsForUriRequest, - WithdrawUriInfoResponse, - codecForWithdrawUriInfoResponse, - ConfirmPayRequest, - ConfirmPayResult, - codecForConfirmPayResult, - IntegrationTestArgs, - TestPayArgs, - BalancesResponse, - codecForBalancesResponse, - encodeCrock, - getRandomBytes, - EddsaKeyPair, - eddsaGetPublic, - createEddsaKeyPair, - TransactionsResponse, - codecForTransactionsResponse, - WithdrawTestBalanceRequest, - AmountString, - ApplyRefundRequest, - codecForApplyRefundResponse, - codecForAny, - CoinDumpJson, - ForceExchangeUpdateRequest, - ForceRefreshRequest, - PrepareTipResult, - PrepareTipRequest, - codecForPrepareTipResult, - AcceptTipRequest, - AbortPayWithRefundRequest, -} from "taler-wallet-core"; -import { URL } from "url"; -import axios, { AxiosError } from "axios"; -import { - codecForMerchantOrderPrivateStatusResponse, - codecForPostOrderResponse, - PostOrderRequest, - PostOrderResponse, - MerchantOrderPrivateStatusResponse, - TippingReserveStatus, - TipCreateConfirmation, - TipCreateRequest, -} from "./merchantApiTypes"; -import { ApplyRefundResponse } from "taler-wallet-core"; -import { PendingOperationsResponse } from "taler-wallet-core"; -import { CoinConfig } from "./denomStructures"; - -const exec = util.promisify(require("child_process").exec); - -export async function delayMs(ms: number): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(), ms); - }); -} - -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, -): Promise { - console.log("runing command", command); - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const proc = spawn(command, { - stdio: ["inherit", "pipe", "pipe"], - shell: true, - }); - 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[], -): Promise { - console.log("runing command", shellescape([command, ...args])); - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const proc = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - shell: false, - }); - 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 = []; - - process.on("SIGINT", () => this.shutdownSync()); - process.on("SIGTERM", () => this.shutdownSync()); - process.on("unhandledRejection", () => this.shutdownSync()); - process.on("uncaughtException", () => this.shutdownSync()); - } - - 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 { - return e.isAxiosError; - } - - 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)}`, - ); - } - } - - private 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"); - } else { - } - } - console.log("*** test harness interrupted"); - console.log("*** test state can be found under", this.testDir); - process.exit(1); - } - - spawnService( - command: string, - args: string[], - logName: string, - ): ProcessWrapper { - console.log( - `spawning process (${logName}): ${shellescape([command, ...args])}`, - ); - const proc = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - }); - 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; - } - this.inShutdown = true; - console.log("shutting down"); - if (shouldLingerAlways()) { - console.log("*** test finished, but requested to linger"); - console.log("*** test state can be found under", this.testDir); - return; - } - 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 interface TalerConfigSection { - options: Record; -} - -export interface TalerConfig { - sections: Record; -} - -export interface DbInfo { - 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; -} - -function setPaths(config: Configuration, home: string) { - config.setString("paths", "taler_home", home); - config.setString("paths", "taler_runtime_dir", "$TALER_HOME/taler-runtime/"); - 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/"); - config.setString( - "paths", - "taler_runtime_dir", - "${TMPDIR:-${TMP:-/tmp}}/taler-system-runtime/", - ); -} - -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}`); -} - -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) { - 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 ExchangeBankAccount { - 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: ExchangeBankAccount; - 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(); - setPaths(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 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"], - ); - } - - 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`, - ); - setPaths(config, gc.testDir + "/talerhome"); - - config.setString( - "exchange", - "keydir", - "${TALER_DATA_HOME}/exchange/live-keys/", - ); - config.setString( - "exchage", - "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("exchange", "signkey_duration", "4 weeks"); - config.setString("exchange", "legal_duraction", "2 years"); - config.setString("exchange", "lookahead_sign", "32 weeks 1 day"); - config.setString("exchange", "lookahead_provide", "4 weeks 1 day"); - - config.setString("exchangedb-postgres", "config", e.database); - - 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: ExchangeBankAccount, - ): 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-account-${localName}`, - "wire_gateway_url", - exchangeBankAccount.wireGatewayApiBaseUrl, - ); - config.setString( - `exchange-account-${localName}`, - "wire_gateway_auth_method", - "basic", - ); - config.setString( - `exchange-account-${localName}`, - "username", - exchangeBankAccount.accountName, - ); - config.setString( - `exchange-account-${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, - ...this.timetravelArgArr, - "download", - "sign", - "upload", - ], - ); - - const accounts: string[] = []; - - const config = Configuration.load(this.configFilename); - for (const sectionName of config.getSectionNames()) { - if (sectionName.startsWith("exchange-account")) { - accounts.push(config.getString(sectionName, "payto_uri").required()); - } - } - - console.log("configuring bank accounts", accounts); - - for (const acc of accounts) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - ...this.timetravelArgArr, - "enable-account", - acc, - "upload", - ], - ); - } - - const year = new Date().getFullYear(); - for (let i = year; i < year+5; i++) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - ...this.timetravelArgArr, - "wire-fee", - `${i}`, - "x-taler-bank", - `${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, - ...this.timetravelArgArr, - "revoke-denomination", - denomPubHash, - "upload", - ], - ); - } - - 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-helper-crypto-eddsa", - ["-c", this.configFilename, ...this.timetravelArgArr], - `exchange-crypto-eddsa-${this.name}`, - ); - - this.helperCryptoRsaProc = this.globalState.spawnService( - "taler-helper-crypto-rsa", - ["-c", this.configFilename, ...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, - "--num-threads", - "1", - ...this.timetravelArgArr, - ], - `exchange-httpd-${this.name}`, - ); - - await this.keyup(); - } - - async pingUntilAvailable(): Promise { - const url = `http://localhost:${this.exchangeConfig.httpPort}/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 namespace MerchantPrivateApi { - export async function createOrder( - merchantService: MerchantServiceInterface, - instanceName: string, - req: PostOrderRequest, - ): Promise { - const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); - let url = new URL("private/orders", baseUrl); - const resp = await axios.post(url.href, req); - return codecForPostOrderResponse().decode(resp.data); - } - - export async function queryPrivateOrderStatus( - merchantService: MerchantServiceInterface, - query: PrivateOrderStatusQuery, - ): 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); - 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`; - setPaths(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 addInstance(instanceConfig: MerchantInstanceConfig): Promise { - if (!this.proc) { - throw Error("merchant must be running to add instance"); - } - console.log("adding instance"); - const url = `http://localhost:${this.merchantConfig.httpPort}/private/instances`; - await axios.post(url, { - 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 MerchantInstanceConfig { - id: string; - name: string; - paytoUris: string[]; - address?: unknown; - jurisdiction?: unknown; - defaultMaxWireFee?: string; - defaultMaxDepositFee?: string; - defaultWireFeeAmortization?: number; - defaultWireTransferDelay?: Duration; - defaultPayDelay?: Duration; -} - -/** - * Check if the test should hang around after it failed. - */ -function shouldLinger(): boolean { - return ( - process.env["TALER_TEST_LINGER"] == "1" || - process.env["TALER_TEST_LINGER_ALWAYS"] == "1" - ); -} - -/** - * Check if the test should hang around even after it finished - * successfully. - */ -function shouldLingerAlways(): boolean { - return process.env["TALER_TEST_LINGER_ALWAYS"] == "1"; -} - -function updateCurrentSymlink(testDir: string): void { - const currLink = path.join(os.tmpdir(), "taler-integrationtest-current"); - try { - fs.unlinkSync(currLink); - } catch (e) { - // Ignore - } - try { - fs.symlinkSync(testDir, currLink); - } catch (e) { - console.log(e); - // Ignore - } -} - -export function runTestWithState( - gc: GlobalTestState, - testMain: (t: GlobalTestState) => Promise, -) { - const main = async () => { - let ret = 0; - try { - updateCurrentSymlink(gc.testDir); - console.log("running test in directory", gc.testDir); - await testMain(gc); - } catch (e) { - console.error("FATAL: test failed with exception", e); - ret = 1; - } finally { - if (gc) { - if (shouldLinger()) { - console.log("test logs and config can be found under", gc.testDir); - console.log("keeping test environment running"); - } else { - await gc.shutdown(); - console.log("test logs and config can be found under", gc.testDir); - process.exit(ret); - } - } - } - }; - - main(); -} - -export function runTest( - testMain: (gc: GlobalTestState) => Promise, -): void { - const gc = new GlobalTestState({ - testDir: fs.mkdtempSync(path.join(os.tmpdir(), "taler-integrationtest-")), - }); - runTestWithState(gc, testMain); -} - -function shellWrap(s: string) { - return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"; -} - -export class WalletCli { - private currentTimetravel: Duration | undefined; - - 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", - ) {} - - 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 []; - } - - async apiRequest( - request: string, - payload: unknown, - ): Promise { - const resp = await sh( - this.globalTestState, - `wallet-${this.name}`, - `taler-wallet-cli ${ - this.timetravelArg ?? "" - } --no-throttle --wallet-db '${this.dbfile}' api '${request}' ${shellWrap( - JSON.stringify(payload), - )}`, - ); - console.log(resp); - return JSON.parse(resp) as CoreApiResponse; - } - - 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", - ], - ); - } - - async applyRefund(req: ApplyRefundRequest): Promise { - const resp = await this.apiRequest("applyRefund", req); - if (resp.type === "response") { - return codecForApplyRefundResponse().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } - - async preparePay(req: PreparePayRequest): Promise { - const resp = await this.apiRequest("preparePay", req); - if (resp.type === "response") { - return codecForPreparePayResult().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } - - async abortFailedPayWithRefund( - req: AbortPayWithRefundRequest, - ): Promise { - const resp = await this.apiRequest("abortFailedPayWithRefund", req); - if (resp.type === "response") { - return; - } - throw new OperationFailedError(resp.error); - } - - async confirmPay(req: ConfirmPayRequest): Promise { - const resp = await this.apiRequest("confirmPay", req); - if (resp.type === "response") { - return codecForConfirmPayResult().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } - - async prepareTip(req: PrepareTipRequest): Promise { - const resp = await this.apiRequest("prepareTip", req); - if (resp.type === "response") { - return codecForPrepareTipResult().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } - - async acceptTip(req: AcceptTipRequest): Promise { - const resp = await this.apiRequest("acceptTip", req); - if (resp.type === "response") { - return; - } - throw new OperationFailedError(resp.error); - } - - async dumpCoins(): Promise { - const resp = await this.apiRequest("dumpCoins", {}); - if (resp.type === "response") { - return codecForAny().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } - - async addExchange(req: AddExchangeRequest): Promise { - const resp = await this.apiRequest("addExchange", req); - if (resp.type === "response") { - return; - } - throw new OperationFailedError(resp.error); - } - - async forceUpdateExchange(req: ForceExchangeUpdateRequest): Promise { - const resp = await this.apiRequest("forceUpdateExchange", req); - if (resp.type === "response") { - return; - } - throw new OperationFailedError(resp.error); - } - - async forceRefresh(req: ForceRefreshRequest): Promise { - const resp = await this.apiRequest("forceRefresh", req); - if (resp.type === "response") { - return; - } - throw new OperationFailedError(resp.error); - } - - async listExchanges(): Promise { - const resp = await this.apiRequest("listExchanges", {}); - if (resp.type === "response") { - return codecForExchangesListResponse().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } - - async getBalances(): Promise { - const resp = await this.apiRequest("getBalances", {}); - if (resp.type === "response") { - return codecForBalancesResponse().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } - - async getPendingOperations(): Promise { - const resp = await this.apiRequest("getPendingOperations", {}); - if (resp.type === "response") { - // FIXME: validate properly! - return codecForAny().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } - - async getTransactions(): Promise { - const resp = await this.apiRequest("getTransactions", {}); - if (resp.type === "response") { - return codecForTransactionsResponse().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } - - async runIntegrationTest(args: IntegrationTestArgs): Promise { - const resp = await this.apiRequest("runIntegrationTest", args); - if (resp.type === "response") { - return; - } - throw new OperationFailedError(resp.error); - } - - async testPay(args: TestPayArgs): Promise { - const resp = await this.apiRequest("testPay", args); - if (resp.type === "response") { - return; - } - throw new OperationFailedError(resp.error); - } - - async withdrawTestBalance(args: WithdrawTestBalanceRequest): Promise { - const resp = await this.apiRequest("withdrawTestBalance", args); - if (resp.type === "response") { - return; - } - throw new OperationFailedError(resp.error); - } - - async getWithdrawalDetailsForUri( - req: GetWithdrawalDetailsForUriRequest, - ): Promise { - const resp = await this.apiRequest("getWithdrawalDetailsForUri", req); - if (resp.type === "response") { - return codecForWithdrawUriInfoResponse().decode(resp.result); - } - throw new OperationFailedError(resp.error); - } -} diff --git a/packages/taler-integrationtests/src/helpers.ts b/packages/taler-integrationtests/src/helpers.ts deleted file mode 100644 index f4e676b61..000000000 --- a/packages/taler-integrationtests/src/helpers.ts +++ /dev/null @@ -1,370 +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 { - GlobalTestState, - DbInfo, - ExchangeService, - WalletCli, - MerchantService, - setupDb, - BankService, - ExchangeBankAccount, - MerchantServiceInterface, - BankApi, - BankAccessApi, - MerchantPrivateApi, - ExchangeServiceInterface, -} from "./harness"; -import { - AmountString, - Duration, - PreparePayResultType, - ConfirmPayResultType, - ContractTerms, -} from "taler-wallet-core"; -import { FaultInjectedMerchantService } from "./faultInjection"; -import { defaultCoinConfig } from "./denomStructures"; - -export interface SimpleTestEnvironment { - commonDb: DbInfo; - bank: BankService; - exchange: ExchangeService; - exchangeBankAccount: ExchangeBankAccount; - merchant: MerchantService; - wallet: WalletCli; -} - -/** - * 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, -): 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.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - 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; - exchangeBankAccount: ExchangeBankAccount; - 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 exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - faultyMerchant, - }; -} - -/** - * 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 - - const r1 = await wallet.apiRequest("getWithdrawalDetailsForUri", { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - t.assertTrue(r1.type === "response"); - - await wallet.runPending(); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bank, user, wop); - - // Withdraw - - const r2 = await wallet.apiRequest("acceptBankIntegratedWithdrawal", { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }); - t.assertTrue(r2.type === "response"); -} - -/** - * 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 - - const balApiResp = await wallet.apiRequest("getBalances", {}); - t.assertTrue(balApiResp.type === "response"); -} - -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; - }, -): Promise { - // Set up order. - - const { wallet, merchant } = args; - const instance = args.instance ?? "default"; - - const orderResp = await MerchantPrivateApi.createOrder(merchant, instance, { - order: args.order, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const preparePayResult = await wallet.preparePay({ - talerPayUri: orderStatus.taler_pay_uri, - }); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - const r2 = await wallet.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, - }); - - t.assertTrue(orderStatus.order_status === "paid"); -} diff --git a/packages/taler-integrationtests/src/merchantApiTypes.ts b/packages/taler-integrationtests/src/merchantApiTypes.ts deleted file mode 100644 index 6782391a2..000000000 --- a/packages/taler-integrationtests/src/merchantApiTypes.ts +++ /dev/null @@ -1,304 +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, -} from "taler-wallet-core"; -import { codecForAmountString } from "taler-wallet-core/lib/util/amounts"; - -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< - CheckPaymentPaidResponse -> => - 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< - CheckPaymentUnpaidResponse -> => - 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< - CheckPaymentClaimedResponse -> => - buildCodecForObject() - .property("order_status", codecForConstString("claimed")) - .property("contract_terms", codecForContractTerms()) - .build("CheckPaymentClaimedResponse"); - -export const codecForMerchantOrderPrivateStatusResponse = (): Codec< - MerchantOrderPrivateStatusResponse -> => - 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 transfered - // 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; -} diff --git a/packages/taler-integrationtests/src/scenario-prompt-payment.ts b/packages/taler-integrationtests/src/scenario-prompt-payment.ts deleted file mode 100644 index 3c34075d1..000000000 --- a/packages/taler-integrationtests/src/scenario-prompt-payment.ts +++ /dev/null @@ -1,60 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - console.log(orderStatus); - - // Wait "forever" - await new Promise(() => {}); -}); diff --git a/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts b/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts deleted file mode 100644 index 3a98987b3..000000000 --- a/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts +++ /dev/null @@ -1,128 +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 - */ - -/** - * Imports. - */ -import { - GlobalTestState, - BankService, - ExchangeService, - MerchantService, - WalletCli, - runTestWithState, - MerchantPrivateApi, -} from "./harness"; -import { withdrawViaBank } from "./helpers"; -import fs from "fs"; - -let existingTestDir = - process.env["TALER_TEST_OLD_DIR"] ?? "/tmp/taler-integrationtest-current"; - -if (!fs.existsSync(existingTestDir)) { - throw Error("old test dir not found"); -} - -existingTestDir = fs.realpathSync(existingTestDir); - -const prevT = new GlobalTestState({ - testDir: existingTestDir, -}); - -async function withdrawAndPay( - t: GlobalTestState, - wallet: WalletCli, - bank: BankService, - exchange: ExchangeService, - merchant: MerchantService, -): Promise { - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:100" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:80", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.apiRequest("preparePay", { - talerPayUri: orderStatus.taler_pay_uri, - }); - t.assertTrue(r1.type === "response"); - - const r2 = await wallet.apiRequest("confirmPay", { - // FIXME: should be validated, don't cast! - proposalId: (r1.result as any).proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); -} - -/** - * Run test. - */ -runTestWithState(prevT, async (t: GlobalTestState) => { - // Set up test environment - - const bank = BankService.fromExistingConfig(t); - const exchange = ExchangeService.fromExistingConfig(t, "testexchange-1"); - const merchant = MerchantService.fromExistingConfig(t, "testmerchant-1"); - - await bank.start(); - await exchange.start(); - await merchant.start(); - await Promise.all([ - bank.pingUntilAvailable(), - merchant.pingUntilAvailable(), - exchange.pingUntilAvailable(), - ]); - - const wallet = new WalletCli(t); - - // Withdraw digital cash into the wallet. - - const repetitions = Number.parseInt(process.env["TALER_TEST_REPEAT"] ?? "1"); - - for (let rep = 0; rep < repetitions; rep++) { - console.log("repetition", rep); - try { - wallet.deleteDatabase(); - await withdrawAndPay(t, wallet, bank, exchange, merchant); - } catch (e) { - console.log("ignoring exception", e); - } - } - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-bank-api.ts b/packages/taler-integrationtests/src/test-bank-api.ts deleted file mode 100644 index 08991e279..000000000 --- a/packages/taler-integrationtests/src/test-bank-api.ts +++ /dev/null @@ -1,137 +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 - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - WalletCli, - ExchangeService, - setupDb, - BankService, - MerchantService, - BankApi, - BankAccessApi, - CreditDebitIndicator, -} from "./harness"; -import { createEddsaKeyPair, encodeCrock } from "taler-wallet-core"; -import { defaultCoinConfig } from "./denomStructures"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - 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.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - const bankUser = await BankApi.registerAccount(bank, "user1", "pw1"); - - // Make sure that registering twice results in a 409 Conflict - { - const e = await t.assertThrowsAsync(async () => { - await BankApi.registerAccount(bank, "user1", "pw1"); - }); - t.assertAxiosError(e); - t.assertTrue(e.response?.status === 409); - } - - let balResp = await BankAccessApi.getAccountBalance(bank, bankUser); - - console.log(balResp); - - // Check that we got the sign-up bonus. - t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:100"); - t.assertTrue( - balResp.balance.credit_debit_indicator === CreditDebitIndicator.Credit, - ); - - const res = createEddsaKeyPair(); - - await BankApi.adminAddIncoming(bank, { - amount: "TESTKUDOS:115", - debitAccountPayto: bankUser.accountPaytoUri, - exchangeBankAccount: exchangeBankAccount, - reservePub: encodeCrock(res.eddsaPub), - }); - - balResp = await BankAccessApi.getAccountBalance(bank, bankUser); - t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:15"); - t.assertTrue( - balResp.balance.credit_debit_indicator === CreditDebitIndicator.Debit, - ); -}); diff --git a/packages/taler-integrationtests/src/test-claim-loop.ts b/packages/taler-integrationtests/src/test-claim-loop.ts deleted file mode 100644 index 8c4df8740..000000000 --- a/packages/taler-integrationtests/src/test-claim-loop.ts +++ /dev/null @@ -1,83 +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 - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - MerchantPrivateApi, - WalletCli, -} from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; -import { URL } from "url"; - -/** - * Run test for the merchant's order lifecycle. - * - * FIXME: Is this test still necessary? We initially wrote if to confirm/document - * assumptions about how the merchant should work. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - // Query private order status before claiming it. - let orderStatusBefore = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - ); - t.assertTrue(orderStatusBefore.order_status === "unpaid"); - let statusUrlBefore = new URL(orderStatusBefore.order_status_url); - - // Make wallet claim the unpaid order. - t.assertTrue(orderStatusBefore.order_status === "unpaid"); - const talerPayUri = orderStatusBefore.taler_pay_uri; - const y = await wallet.preparePay({ - talerPayUri, - }); - - // Query private order status after claiming it. - let orderStatusAfter = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - ); - t.assertTrue(orderStatusAfter.order_status === "claimed"); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-exchange-management.ts b/packages/taler-integrationtests/src/test-exchange-management.ts deleted file mode 100644 index be990d9b6..000000000 --- a/packages/taler-integrationtests/src/test-exchange-management.ts +++ /dev/null @@ -1,250 +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 - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - WalletCli, - setupDb, - BankService, - ExchangeService, - MerchantService, - BankApi, - BankAccessApi, -} from "./harness"; -import { - PreparePayResultType, - ExchangesListRespose, - URL, - TalerErrorCode, -} from "taler-wallet-core"; -import { - FaultInjectedExchangeService, - FaultInjectionResponseContext, -} from "./faultInjection"; -import { defaultCoinConfig } from "./denomStructures"; - -/** - * Test if the wallet handles outdated exchange versions correct.y - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - 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); - - const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); - - bank.setSuggestedExchange( - faultyExchange, - exchangeBankAccount.accountPaytoUri, - ); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - /* - * ========================================================================= - * Check that the exchange can be added to the wallet - * (without any faults active). - * ========================================================================= - */ - - const wallet = new WalletCli(t); - - let exchangesList: ExchangesListRespose; - - exchangesList = await wallet.listExchanges(); - t.assertTrue(exchangesList.exchanges.length === 0); - - // Try before fault is injected - await wallet.addExchange({ - exchangeBaseUrl: faultyExchange.baseUrl, - }); - - exchangesList = await wallet.listExchanges(); - t.assertTrue(exchangesList.exchanges.length === 1); - - await wallet.addExchange({ - exchangeBaseUrl: faultyExchange.baseUrl, - }); - - console.log("listing exchanges"); - - exchangesList = await wallet.listExchanges(); - t.assertTrue(exchangesList.exchanges.length === 1); - - console.log("got list", exchangesList); - - /* - * ========================================================================= - * Check what happens if the exchange returns something totally - * bogus for /keys. - * ========================================================================= - */ - - wallet.deleteDatabase(); - - exchangesList = await wallet.listExchanges(); - t.assertTrue(exchangesList.exchanges.length === 0); - - faultyExchange.faultProxy.addFault({ - modifyResponse(ctx: FaultInjectionResponseContext) { - const url = new URL(ctx.request.requestUrl); - if (url.pathname === "/keys") { - const body = { - version: "whaaat", - }; - ctx.responseBody = Buffer.from(JSON.stringify(body), "utf-8"); - } - }, - }); - - const err1 = await t.assertThrowsOperationErrorAsync(async () => { - await wallet.addExchange({ - exchangeBaseUrl: faultyExchange.baseUrl, - }); - }); - - // Response is malformed, since it didn't even contain a version code - // in a format the wallet can understand. - t.assertTrue( - err1.operationError.code === - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - ); - - exchangesList = await wallet.listExchanges(); - t.assertTrue(exchangesList.exchanges.length === 0); - - /* - * ========================================================================= - * Check what happens if the exchange returns an old, unsupported - * version for /keys - * ========================================================================= - */ - - wallet.deleteDatabase(); - faultyExchange.faultProxy.clearAllFaults(); - - faultyExchange.faultProxy.addFault({ - modifyResponse(ctx: FaultInjectionResponseContext) { - const url = new URL(ctx.request.requestUrl); - if (url.pathname === "/keys") { - const keys = ctx.responseBody?.toString("utf-8"); - t.assertTrue(keys != null); - const keysJson = JSON.parse(keys); - keysJson["version"] = "2:0:0"; - ctx.responseBody = Buffer.from(JSON.stringify(keysJson), "utf-8"); - } - }, - }); - - const err2 = await t.assertThrowsOperationErrorAsync(async () => { - await wallet.addExchange({ - exchangeBaseUrl: faultyExchange.baseUrl, - }); - }); - - t.assertTrue( - err2.operationError.code === - TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, - ); - - exchangesList = await wallet.listExchanges(); - t.assertTrue(exchangesList.exchanges.length === 0); - - /* - * ========================================================================= - * Check that the exchange version is also checked when - * the exchange is implicitly added via the suggested - * exchange of a bank-integrated withdrawal. - * ========================================================================= - */ - - // Fault from above is still active! - - // Create withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, - "TESTKUDOS:10", - ); - - // Hand it to the wallet - - const wd = await wallet.getWithdrawalDetailsForUri({ - talerWithdrawUri: wop.taler_withdraw_uri, - }); - - // Make sure the faulty exchange isn't used for the suggestion. - t.assertTrue(wd.possibleExchanges.length === 0); -}); diff --git a/packages/taler-integrationtests/src/test-fee-regression.ts b/packages/taler-integrationtests/src/test-fee-regression.ts deleted file mode 100644 index 7b3193df2..000000000 --- a/packages/taler-integrationtests/src/test-fee-regression.ts +++ /dev/null @@ -1,206 +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 - */ - -/** - * Imports. - */ -import { defaultCoinConfig } from "./denomStructures"; -import { - runTest, - GlobalTestState, - BankService, - ExchangeService, - MerchantService, - setupDb, - WalletCli, -} from "./harness"; -import { - withdrawViaBank, - makeTestPayment, - SimpleTestEnvironment, -} from "./helpers"; - -/** - * Run a test case with a simple TESTKUDOS Taler environment, consisting - * of one exchange, one bank and one merchant. - */ -export async function createMyTestkudosEnvironment( - 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 exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - const coinCommon = { - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, - feeDeposit: "TESTKUDOS:0.0025", - feeWithdraw: "TESTKUDOS:0", - feeRefresh: "TESTKUDOS:0", - feeRefund: "TESTKUDOS:0", - }; - - exchange.addCoinConfigList([ - { - ...coinCommon, - name: "c1", - value: "TESTKUDOS:1.28", - }, - { - ...coinCommon, - name: "c2", - value: "TESTKUDOS:0.64", - }, - { - ...coinCommon, - name: "c3", - value: "TESTKUDOS:0.32", - }, - { - ...coinCommon, - name: "c4", - value: "TESTKUDOS:0.16", - }, - { - ...coinCommon, - name: "c5", - value: "TESTKUDOS:0.08", - }, - { - ...coinCommon, - name: "c5", - value: "TESTKUDOS:0.04", - }, - { - ...coinCommon, - name: "c6", - value: "TESTKUDOS:0.02", - }, - { - ...coinCommon, - name: "c7", - value: "TESTKUDOS:0.01", - }, - ]); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - }; -} - -/** - * Run test for basic, bank-integrated withdrawal and payment. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createMyTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { - wallet, - bank, - exchange, - amount: "TESTKUDOS:1.92", - }); - - const coins = await wallet.dumpCoins(); - - // Make sure we really withdraw one 0.64 and one 1.28 coin. - t.assertTrue(coins.coins.length === 2); - - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:1.30", - fulfillment_url: "taler://fulfillment-success/thx", - }; - - await makeTestPayment(t, { wallet, merchant, order }); - - await wallet.runUntilDone(); - - const txs = await wallet.getTransactions(); - t.assertAmountEquals(txs.transactions[1].amountEffective, "TESTKUDOS:1.30"); - console.log(txs); -}); diff --git a/packages/taler-integrationtests/src/test-merchant-longpolling.ts b/packages/taler-integrationtests/src/test-merchant-longpolling.ts deleted file mode 100644 index 5189d247e..000000000 --- a/packages/taler-integrationtests/src/test-merchant-longpolling.ts +++ /dev/null @@ -1,132 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; -import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, - ConfirmPayResultType, - URL, -} from "taler-wallet-core"; -import axios from "axios"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - /** - * ========================================================================= - * Create an order and let the wallet pay under a session ID - * - * We check along the way that the JSON response to /orders/{order_id} - * returns the right thing. - * ========================================================================= - */ - - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - }, - }); - - const firstOrderId = orderResp.order_id; - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-one", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - let publicOrderStatusUrl = new URL(orderStatus.order_status_url); - - // Wait for half a second seconds! - publicOrderStatusUrl.searchParams.set("timeout_ms", "500"); - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(pubUnpaidStatus); - - /** - * ========================================================================= - * Now actually pay, but WHILE a long poll is active! - * ========================================================================= - */ - - publicOrderStatusUrl.searchParams.set("timeout_ms", "5000"); - - let publicOrderStatusPromise = axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - - let preparePayResp = await wallet.preparePay({ - talerPayUri: pubUnpaidStatus.taler_pay_uri, - }); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - const proposalId = preparePayResp.proposalId; - - publicOrderStatusResp = await publicOrderStatusPromise; - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - const confirmPayRes = await wallet.confirmPay({ - proposalId: proposalId, - }); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); -}); diff --git a/packages/taler-integrationtests/src/test-merchant-refund-api.ts b/packages/taler-integrationtests/src/test-merchant-refund-api.ts deleted file mode 100644 index 121c571d2..000000000 --- a/packages/taler-integrationtests/src/test-merchant-refund-api.ts +++ /dev/null @@ -1,286 +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 - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - MerchantPrivateApi, - MerchantService, - BankServiceInterface, - MerchantServiceInterface, - WalletCli, - ExchangeServiceInterface, -} from "./harness"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - SimpleTestEnvironment, -} from "./helpers"; -import { durationFromSpec, PreparePayResultType, URL } from "taler-wallet-core"; -import axios from "axios"; - -async function testRefundApiWithFulfillmentUrl( - t: GlobalTestState, - env: { - merchant: MerchantServiceInterface; - bank: BankServiceInterface; - wallet: WalletCli; - exchange: ExchangeServiceInterface; - }, -): Promise { - const { wallet, bank, exchange, merchant } = env; - - // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/fulfillment", - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUri = orderStatus.taler_pay_uri; - const orderId = orderResp.order_id; - - // Make wallet pay for the order - - let preparePayResult = await wallet.preparePay({ - talerPayUri, - }); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - const r2 = await wallet.apiRequest("confirmPay", { - proposalId: preparePayResult.proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - preparePayResult = await wallet.preparePay({ - talerPayUri, - }); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.AlreadyConfirmed, - ); - - await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5"); - - // Now test what the merchant gives as a response for various requests to the - // public order status URL! - - let publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - publicOrderStatusUrl.searchParams.set( - "h_contract", - preparePayResult.contractTermsHash, - ); - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - console.log(publicOrderStatusResp.data); - t.assertTrue(publicOrderStatusResp.status === 200); - t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5"); - - publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - console.log(publicOrderStatusResp.data); - // We didn't give any authentication, so we should get a fulfillment URL back - t.assertTrue(publicOrderStatusResp.status === 202); - const fu = publicOrderStatusResp.data.fulfillment_url; - t.assertTrue(typeof fu === "string" && fu.startsWith("https://example.com")); -} - -async function testRefundApiWithFulfillmentMessage( - t: GlobalTestState, - env: { - merchant: MerchantServiceInterface; - bank: BankServiceInterface; - wallet: WalletCli; - exchange: ExchangeServiceInterface; - }, -): Promise { - const { wallet, bank, exchange, merchant } = env; - - // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_message: "Thank you for buying foobar", - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUri = orderStatus.taler_pay_uri; - const orderId = orderResp.order_id; - - // Make wallet pay for the order - - let preparePayResult = await wallet.preparePay({ - talerPayUri, - }); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - const r2 = await wallet.apiRequest("confirmPay", { - proposalId: preparePayResult.proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - preparePayResult = await wallet.preparePay({ - talerPayUri, - }); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.AlreadyConfirmed, - ); - - await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5"); - - // Now test what the merchant gives as a response for various requests to the - // public order status URL! - - let publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - publicOrderStatusUrl.searchParams.set( - "h_contract", - preparePayResult.contractTermsHash, - ); - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - console.log(publicOrderStatusResp.data); - t.assertTrue(publicOrderStatusResp.status === 200); - t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5"); - - publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - console.log(publicOrderStatusResp.data); - // We didn't give any authentication, so we should get a fulfillment URL back - t.assertTrue(publicOrderStatusResp.status === 403); -} - -/** - * Test case for the refund API of the merchant backend. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - await testRefundApiWithFulfillmentUrl(t, { - wallet, - bank, - exchange, - merchant, - }); - - await testRefundApiWithFulfillmentMessage(t, { - wallet, - bank, - exchange, - merchant, - }); -}); diff --git a/packages/taler-integrationtests/src/test-pay-abort.ts b/packages/taler-integrationtests/src/test-pay-abort.ts deleted file mode 100644 index 566500091..000000000 --- a/packages/taler-integrationtests/src/test-pay-abort.ts +++ /dev/null @@ -1,202 +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 test to check aborting partial payment - * via refunds. - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - MerchantService, - ExchangeService, - setupDb, - BankService, - WalletCli, - MerchantPrivateApi, - BankApi, - BankAccessApi, -} from "./harness"; -import { - FaultInjectedExchangeService, - FaultInjectionRequestContext, - FaultInjectionResponseContext, -} from "./faultInjection"; -import { PreparePayResultType, URL, TalerErrorCode } from "taler-wallet-core"; -import { defaultCoinConfig } from "./denomStructures"; -import { withdrawViaBank, makeTestPayment } from "./helpers"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - 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 exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - await exchange.addBankAccount("1", exchangeBankAccount); - exchange.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - merchant.addExchange(faultyExchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - // Create withdrawal operation - - await withdrawViaBank(t, { - wallet, - exchange: faultyExchange, - amount: "TESTKUDOS:20", - bank, - }); - - // faultyExchange.faultProxy.addFault({ - // modifyRequest(ctx: FaultInjectionRequestContext) { - // console.log("proxy request to", ctx.requestUrl); - // } - // }); - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:15", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const preparePayResult = await wallet.preparePay({ - talerPayUri: orderStatus.taler_pay_uri, - }); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - // We let only the first deposit through! - let firstDepositUrl: string | undefined; - - faultyExchange.faultProxy.addFault({ - modifyRequest(ctx: FaultInjectionRequestContext) { - const url = new URL(ctx.requestUrl); - if (url.pathname.endsWith("/deposit")) { - if (!firstDepositUrl) { - firstDepositUrl = url.href; - return; - } - if (url.href != firstDepositUrl) { - url.pathname = "/doesntexist"; - ctx.requestUrl = url.href; - } - } - }, - modifyResponse(ctx: FaultInjectionResponseContext) { - const url = new URL(ctx.request.requestUrl); - if (url.pathname.endsWith("/deposit") && url.href != firstDepositUrl) { - ctx.responseBody = Buffer.from("{}"); - ctx.statusCode = 500; - } - }, - }); - - await t.assertThrowsOperationErrorAsync(async () => { - await wallet.confirmPay({ - proposalId: preparePayResult.proposalId, - }); - }); - - let txr = await wallet.getTransactions(); - console.log(JSON.stringify(txr, undefined, 2)); - - t.assertDeepEqual(txr.transactions[1].type, "payment"); - t.assertDeepEqual(txr.transactions[1].pending, true); - t.assertDeepEqual( - txr.transactions[1].error?.code, - TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - ); - - await wallet.abortFailedPayWithRefund({ - proposalId: preparePayResult.proposalId, - }); - - await wallet.runUntilDone(); - - txr = await wallet.getTransactions(); - console.log(JSON.stringify(txr, undefined, 2)); - - const txTypes = txr.transactions.map((x) => x.type); - - t.assertDeepEqual(txTypes, ["withdrawal", "payment", "refund"]); -}); diff --git a/packages/taler-integrationtests/src/test-pay-paid.ts b/packages/taler-integrationtests/src/test-pay-paid.ts deleted file mode 100644 index 40f7d014f..000000000 --- a/packages/taler-integrationtests/src/test-pay-paid.ts +++ /dev/null @@ -1,206 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness"; -import { - withdrawViaBank, - createFaultInjectedMerchantTestkudosEnvironment, -} from "./helpers"; -import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, - ConfirmPayResultType, - URL, -} from "taler-wallet-core"; -import axios from "axios"; -import { FaultInjectionRequestContext } from "./faultInjection"; - -/** - * Run test for the wallets repurchase detection mechanism - * based on the fulfillment URL. - * - * FIXME: This test is now almost the same as test-paywall-flow, - * since we can't initiate payment via a "claimed" private order status - * response. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - faultyMerchant, - } = await createFaultInjectedMerchantTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - /** - * ========================================================================= - * Create an order and let the wallet pay under a session ID - * - * We check along the way that the JSON response to /orders/{order_id} - * returns the right thing. - * ========================================================================= - */ - - const merchant = faultyMerchant; - - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-one", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - let publicOrderStatusUrl = orderStatus.order_status_url; - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(pubUnpaidStatus); - - let preparePayResp = await wallet.preparePay({ - talerPayUri: pubUnpaidStatus.taler_pay_uri, - }); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - const proposalId = preparePayResp.proposalId; - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - const confirmPayRes = await wallet.confirmPay({ - proposalId: proposalId, - }); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - console.log(publicOrderStatusResp.data); - - if (publicOrderStatusResp.status != 202) { - console.log(publicOrderStatusResp.data); - throw Error( - `expected status 202 (after paying), but got ${publicOrderStatusResp.status}`, - ); - } - - /** - * ========================================================================= - * Now change up the session ID and do payment re-play! - * ========================================================================= - */ - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-two", - }); - - console.log( - "order status under mysession-two:", - JSON.stringify(orderStatus, undefined, 2), - ); - - // Should be claimed (not paid!) because of a new session ID - t.assertTrue(orderStatus.order_status === "claimed"); - - let numPayRequested = 0; - let numPaidRequested = 0; - - faultyMerchant.faultProxy.addFault({ - modifyRequest(ctx: FaultInjectionRequestContext) { - const url = new URL(ctx.requestUrl); - if (url.pathname.endsWith("/pay")) { - numPayRequested++; - } else if (url.pathname.endsWith("/paid")) { - numPaidRequested++; - } - }, - }); - - let orderRespTwo = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - }, - }); - - let orderStatusTwo = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderRespTwo.order_id, - sessionId: "mysession-two", - }, - ); - - t.assertTrue(orderStatusTwo.order_status === "unpaid"); - - // Pay with new taler://pay URI, which should - // have the new session ID! - // Wallet should now automatically re-play payment. - preparePayResp = await wallet.preparePay({ - talerPayUri: orderStatusTwo.taler_pay_uri, - }); - - t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); - t.assertTrue(preparePayResp.paid); - - // Make sure the wallet is actually doing the replay properly. - t.assertTrue(numPaidRequested == 1); - t.assertTrue(numPayRequested == 0); -}); diff --git a/packages/taler-integrationtests/src/test-payment-claim.ts b/packages/taler-integrationtests/src/test-payment-claim.ts deleted file mode 100644 index 6aed7e9e1..000000000 --- a/packages/taler-integrationtests/src/test-payment-claim.ts +++ /dev/null @@ -1,109 +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 - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - MerchantPrivateApi, - WalletCli, -} from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; -import { PreparePayResultType, TalerErrorCode } from "taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - const walletTwo = new WalletCli(t, "two"); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUri = orderStatus.taler_pay_uri; - - // Make wallet pay for the order - - const preparePayResult = await wallet.preparePay({ - talerPayUri, - }); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - t.assertThrowsOperationErrorAsync(async () => { - await walletTwo.preparePay({ - talerPayUri, - }); - }); - - const r2 = await wallet.apiRequest("confirmPay", { - // FIXME: should be validated, don't cast! - proposalId: preparePayResult.proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - walletTwo.deleteDatabase(); - - const err = await t.assertThrowsOperationErrorAsync(async () => { - await walletTwo.preparePay({ - talerPayUri, - }); - }); - - t.assertTrue( - err.operationError.code === TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED, - ); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-payment-fault.ts b/packages/taler-integrationtests/src/test-payment-fault.ts deleted file mode 100644 index ca31e8eeb..000000000 --- a/packages/taler-integrationtests/src/test-payment-fault.ts +++ /dev/null @@ -1,210 +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 - */ - -/** - * Sample fault injection test. - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - MerchantService, - ExchangeService, - setupDb, - BankService, - WalletCli, - MerchantPrivateApi, - BankApi, - BankAccessApi, -} from "./harness"; -import { - FaultInjectedExchangeService, - FaultInjectionRequestContext, - FaultInjectionResponseContext, -} from "./faultInjection"; -import { CoreApiResponse } from "taler-wallet-core"; -import { defaultCoinConfig } from "./denomStructures"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - 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 exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - await exchange.addBankAccount("1", exchangeBankAccount); - exchange.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); - - // Print all requests to the exchange - faultyExchange.faultProxy.addFault({ - modifyRequest(ctx: FaultInjectionRequestContext) { - console.log("got request", ctx); - }, - modifyResponse(ctx: FaultInjectionResponseContext) { - console.log("got response", ctx); - }, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - merchant.addExchange(faultyExchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - // Create withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, - "TESTKUDOS:20", - ); - - // Hand it to the wallet - - const r1 = await wallet.apiRequest("getWithdrawalDetailsForUri", { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - t.assertTrue(r1.type === "response"); - - await wallet.runPending(); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bank, user, wop); - - // Withdraw - - const r2 = await wallet.apiRequest("acceptBankIntegratedWithdrawal", { - exchangeBaseUrl: faultyExchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }); - t.assertTrue(r2.type === "response"); - await wallet.runUntilDone(); - - // Check balance - - const balApiResp = await wallet.apiRequest("getBalances", {}); - t.assertTrue(balApiResp.type === "response"); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - let apiResp: CoreApiResponse; - - apiResp = await wallet.apiRequest("preparePay", { - talerPayUri: orderStatus.taler_pay_uri, - }); - t.assertTrue(apiResp.type === "response"); - - const proposalId = (apiResp.result as any).proposalId; - - await wallet.runPending(); - - // Drop 3 responses from the exchange. - let faultCount = 0; - faultyExchange.faultProxy.addFault({ - modifyResponse(ctx: FaultInjectionResponseContext) { - if (faultCount < 3) { - faultCount++; - ctx.dropResponse = true; - } - }, - }); - - // confirmPay won't work, as the exchange is unreachable - - apiResp = await wallet.apiRequest("confirmPay", { - // FIXME: should be validated, don't cast! - proposalId: proposalId, - }); - t.assertTrue(apiResp.type === "error"); - - await wallet.runUntilDone(); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); -}); diff --git a/packages/taler-integrationtests/src/test-payment-idempotency.ts b/packages/taler-integrationtests/src/test-payment-idempotency.ts deleted file mode 100644 index 85be04d59..000000000 --- a/packages/taler-integrationtests/src/test-payment-idempotency.ts +++ /dev/null @@ -1,103 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; -import { PreparePayResultType } from "taler-wallet-core"; - -/** - * Test the wallet-core payment API, especially that repeated operations - * return the expected result. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUri = orderStatus.taler_pay_uri; - - // Make wallet pay for the order - - const preparePayResult = await wallet.preparePay({ - talerPayUri: orderStatus.taler_pay_uri, - }); - - const preparePayResultRep = await wallet.preparePay({ - talerPayUri: orderStatus.taler_pay_uri, - }); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - t.assertTrue( - preparePayResultRep.status === PreparePayResultType.PaymentPossible, - ); - - const proposalId = preparePayResult.proposalId; - - const r2 = await wallet.apiRequest("confirmPay", { - // FIXME: should be validated, don't cast! - proposalId: proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - const preparePayResultAfter = await wallet.preparePay({ - talerPayUri, - }); - - t.assertTrue( - preparePayResultAfter.status === PreparePayResultType.AlreadyConfirmed, - ); - t.assertTrue(preparePayResultAfter.paid === true); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-payment-multiple.ts b/packages/taler-integrationtests/src/test-payment-multiple.ts deleted file mode 100644 index c6a0868af..000000000 --- a/packages/taler-integrationtests/src/test-payment-multiple.ts +++ /dev/null @@ -1,161 +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 - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - setupDb, - BankService, - ExchangeService, - MerchantService, - WalletCli, - MerchantPrivateApi, -} from "./harness"; -import { withdrawViaBank } from "./helpers"; -import { coin_ct10, coin_u1 } from "./denomStructures"; - -async function setupTest( - t: GlobalTestState, -): Promise<{ - merchant: MerchantService; - exchange: ExchangeService; - bank: BankService; -}> { - 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 exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - - exchange.addOfferedCoins([coin_ct10, coin_u1]); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - await exchange.addBankAccount("1", exchangeBankAccount); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - return { - merchant, - bank, - exchange, - }; -} - -/** - * Run test. - * - * This test uses a very sub-optimal denomination structure. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { merchant, bank, exchange } = await setupTest(t); - - const wallet = new WalletCli(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:100" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:80", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.apiRequest("preparePay", { - talerPayUri: orderStatus.taler_pay_uri, - }); - t.assertTrue(r1.type === "response"); - - const r2 = await wallet.apiRequest("confirmPay", { - // FIXME: should be validated, don't cast! - proposalId: (r1.result as any).proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-payment-transient.ts b/packages/taler-integrationtests/src/test-payment-transient.ts deleted file mode 100644 index dc7ebbb1d..000000000 --- a/packages/taler-integrationtests/src/test-payment-transient.ts +++ /dev/null @@ -1,172 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness"; -import { - withdrawViaBank, - createFaultInjectedMerchantTestkudosEnvironment, -} from "./helpers"; -import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, - ConfirmPayResultType, - URL, - codecForExchangeKeysJson, - TalerErrorDetails, - TalerErrorCode, -} from "taler-wallet-core"; -import axios from "axios"; -import { - FaultInjectionRequestContext, - FaultInjectionResponseContext, -} from "./faultInjection"; - -/** - * Run test for a payment where the merchant has a transient - * failure in /pay - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - faultyMerchant, - } = await createFaultInjectedMerchantTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - const merchant = faultyMerchant; - - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-one", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - let publicOrderStatusUrl = orderStatus.order_status_url; - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(pubUnpaidStatus); - - let preparePayResp = await wallet.preparePay({ - talerPayUri: pubUnpaidStatus.taler_pay_uri, - }); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - const proposalId = preparePayResp.proposalId; - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - let faultInjected = false; - - faultyMerchant.faultProxy.addFault({ - modifyResponse(ctx: FaultInjectionResponseContext) { - console.log("in modifyResponse"); - const url = new URL(ctx.request.requestUrl); - console.log("pathname is", url.pathname); - if (!url.pathname.endsWith("/pay")) { - return; - } - if (faultInjected) { - console.log("not injecting pay fault"); - return; - } - faultInjected = true; - console.log("injecting pay fault"); - const err: TalerErrorDetails = { - code: TalerErrorCode.GENERIC_DB_COMMIT_FAILED, - details: {}, - hint: "huh", - message: "something went wrong", - }; - ctx.responseBody = Buffer.from(JSON.stringify(err)); - ctx.statusCode = 500; - }, - }); - - const confirmPayResp = await wallet.confirmPay({ - proposalId, - }); - - console.log(confirmPayResp); - - t.assertTrue(confirmPayResp.type === ConfirmPayResultType.Pending); - t.assertTrue(faultInjected); - - const confirmPayRespTwo = await wallet.confirmPay({ - proposalId, - }); - - t.assertTrue(confirmPayRespTwo.type === ConfirmPayResultType.Done); - - // Now ask the merchant if paid - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - console.log(publicOrderStatusResp.data); - - if (publicOrderStatusResp.status != 202) { - console.log(publicOrderStatusResp.data); - throw Error( - `expected status 202 (after paying), but got ${publicOrderStatusResp.status}`, - ); - } -}); diff --git a/packages/taler-integrationtests/src/test-payment.ts b/packages/taler-integrationtests/src/test-payment.ts deleted file mode 100644 index 8a1240dab..000000000 --- a/packages/taler-integrationtests/src/test-payment.ts +++ /dev/null @@ -1,53 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState } from "./harness"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - makeTestPayment, -} from "./helpers"; - -/** - * Run test for basic, bank-integrated withdrawal and payment. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }; - - await makeTestPayment(t, { wallet, merchant, order }); - - await wallet.runUntilDone(); -}); diff --git a/packages/taler-integrationtests/src/test-paywall-flow.ts b/packages/taler-integrationtests/src/test-paywall-flow.ts deleted file mode 100644 index 54c8ab463..000000000 --- a/packages/taler-integrationtests/src/test-paywall-flow.ts +++ /dev/null @@ -1,233 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; -import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, - ConfirmPayResultType, -} from "taler-wallet-core"; -import axios from "axios"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - /** - * ========================================================================= - * Create an order and let the wallet pay under a session ID - * - * We check along the way that the JSON response to /orders/{order_id} - * returns the right thing. - * ========================================================================= - */ - - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - }, - }); - - const firstOrderId = orderResp.order_id; - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-one", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUriOne = orderStatus.taler_pay_uri; - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - let publicOrderStatusUrl = orderStatus.order_status_url; - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(pubUnpaidStatus); - - let preparePayResp = await wallet.preparePay({ - talerPayUri: pubUnpaidStatus.taler_pay_uri, - }); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - const proposalId = preparePayResp.proposalId; - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - const confirmPayRes = await wallet.confirmPay({ - proposalId: proposalId, - }); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - console.log(publicOrderStatusResp.data); - - if (publicOrderStatusResp.status != 202) { - console.log(publicOrderStatusResp.data); - throw Error( - `expected status 202 (after paying), but got ${publicOrderStatusResp.status}`, - ); - } - - /** - * ========================================================================= - * Now change up the session ID! - * ========================================================================= - */ - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-two", - }); - - // Should be claimed (not paid!) because of a new session ID - t.assertTrue(orderStatus.order_status === "claimed"); - - // Pay with new taler://pay URI, which should - // have the new session ID! - // Wallet should now automatically re-play payment. - preparePayResp = await wallet.preparePay({ - talerPayUri: talerPayUriOne, - }); - - t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); - t.assertTrue(preparePayResp.paid); - - /** - * ========================================================================= - * Now we test re-purchase detection. - * ========================================================================= - */ - - orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - // Same fulfillment URL as previously! - fulfillment_url: "https://example.com/article42", - }, - }); - - const secondOrderId = orderResp.order_id; - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: secondOrderId, - sessionId: "mysession-three", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - publicOrderStatusUrl = orderStatus.order_status_url; - - // Here the re-purchase detection should kick in, - // and the wallet should re-pay for the old order - // under the new session ID (mysession-three). - preparePayResp = await wallet.preparePay({ - talerPayUri: orderStatus.taler_pay_uri, - }); - - t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); - t.assertTrue(preparePayResp.paid); - - // The first order should now be paid under "mysession-three", - // as the wallet did re-purchase detection - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: firstOrderId, - sessionId: "mysession-three", - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - // Check that with a completely new session ID, the status would NOT - // be paid. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: firstOrderId, - sessionId: "mysession-four", - }); - - t.assertTrue(orderStatus.order_status === "claimed"); - - // Now check if the public status of the new order is correct. - - console.log("requesting public status", publicOrderStatusUrl); - - // Ask the order status of the claimed-but-unpaid order - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error(`expected status 402, but got ${publicOrderStatusResp.status}`); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(publicOrderStatusResp.data); - - t.assertTrue(pubUnpaidStatus.already_paid_order_id === firstOrderId); -}); diff --git a/packages/taler-integrationtests/src/test-refund-auto.ts b/packages/taler-integrationtests/src/test-refund-auto.ts deleted file mode 100644 index 1a7055fd4..000000000 --- a/packages/taler-integrationtests/src/test-refund-auto.ts +++ /dev/null @@ -1,100 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; -import { CoreApiResponse, durationFromSpec } from "taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - auto_refund: { - d_ms: 3000, - }, - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.apiRequest("preparePay", { - talerPayUri: orderStatus.taler_pay_uri, - }); - t.assertTrue(r1.type === "response"); - - const r2 = await wallet.apiRequest("confirmPay", { - // FIXME: should be validated, don't cast! - proposalId: (r1.result as any).proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - const ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - console.log(ref); - - // The wallet should now automatically pick up the refund. - await wallet.runUntilDone(); - - const transactions = await wallet.getTransactions(); - console.log(JSON.stringify(transactions, undefined, 2)); - - const transactionTypes = transactions.transactions.map((x) => x.type); - t.assertDeepEqual(transactionTypes, ["withdrawal", "payment", "refund"]); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-refund-gone.ts b/packages/taler-integrationtests/src/test-refund-gone.ts deleted file mode 100644 index 764d5c28d..000000000 --- a/packages/taler-integrationtests/src/test-refund-gone.ts +++ /dev/null @@ -1,127 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - applyTimeTravel, -} from "./helpers"; -import { - durationFromSpec, - timestampAddDuration, - getTimestampNow, - timestampTruncateToSecond, -} from "taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - pay_deadline: timestampTruncateToSecond( - timestampAddDuration( - getTimestampNow(), - durationFromSpec({ - minutes: 10, - }), - ), - ), - }, - refund_delay: durationFromSpec({ minutes: 1 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.apiRequest("preparePay", { - talerPayUri: orderStatus.taler_pay_uri, - }); - t.assertTrue(r1.type === "response"); - - const r2 = await wallet.apiRequest("confirmPay", { - // FIXME: should be validated, don't cast! - proposalId: (r1.result as any).proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - console.log(orderStatus); - - await applyTimeTravel(durationFromSpec({ hours: 1 }), { exchange, wallet }); - - await exchange.runAggregatorOnce(); - - const ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - console.log(ref); - - let rr = await wallet.applyRefund({ - talerRefundUri: ref.talerRefundUri, - }); - - t.assertAmountEquals(rr.amountRefundGone, "TESTKUDOS:5"); - console.log(rr); - - await wallet.runUntilDone(); - - let r = await wallet.apiRequest("getBalances", {}); - console.log(JSON.stringify(r, undefined, 2)); - - r = await wallet.apiRequest("getTransactions", {}); - console.log(JSON.stringify(r, undefined, 2)); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-refund-incremental.ts b/packages/taler-integrationtests/src/test-refund-incremental.ts deleted file mode 100644 index 7ad406daf..000000000 --- a/packages/taler-integrationtests/src/test-refund-incremental.ts +++ /dev/null @@ -1,191 +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 - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - delayMs, - MerchantPrivateApi, -} from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; -import { TransactionType, Amounts, durationFromSpec } from "taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:10", - fulfillment_url: "taler://fulfillment-success/thx", - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.apiRequest("preparePay", { - talerPayUri: orderStatus.taler_pay_uri, - }); - t.assertTrue(r1.type === "response"); - - const r2 = await wallet.apiRequest("confirmPay", { - // FIXME: should be validated, don't cast! - proposalId: (r1.result as any).proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - let ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:2.5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - console.log("first refund increase response", ref); - - { - let wr = await wallet.applyRefund({ - talerRefundUri: ref.talerRefundUri, - }); - console.log(wr); - const txs = await wallet.getTransactions(); - console.log( - "transactions after applying first refund:", - JSON.stringify(txs, undefined, 2), - ); - } - - // Wait at least a second, because otherwise the increased - // refund will be grouped with the previous one. - await delayMs(1200); - - ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "bar", - orderId: orderResp.order_id, - }); - - console.log("second refund increase response", ref); - - // Wait at least a second, because otherwise the increased - // refund will be grouped with the previous one. - await delayMs(1200); - - ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:10", - instance: "default", - justification: "bar", - orderId: orderResp.order_id, - }); - - console.log("third refund increase response", ref); - - { - let wr = await wallet.applyRefund({ - talerRefundUri: ref.talerRefundUri, - }); - console.log(wr); - } - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:10"); - - console.log(JSON.stringify(orderStatus, undefined, 2)); - - await wallet.runUntilDone(); - - const bal = await wallet.getBalances(); - console.log(JSON.stringify(bal, undefined, 2)); - - { - const txs = await wallet.getTransactions(); - console.log(JSON.stringify(txs, undefined, 2)); - - const txTypes = txs.transactions.map((x) => x.type); - t.assertDeepEqual(txTypes, [ - "withdrawal", - "payment", - "refund", - "refund", - "refund", - ]); - - for (const tx of txs.transactions) { - if (tx.type !== TransactionType.Refund) { - continue; - } - t.assertAmountLeq(tx.amountEffective, tx.amountRaw); - } - - const raw = Amounts.sum( - txs.transactions - .filter((x) => x.type === TransactionType.Refund) - .map((x) => x.amountRaw), - ).amount; - - t.assertAmountEquals("TESTKUDOS:10", raw); - - const effective = Amounts.sum( - txs.transactions - .filter((x) => x.type === TransactionType.Refund) - .map((x) => x.amountEffective), - ).amount; - - t.assertAmountEquals("TESTKUDOS:8.33", effective); - } - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-refund.ts b/packages/taler-integrationtests/src/test-refund.ts deleted file mode 100644 index 908136518..000000000 --- a/packages/taler-integrationtests/src/test-refund.ts +++ /dev/null @@ -1,103 +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 - */ - -/** - * Imports. - */ -import { durationFromSpec } from "taler-wallet-core"; -import { runTest, GlobalTestState, MerchantPrivateApi } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.apiRequest("preparePay", { - talerPayUri: orderStatus.taler_pay_uri, - }); - t.assertTrue(r1.type === "response"); - - const r2 = await wallet.apiRequest("confirmPay", { - // FIXME: should be validated, don't cast! - proposalId: (r1.result as any).proposalId, - }); - t.assertTrue(r2.type === "response"); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - const ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - console.log(ref); - - let r = await wallet.apiRequest("applyRefund", { - talerRefundUri: ref.talerRefundUri, - }); - t.assertTrue(r.type === "response"); - console.log(r); - - await wallet.runUntilDone(); - - r = await wallet.apiRequest("getBalances", {}); - console.log(JSON.stringify(r, undefined, 2)); - - r = await wallet.apiRequest("getTransactions", {}); - console.log(JSON.stringify(r, undefined, 2)); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-revocation.ts b/packages/taler-integrationtests/src/test-revocation.ts deleted file mode 100644 index 32cb5d620..000000000 --- a/packages/taler-integrationtests/src/test-revocation.ts +++ /dev/null @@ -1,122 +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 - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - MerchantPrivateApi, - ExchangeService, - MerchantService, - WalletCli, -} from "./harness"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - makeTestPayment, -} from "./helpers"; -import { CoinDumpJson } from "taler-wallet-core"; - -async function revokeAllWalletCoins(req: { - wallet: WalletCli; - exchange: ExchangeService; - merchant: MerchantService; -}): Promise { - const { wallet, exchange, merchant } = req; - const coinDump = await wallet.dumpCoins(); - console.log(coinDump); - const usedDenomHashes = new Set(); - for (const coin of coinDump.coins) { - usedDenomHashes.add(coin.denom_pub_hash); - } - - await exchange.stop(); - - for (const x of usedDenomHashes.values()) { - await exchange.revokeDenomination(x); - } - - await exchange.keyup(); - - await exchange.start(); - await exchange.pingUntilAvailable(); - await merchant.stop(); - await merchant.start(); - await merchant.pingUntilAvailable(); -} - -/** - * Basic time travel test. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - - await revokeAllWalletCoins({ wallet, exchange, merchant }); - - // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565 - // is implemented. - await wallet.forceUpdateExchange({ exchangeBaseUrl: exchange.baseUrl }); - await wallet.runUntilDone(); - await wallet.runUntilDone(); - const bal = await wallet.getBalances(); - console.log("wallet balance", bal); - - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:10", - fulfillment_url: "taler://fulfillment-success/thx", - }; - - await makeTestPayment(t, { wallet, merchant, order }); - - wallet.deleteDatabase(); - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - - const coinDump = await wallet.dumpCoins(); - console.log(coinDump); - const coinPubList = coinDump.coins.map((x) => x.coin_pub); - await wallet.forceRefresh({ - coinPubList, - }); - await wallet.runUntilDone(); - - await revokeAllWalletCoins({ wallet, exchange, merchant }); - - // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565 - // is implemented. - await wallet.forceUpdateExchange({ exchangeBaseUrl: exchange.baseUrl }); - await wallet.runUntilDone(); - await wallet.runUntilDone(); - { - const bal = await wallet.getBalances(); - console.log("wallet balance", bal); - } - - await makeTestPayment(t, { wallet, merchant, order }); -}); diff --git a/packages/taler-integrationtests/src/test-timetravel-autorefresh.ts b/packages/taler-integrationtests/src/test-timetravel-autorefresh.ts deleted file mode 100644 index 382051c8a..000000000 --- a/packages/taler-integrationtests/src/test-timetravel-autorefresh.ts +++ /dev/null @@ -1,204 +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 - */ - -/** - * Imports. - */ -import { - ConfirmPayResultType, - Duration, - durationFromSpec, - PendingOperationsResponse, - PreparePayResultType, -} from "taler-wallet-core"; -import { makeNoFeeCoinConfig } from "./denomStructures"; -import { - BankService, - ExchangeService, - GlobalTestState, - MerchantPrivateApi, - MerchantService, - runTest, - setupDb, - WalletCli, -} from "./harness"; -import { startWithdrawViaBank, withdrawViaBank } from "./helpers"; - -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); - } -} - -/** - * Basic time travel test. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - 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(makeNoFeeCoinConfig("TESTKUDOS")); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - - // Travel into the future, the deposit expiration is two years - // into the future. - await applyTimeTravel(durationFromSpec({ days: 400 }), { - wallet, - exchange, - merchant, - }); - - await wallet.runUntilDone(); - - let p: PendingOperationsResponse; - p = await wallet.getPendingOperations(); - - console.log("pending operations after first time travel"); - console.log(JSON.stringify(p, undefined, 2)); - - await startWithdrawViaBank(t, { - wallet, - bank, - exchange, - amount: "TESTKUDOS:20", - }); - - await wallet.runUntilDone(); - - // Travel into the future, the deposit expiration is two years - // into the future. - await applyTimeTravel(durationFromSpec({ years: 2, months: 6 }), { - wallet, - exchange, - merchant, - }); - - await wallet.runUntilDone(); - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - fulfillment_url: "http://example.com", - summary: "foo", - amount: "TESTKUDOS:30", - }, - }); - - const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - instance: "default", - }, - ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const r = await wallet.preparePay({ - talerPayUri: orderStatus.taler_pay_uri, - }); - - console.log(r); - - t.assertTrue(r.status === PreparePayResultType.PaymentPossible); - - const cpr = await wallet.confirmPay({ - proposalId: r.proposalId, - }); - - t.assertTrue(cpr.type === ConfirmPayResultType.Done); -}); diff --git a/packages/taler-integrationtests/src/test-timetravel-withdraw.ts b/packages/taler-integrationtests/src/test-timetravel-withdraw.ts deleted file mode 100644 index e0124d462..000000000 --- a/packages/taler-integrationtests/src/test-timetravel-withdraw.ts +++ /dev/null @@ -1,90 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState } from "./harness"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - startWithdrawViaBank, -} from "./helpers"; -import { Duration, TransactionType } from "taler-wallet-core"; - -/** - * Basic time travel test. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - - // Travel 400 days into the future, - // as the deposit expiration is two years - // into the future. - const timetravelDuration: Duration = { - d_ms: 1000 * 60 * 60 * 24 * 400, - }; - - await exchange.stop(); - exchange.setTimetravel(timetravelDuration); - await exchange.keyup(); - await exchange.start(); - await exchange.pingUntilAvailable(); - - await merchant.stop(); - merchant.setTimetravel(timetravelDuration); - await merchant.start(); - await merchant.pingUntilAvailable(); - - // This should fail, as the wallet didn't time travel yet. - await startWithdrawViaBank(t, { - wallet, - bank, - exchange, - amount: "TESTKUDOS:20", - }); - - // Check that transactions are correct for the failed withdrawal - { - await wallet.runUntilDone({ maxRetries: 5 }); - const transactions = await wallet.getTransactions(); - console.log(transactions); - const types = transactions.transactions.map((x) => x.type); - t.assertDeepEqual(types, ["withdrawal", "withdrawal"]); - const wtrans = transactions.transactions[1]; - t.assertTrue(wtrans.type === TransactionType.Withdrawal); - t.assertTrue(wtrans.pending); - } - - // Now we also let the wallet time travel - - wallet.setTimetravel(timetravelDuration); - - // This doesn't work yet, see https://bugs.taler.net/n/6585 - - // await wallet.runUntilDone({ maxRetries: 5 }); -}); diff --git a/packages/taler-integrationtests/src/test-tipping.ts b/packages/taler-integrationtests/src/test-tipping.ts deleted file mode 100644 index 4735de81a..000000000 --- a/packages/taler-integrationtests/src/test-tipping.ts +++ /dev/null @@ -1,128 +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 - */ - -/** - * Imports. - */ -import { - runTest, - GlobalTestState, - MerchantPrivateApi, - BankApi, -} from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - exchangeBankAccount, - } = await createSimpleTestkudosEnvironment(t); - - const mbu = await BankApi.createRandomBankUser(bank); - - const tipReserveResp = await MerchantPrivateApi.createTippingReserve( - merchant, - "default", - { - exchange_url: exchange.baseUrl, - initial_balance: "TESTKUDOS:10", - wire_method: "x-taler-bank", - }, - ); - - console.log("tipReserveResp:", tipReserveResp); - - t.assertDeepEqual( - tipReserveResp.payto_uri, - exchangeBankAccount.accountPaytoUri, - ); - - await BankApi.adminAddIncoming(bank, { - amount: "TESTKUDOS:10", - debitAccountPayto: mbu.accountPaytoUri, - exchangeBankAccount, - reservePub: tipReserveResp.reserve_pub, - }); - - await exchange.runWirewatchOnce(); - await merchant.stop(); - await merchant.start(); - await merchant.pingUntilAvailable(); - - const r = await MerchantPrivateApi.queryTippingReserves(merchant, "default"); - console.log("tipping reserves:", JSON.stringify(r, undefined, 2)); - - t.assertTrue(r.reserves.length === 1); - t.assertDeepEqual( - r.reserves[0].exchange_initial_amount, - r.reserves[0].merchant_initial_amount, - ); - - const tip = await MerchantPrivateApi.giveTip(merchant, "default", { - amount: "TESTKUDOS:5", - justification: "why not?", - next_url: "https://example.com/after-tip", - }); - - console.log("created tip", tip); - - const doTip = async (): Promise => { - const ptr = await wallet.prepareTip({ - talerTipUri: tip.taler_tip_uri, - }); - - console.log(ptr); - - t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5"); - t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85"); - - await wallet.acceptTip({ - walletTipId: ptr.walletTipId, - }); - - await wallet.runUntilDone(); - - const bal = await wallet.getBalances(); - - console.log(bal); - - t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:4.85"); - - const txns = await wallet.getTransactions(); - - console.log("Transactions:", JSON.stringify(txns, undefined, 2)); - - t.assertDeepEqual(txns.transactions[0].type, "tip"); - t.assertDeepEqual(txns.transactions[0].pending, false); - t.assertAmountEquals( - txns.transactions[0].amountEffective, - "TESTKUDOS:4.85", - ); - t.assertAmountEquals(txns.transactions[0].amountRaw, "TESTKUDOS:5.0"); - }; - - // Check twice so make sure tip handling is idempotent - await doTip(); - await doTip(); -}); diff --git a/packages/taler-integrationtests/src/test-wallettesting.ts b/packages/taler-integrationtests/src/test-wallettesting.ts deleted file mode 100644 index a6014a88d..000000000 --- a/packages/taler-integrationtests/src/test-wallettesting.ts +++ /dev/null @@ -1,87 +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 - */ - -/** - * Integration test for the wallet testing functionality used by the exchange - * test cases. - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState } from "./harness"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - await wallet.runIntegrationTest({ - amountToSpend: "TESTKUDOS:5", - amountToWithdraw: "TESTKUDOS:10", - bankBaseUrl: bank.baseUrl, - exchangeBaseUrl: exchange.baseUrl, - merchantApiKey: "sandbox", - merchantBaseUrl: merchant.makeInstanceBaseUrl(), - }); - - let txns = await wallet.getTransactions(); - console.log(JSON.stringify(txns, undefined, 2)); - let txTypes = txns.transactions.map((x) => x.type); - - t.assertDeepEqual(txTypes, [ - "withdrawal", - "payment", - "withdrawal", - "payment", - "refund", - "payment", - ]); - - wallet.deleteDatabase(); - - await wallet.withdrawTestBalance({ - amount: "TESTKUDOS:10", - bankBaseUrl: bank.baseUrl, - exchangeBaseUrl: exchange.baseUrl, - }); - - await wallet.runUntilDone(); - - await wallet.testPay({ - amount: "TESTKUDOS:5", - merchantApiKey: "sandbox", - merchantBaseUrl: merchant.makeInstanceBaseUrl(), - summary: "foo", - }); - - await wallet.runUntilDone(); - - txns = await wallet.getTransactions(); - console.log(JSON.stringify(txns, undefined, 2)); - txTypes = txns.transactions.map((x) => x.type); - - t.assertDeepEqual(txTypes, ["withdrawal", "payment"]); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-withdrawal-abort-bank.ts b/packages/taler-integrationtests/src/test-withdrawal-abort-bank.ts deleted file mode 100644 index dd848b93d..000000000 --- a/packages/taler-integrationtests/src/test-withdrawal-abort-bank.ts +++ /dev/null @@ -1,67 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, BankApi, BankAccessApi } from "./harness"; -import { createSimpleTestkudosEnvironment } from "./helpers"; -import { codecForBalancesResponse, TalerErrorCode } from "taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t); - - // Create a withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, - "TESTKUDOS:10", - ); - - // Hand it to the wallet - - const r1 = await wallet.apiRequest("getWithdrawalDetailsForUri", { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - t.assertTrue(r1.type === "response"); - - await wallet.runPending(); - - // Confirm it - - await BankApi.abortWithdrawalOperation(bank, user, wop); - - // Withdraw - - const r2 = await wallet.apiRequest("acceptBankIntegratedWithdrawal", { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }); - t.assertTrue(r2.type === "error"); - t.assertTrue( - r2.error.code === - TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK, - ); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-withdrawal-bank-integrated.ts b/packages/taler-integrationtests/src/test-withdrawal-bank-integrated.ts deleted file mode 100644 index d54309b31..000000000 --- a/packages/taler-integrationtests/src/test-withdrawal-bank-integrated.ts +++ /dev/null @@ -1,71 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, BankApi, BankAccessApi } from "./harness"; -import { createSimpleTestkudosEnvironment } from "./helpers"; -import { codecForBalancesResponse } from "taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t); - - // Create a withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, - "TESTKUDOS:10", - ); - - // Hand it to the wallet - - const r1 = await wallet.apiRequest("getWithdrawalDetailsForUri", { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - t.assertTrue(r1.type === "response"); - - await wallet.runPending(); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bank, user, wop); - - // Withdraw - - const r2 = await wallet.apiRequest("acceptBankIntegratedWithdrawal", { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }); - t.assertTrue(r2.type === "response"); - await wallet.runUntilDone(); - - // Check balance - - const balApiResp = await wallet.apiRequest("getBalances", {}); - t.assertTrue(balApiResp.type === "response"); - const balResp = codecForBalancesResponse().decode(balApiResp.result); - t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/src/test-withdrawal-manual.ts b/packages/taler-integrationtests/src/test-withdrawal-manual.ts deleted file mode 100644 index aeac74d9f..000000000 --- a/packages/taler-integrationtests/src/test-withdrawal-manual.ts +++ /dev/null @@ -1,78 +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 - */ - -/** - * Imports. - */ -import { runTest, GlobalTestState, BankApi } from "./harness"; -import { createSimpleTestkudosEnvironment } from "./helpers"; -import { CoreApiResponse } from "taler-wallet-core"; -import { codecForBalancesResponse } from "taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -runTest(async (t: GlobalTestState) => { - // Set up test environment - - const { - wallet, - bank, - exchange, - exchangeBankAccount, - } = await createSimpleTestkudosEnvironment(t); - - // Create a withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - - let wresp: CoreApiResponse; - - wresp = await wallet.apiRequest("addExchange", { - exchangeBaseUrl: exchange.baseUrl, - }); - - t.assertTrue(wresp.type === "response"); - - wresp = await wallet.apiRequest("acceptManualWithdrawal", { - exchangeBaseUrl: exchange.baseUrl, - amount: "TESTKUDOS:10", - }); - - t.assertTrue(wresp.type === "response"); - - const reservePub: string = (wresp.result as any).reservePub; - - await BankApi.adminAddIncoming(bank, { - exchangeBankAccount, - amount: "TESTKUDOS:10", - debitAccountPayto: user.accountPaytoUri, - reservePub: reservePub, - }); - - await exchange.runWirewatchOnce(); - - await wallet.runUntilDone(); - - // Check balance - - const balApiResp = await wallet.apiRequest("getBalances", {}); - t.assertTrue(balApiResp.type === "response"); - const balResp = codecForBalancesResponse().decode(balApiResp.result); - t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); - - await t.shutdown(); -}); diff --git a/packages/taler-integrationtests/testrunner b/packages/taler-integrationtests/testrunner deleted file mode 100755 index c03f6ed97..000000000 --- a/packages/taler-integrationtests/testrunner +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash - -# Simple test runner for the wallet integration tests. -# -# Usage: $0 TESTGLOB -# -# The TESTGLOB can be used to select which test cases to execute - -set -eu - -exit_int() { - echo "Interrupted..." - exit 2 -} - -trap "exit_int" INT - -if [ "$#" -ne 1 ]; then - echo "Usage: $0 TESTGLOB" - exit 1 -fi - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" - -cd $DIR - -./node_modules/.bin/tsc -b - -export ESM_OPTIONS='{"sourceMap": true}' - -shopt -s extglob - -num_exec=0 -num_fail=0 -num_succ=0 - -files_failed='' - -# Glob tests -for file in lib/$1?(.js); do - case "$file" in - */test-*.js) - echo "executing test $file" - ret=0 - node -r source-map-support/register -r esm $file || ret=$? - num_exec=$((num_exec+1)) - case $ret in - 0) - num_succ=$((num_succ+1)) - ;; - *) - num_fail=$((num_fail+1)) - files_failed=$files_failed:$file - ;; - esac - ;; - *) - continue - ;; - esac -done - -echo "-----------------------------------" -echo "Tests finished" -echo "$num_succ/$num_exec tests succeeded" -if [[ $num_fail != 0 ]]; then - echo "These tests failed:" - echo $files_failed | tr : \\n | sed '/^$/d' -fi -echo "-----------------------------------" - -if [[ $num_fail = 0 ]]; then - exit 0 -else - exit 1 -fi - diff --git a/packages/taler-integrationtests/tsconfig.json b/packages/taler-integrationtests/tsconfig.json deleted file mode 100644 index 2fe0853d4..000000000 --- a/packages/taler-integrationtests/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compileOnSave": true, - "compilerOptions": { - "composite": true, - "declaration": true, - "declarationMap": false, - "target": "ES6", - "module": "ESNext", - "moduleResolution": "node", - "sourceMap": true, - "lib": ["es6"], - "types": ["node"], - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "strict": true, - "strictPropertyInitialization": false, - "outDir": "lib", - "noImplicitAny": true, - "noImplicitThis": true, - "incremental": true, - "esModuleInterop": true, - "importHelpers": true, - "rootDir": "./src", - "typeRoots": ["./node_modules/@types"] - }, - "references": [ - { - "path": "../taler-wallet-core" - } - ], - "include": ["src/**/*"] -} diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json index 564cf7f4a..c3fa6e794 100644 --- a/packages/taler-wallet-cli/package.json +++ b/packages/taler-wallet-cli/package.json @@ -43,7 +43,9 @@ "typescript": "^4.0.5" }, "dependencies": { + "@types/minimatch": "^3.0.3", "axios": "^0.21.0", + "minimatch": "^3.0.4", "source-map-support": "^0.5.19", "taler-wallet-core": "workspace:*", "tslib": "^2.0.3" diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 87a51f30d..e4f1ccb50 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -41,6 +41,7 @@ import { } from "taler-wallet-core"; import * as clk from "./clk"; import { deepStrictEqual } from "assert"; +import { getTestInfo, runTests } from "./integrationtests/testrunner"; // This module also serves as the entry point for the crypto // thread worker, and thus must expose these two handlers. @@ -749,6 +750,25 @@ const testCli = walletCli.subcommand("testingArgs", "testing", { help: "Subcommands for testing GNU Taler deployments.", }); +testCli + .subcommand("listIntegrationtests", "list-integrationtests") + .action(async (args) => { + for (const t of getTestInfo()) { + console.log(t.name); + } + }); + +testCli + .subcommand("runIntegrationtests", "run-integrationtests") + .maybeArgument("pattern", clk.STRING, { + help: "Glob pattern to select which tests to run", + }) + .action(async (args) => { + await runTests({ + include_pattern: args.runIntegrationtests.pattern, + }); + }); + testCli.subcommand("vectors", "vectors").action(async (args) => { printTestVectors(); }); diff --git a/packages/taler-wallet-cli/src/integrationtests/denomStructures.ts b/packages/taler-wallet-cli/src/integrationtests/denomStructures.ts new file mode 100644 index 000000000..5ab9aca00 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/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/integrationtests/faultInjection.ts b/packages/taler-wallet-cli/src/integrationtests/faultInjection.ts new file mode 100644 index 000000000..a2d4836d9 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/faultInjection.ts @@ -0,0 +1,263 @@ +/* + 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, + BankService, + ExchangeServiceInterface, + MerchantServiceInterface, + MerchantService, + PrivateOrderStatusQuery, +} from "./harness"; +import { + PostOrderRequest, + PostOrderResponse, + MerchantOrderPrivateStatusResponse, +} from "./merchantApiTypes"; + +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) => void; + modifyResponse?: (ctx: FaultInjectionResponseContext) => void; +} + +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", () => { + 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) { + 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", () => { + 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) { + modResponse(faultRespContext); + } + } + if (faultRespContext.dropResponse) { + req.destroy(); + return; + } + if (faultRespContext.responseBody) { + // We must accomodate 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 new file mode 100644 index 000000000..108b78540 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/harness.ts @@ -0,0 +1,1718 @@ +/* + 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 os from "os"; +import * as http from "http"; +import { deepStrictEqual } from "assert"; +import { ChildProcess, spawn } from "child_process"; +import { + Configuration, + AmountJson, + Amounts, + Codec, + buildCodecForObject, + codecForString, + Duration, + CoreApiResponse, + PreparePayResult, + PreparePayRequest, + codecForPreparePayResult, + OperationFailedError, + AddExchangeRequest, + ExchangesListRespose, + codecForExchangesListResponse, + GetWithdrawalDetailsForUriRequest, + WithdrawUriInfoResponse, + codecForWithdrawUriInfoResponse, + ConfirmPayRequest, + ConfirmPayResult, + codecForConfirmPayResult, + IntegrationTestArgs, + TestPayArgs, + BalancesResponse, + codecForBalancesResponse, + encodeCrock, + getRandomBytes, + EddsaKeyPair, + eddsaGetPublic, + createEddsaKeyPair, + TransactionsResponse, + codecForTransactionsResponse, + WithdrawTestBalanceRequest, + AmountString, + ApplyRefundRequest, + codecForApplyRefundResponse, + codecForAny, + CoinDumpJson, + ForceExchangeUpdateRequest, + ForceRefreshRequest, + PrepareTipResult, + PrepareTipRequest, + codecForPrepareTipResult, + AcceptTipRequest, + AbortPayWithRefundRequest, + handleWorkerError, +} from "taler-wallet-core"; +import { URL } from "url"; +import axios, { AxiosError } from "axios"; +import { + codecForMerchantOrderPrivateStatusResponse, + codecForPostOrderResponse, + PostOrderRequest, + PostOrderResponse, + MerchantOrderPrivateStatusResponse, + TippingReserveStatus, + TipCreateConfirmation, + TipCreateRequest, +} from "./merchantApiTypes"; +import { ApplyRefundResponse } from "taler-wallet-core"; +import { PendingOperationsResponse } from "taler-wallet-core"; +import { CoinConfig } from "./denomStructures"; +import { after } from "taler-wallet-core/src/util/timer"; + +const exec = util.promisify(require("child_process").exec); + +export async function delayMs(ms: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), ms); + }); +} + +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, +): Promise { + console.log("runing command", command); + return new Promise((resolve, reject) => { + const stdoutChunks: Buffer[] = []; + const proc = spawn(command, { + stdio: ["inherit", "pipe", "pipe"], + shell: true, + }); + 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[], +): Promise { + console.log("runing command", shellescape([command, ...args])); + return new Promise((resolve, reject) => { + const stdoutChunks: Buffer[] = []; + const proc = spawn(command, args, { + stdio: ["inherit", "pipe", "pipe"], + shell: false, + }); + 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 { + return e.isAxiosError; + } + + 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, + ): ProcessWrapper { + console.log( + `spawning process (${logName}): ${shellescape([command, ...args])}`, + ); + const proc = spawn(command, args, { + stdio: ["inherit", "pipe", "pipe"], + }); + 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; + } + 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 interface TalerConfigSection { + options: Record; +} + +export interface TalerConfig { + sections: Record; +} + +export interface DbInfo { + 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; +} + +function setPaths(config: Configuration, home: string) { + config.setString("paths", "taler_home", home); + config.setString("paths", "taler_runtime_dir", "$TALER_HOME/taler-runtime/"); + 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/"); + config.setString( + "paths", + "taler_runtime_dir", + "${TMPDIR:-${TMP:-/tmp}}/taler-system-runtime/", + ); +} + +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}`); +} + +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) { + 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 ExchangeBankAccount { + 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: ExchangeBankAccount; + 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(); + setPaths(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 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"], + ); + } + + 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`, + ); + setPaths(config, gc.testDir + "/talerhome"); + + config.setString( + "exchange", + "keydir", + "${TALER_DATA_HOME}/exchange/live-keys/", + ); + config.setString( + "exchage", + "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("exchange", "signkey_duration", "4 weeks"); + config.setString("exchange", "legal_duraction", "2 years"); + config.setString("exchange", "lookahead_sign", "32 weeks 1 day"); + config.setString("exchange", "lookahead_provide", "4 weeks 1 day"); + + config.setString("exchangedb-postgres", "config", e.database); + + 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: ExchangeBankAccount, + ): 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-account-${localName}`, + "wire_gateway_url", + exchangeBankAccount.wireGatewayApiBaseUrl, + ); + config.setString( + `exchange-account-${localName}`, + "wire_gateway_auth_method", + "basic", + ); + config.setString( + `exchange-account-${localName}`, + "username", + exchangeBankAccount.accountName, + ); + config.setString( + `exchange-account-${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, + ...this.timetravelArgArr, + "download", + "sign", + "upload", + ], + ); + + const accounts: string[] = []; + + const config = Configuration.load(this.configFilename); + for (const sectionName of config.getSectionNames()) { + if (sectionName.startsWith("exchange-account")) { + accounts.push(config.getString(sectionName, "payto_uri").required()); + } + } + + console.log("configuring bank accounts", accounts); + + for (const acc of accounts) { + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + [ + "-c", + this.configFilename, + ...this.timetravelArgArr, + "enable-account", + acc, + "upload", + ], + ); + } + + const year = new Date().getFullYear(); + for (let i = year; i < year + 5; i++) { + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + [ + "-c", + this.configFilename, + ...this.timetravelArgArr, + "wire-fee", + `${i}`, + "x-taler-bank", + `${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, + ...this.timetravelArgArr, + "revoke-denomination", + denomPubHash, + "upload", + ], + ); + } + + 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-helper-crypto-eddsa", + ["-c", this.configFilename, ...this.timetravelArgArr], + `exchange-crypto-eddsa-${this.name}`, + ); + + this.helperCryptoRsaProc = this.globalState.spawnService( + "taler-helper-crypto-rsa", + ["-c", this.configFilename, ...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, + "--num-threads", + "1", + ...this.timetravelArgArr, + ], + `exchange-httpd-${this.name}`, + ); + + await this.keyup(); + } + + async pingUntilAvailable(): Promise { + const url = `http://localhost:${this.exchangeConfig.httpPort}/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 namespace MerchantPrivateApi { + export async function createOrder( + merchantService: MerchantServiceInterface, + instanceName: string, + req: PostOrderRequest, + ): Promise { + const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); + let url = new URL("private/orders", baseUrl); + const resp = await axios.post(url.href, req); + return codecForPostOrderResponse().decode(resp.data); + } + + export async function queryPrivateOrderStatus( + merchantService: MerchantServiceInterface, + query: PrivateOrderStatusQuery, + ): 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); + 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`; + setPaths(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 addInstance(instanceConfig: MerchantInstanceConfig): Promise { + if (!this.proc) { + throw Error("merchant must be running to add instance"); + } + console.log("adding instance"); + const url = `http://localhost:${this.merchantConfig.httpPort}/private/instances`; + await axios.post(url, { + 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 MerchantInstanceConfig { + id: string; + name: string; + paytoUris: string[]; + address?: unknown; + jurisdiction?: unknown; + defaultMaxWireFee?: string; + defaultMaxDepositFee?: string; + defaultWireFeeAmortization?: number; + defaultWireTransferDelay?: Duration; + defaultPayDelay?: 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; +} + +export async function runTestWithState( + gc: GlobalTestState, + testMain: (t: GlobalTestState) => Promise, + testName: string, +): Promise { + const startMs = new Date().getTime(); + + const handleSignal = () => { + gc.shutdownSync(); + console.warn("**** received fatal signal, shutting down test harness"); + process.exit(1); + }; + + process.on("SIGINT", handleSignal); + process.on("SIGTERM", handleSignal); + process.on("unhandledRejection", handleSignal); + process.on("uncaughtException", handleSignal); + + let status: TestStatus; + try { + console.log("running test in directory", gc.testDir); + await testMain(gc); + 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; + + 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", + ) {} + + 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 []; + } + + async apiRequest( + request: string, + payload: unknown, + ): Promise { + const resp = await sh( + this.globalTestState, + `wallet-${this.name}`, + `taler-wallet-cli ${ + this.timetravelArg ?? "" + } --no-throttle --wallet-db '${this.dbfile}' api '${request}' ${shellWrap( + JSON.stringify(payload), + )}`, + ); + console.log(resp); + return JSON.parse(resp) as CoreApiResponse; + } + + 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", + ], + ); + } + + async applyRefund(req: ApplyRefundRequest): Promise { + const resp = await this.apiRequest("applyRefund", req); + if (resp.type === "response") { + return codecForApplyRefundResponse().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + + async preparePay(req: PreparePayRequest): Promise { + const resp = await this.apiRequest("preparePay", req); + if (resp.type === "response") { + return codecForPreparePayResult().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + + async abortFailedPayWithRefund( + req: AbortPayWithRefundRequest, + ): Promise { + const resp = await this.apiRequest("abortFailedPayWithRefund", req); + if (resp.type === "response") { + return; + } + throw new OperationFailedError(resp.error); + } + + async confirmPay(req: ConfirmPayRequest): Promise { + const resp = await this.apiRequest("confirmPay", req); + if (resp.type === "response") { + return codecForConfirmPayResult().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + + async prepareTip(req: PrepareTipRequest): Promise { + const resp = await this.apiRequest("prepareTip", req); + if (resp.type === "response") { + return codecForPrepareTipResult().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + + async acceptTip(req: AcceptTipRequest): Promise { + const resp = await this.apiRequest("acceptTip", req); + if (resp.type === "response") { + return; + } + throw new OperationFailedError(resp.error); + } + + async dumpCoins(): Promise { + const resp = await this.apiRequest("dumpCoins", {}); + if (resp.type === "response") { + return codecForAny().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + + async addExchange(req: AddExchangeRequest): Promise { + const resp = await this.apiRequest("addExchange", req); + if (resp.type === "response") { + return; + } + throw new OperationFailedError(resp.error); + } + + async forceUpdateExchange(req: ForceExchangeUpdateRequest): Promise { + const resp = await this.apiRequest("forceUpdateExchange", req); + if (resp.type === "response") { + return; + } + throw new OperationFailedError(resp.error); + } + + async forceRefresh(req: ForceRefreshRequest): Promise { + const resp = await this.apiRequest("forceRefresh", req); + if (resp.type === "response") { + return; + } + throw new OperationFailedError(resp.error); + } + + async listExchanges(): Promise { + const resp = await this.apiRequest("listExchanges", {}); + if (resp.type === "response") { + return codecForExchangesListResponse().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + + async getBalances(): Promise { + const resp = await this.apiRequest("getBalances", {}); + if (resp.type === "response") { + return codecForBalancesResponse().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + + async getPendingOperations(): Promise { + const resp = await this.apiRequest("getPendingOperations", {}); + if (resp.type === "response") { + // FIXME: validate properly! + return codecForAny().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + + async getTransactions(): Promise { + const resp = await this.apiRequest("getTransactions", {}); + if (resp.type === "response") { + return codecForTransactionsResponse().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + + async runIntegrationTest(args: IntegrationTestArgs): Promise { + const resp = await this.apiRequest("runIntegrationTest", args); + if (resp.type === "response") { + return; + } + throw new OperationFailedError(resp.error); + } + + async testPay(args: TestPayArgs): Promise { + const resp = await this.apiRequest("testPay", args); + if (resp.type === "response") { + return; + } + throw new OperationFailedError(resp.error); + } + + async withdrawTestBalance(args: WithdrawTestBalanceRequest): Promise { + const resp = await this.apiRequest("withdrawTestBalance", args); + if (resp.type === "response") { + return; + } + throw new OperationFailedError(resp.error); + } + + async getWithdrawalDetailsForUri( + req: GetWithdrawalDetailsForUriRequest, + ): Promise { + const resp = await this.apiRequest("getWithdrawalDetailsForUri", req); + if (resp.type === "response") { + return codecForWithdrawUriInfoResponse().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } +} diff --git a/packages/taler-wallet-cli/src/integrationtests/helpers.ts b/packages/taler-wallet-cli/src/integrationtests/helpers.ts new file mode 100644 index 000000000..f4e676b61 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/helpers.ts @@ -0,0 +1,370 @@ +/* + 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 { + GlobalTestState, + DbInfo, + ExchangeService, + WalletCli, + MerchantService, + setupDb, + BankService, + ExchangeBankAccount, + MerchantServiceInterface, + BankApi, + BankAccessApi, + MerchantPrivateApi, + ExchangeServiceInterface, +} from "./harness"; +import { + AmountString, + Duration, + PreparePayResultType, + ConfirmPayResultType, + ContractTerms, +} from "taler-wallet-core"; +import { FaultInjectedMerchantService } from "./faultInjection"; +import { defaultCoinConfig } from "./denomStructures"; + +export interface SimpleTestEnvironment { + commonDb: DbInfo; + bank: BankService; + exchange: ExchangeService; + exchangeBankAccount: ExchangeBankAccount; + merchant: MerchantService; + wallet: WalletCli; +} + +/** + * 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, +): 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.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: ["payto://x-taler-bank/minst1"], + }); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + 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; + exchangeBankAccount: ExchangeBankAccount; + 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 exchangeBankAccount = await bank.createExchangeAccount( + "MyExchange", + "x", + ); + exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + exchange.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: ["payto://x-taler-bank/minst1"], + }); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + return { + commonDb: db, + exchange, + merchant, + wallet, + bank, + exchangeBankAccount, + faultyMerchant, + }; +} + +/** + * 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 + + const r1 = await wallet.apiRequest("getWithdrawalDetailsForUri", { + talerWithdrawUri: wop.taler_withdraw_uri, + }); + t.assertTrue(r1.type === "response"); + + await wallet.runPending(); + + // Confirm it + + await BankApi.confirmWithdrawalOperation(bank, user, wop); + + // Withdraw + + const r2 = await wallet.apiRequest("acceptBankIntegratedWithdrawal", { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + }); + t.assertTrue(r2.type === "response"); +} + +/** + * 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 + + const balApiResp = await wallet.apiRequest("getBalances", {}); + t.assertTrue(balApiResp.type === "response"); +} + +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; + }, +): Promise { + // Set up order. + + const { wallet, merchant } = args; + const instance = args.instance ?? "default"; + + const orderResp = await MerchantPrivateApi.createOrder(merchant, instance, { + order: args.order, + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const preparePayResult = await wallet.preparePay({ + talerPayUri: orderStatus.taler_pay_uri, + }); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + const r2 = await wallet.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, + }); + + t.assertTrue(orderStatus.order_status === "paid"); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts b/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts new file mode 100644 index 000000000..6782391a2 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts @@ -0,0 +1,304 @@ +/* + 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, +} from "taler-wallet-core"; +import { codecForAmountString } from "taler-wallet-core/lib/util/amounts"; + +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< + CheckPaymentPaidResponse +> => + 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< + CheckPaymentUnpaidResponse +> => + 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< + CheckPaymentClaimedResponse +> => + buildCodecForObject() + .property("order_status", codecForConstString("claimed")) + .property("contract_terms", codecForContractTerms()) + .build("CheckPaymentClaimedResponse"); + +export const codecForMerchantOrderPrivateStatusResponse = (): Codec< + MerchantOrderPrivateStatusResponse +> => + 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 transfered + // 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; +} diff --git a/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts b/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts new file mode 100644 index 000000000..e3c2af8e6 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts @@ -0,0 +1,60 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runPromptPaymentScenario(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Set up order. + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }, + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + console.log(orderStatus); + + // Wait "forever" + await new Promise(() => {}); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts new file mode 100644 index 000000000..b5cf6d5ba --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts @@ -0,0 +1,136 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + GlobalTestState, + WalletCli, + ExchangeService, + setupDb, + BankService, + MerchantService, + BankApi, + BankAccessApi, + CreditDebitIndicator, +} from "./harness"; +import { createEddsaKeyPair, encodeCrock } from "taler-wallet-core"; +import { defaultCoinConfig } from "./denomStructures"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runBankApiTest(t: GlobalTestState) { + // Set up test environment + + 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.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: ["payto://x-taler-bank/minst1"], + }); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + const bankUser = await BankApi.registerAccount(bank, "user1", "pw1"); + + // Make sure that registering twice results in a 409 Conflict + { + const e = await t.assertThrowsAsync(async () => { + await BankApi.registerAccount(bank, "user1", "pw1"); + }); + t.assertAxiosError(e); + t.assertTrue(e.response?.status === 409); + } + + let balResp = await BankAccessApi.getAccountBalance(bank, bankUser); + + console.log(balResp); + + // Check that we got the sign-up bonus. + t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:100"); + t.assertTrue( + balResp.balance.credit_debit_indicator === CreditDebitIndicator.Credit, + ); + + const res = createEddsaKeyPair(); + + await BankApi.adminAddIncoming(bank, { + amount: "TESTKUDOS:115", + debitAccountPayto: bankUser.accountPaytoUri, + exchangeBankAccount: exchangeBankAccount, + reservePub: encodeCrock(res.eddsaPub), + }); + + balResp = await BankAccessApi.getAccountBalance(bank, bankUser); + t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:15"); + t.assertTrue( + balResp.balance.credit_debit_indicator === CreditDebitIndicator.Debit, + ); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts b/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts new file mode 100644 index 000000000..5a0540e90 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts @@ -0,0 +1,81 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + GlobalTestState, + MerchantPrivateApi, +} from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { URL } from "url"; + +/** + * Run test for the merchant's order lifecycle. + * + * FIXME: Is this test still necessary? We initially wrote if to confirm/document + * assumptions about how the merchant should work. + */ +export async function runClaimLoopTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Set up order. + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }, + }); + + // Query private order status before claiming it. + let orderStatusBefore = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + }, + ); + t.assertTrue(orderStatusBefore.order_status === "unpaid"); + let statusUrlBefore = new URL(orderStatusBefore.order_status_url); + + // Make wallet claim the unpaid order. + t.assertTrue(orderStatusBefore.order_status === "unpaid"); + const talerPayUri = orderStatusBefore.taler_pay_uri; + const y = await wallet.preparePay({ + talerPayUri, + }); + + // Query private order status after claiming it. + let orderStatusAfter = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + }, + ); + t.assertTrue(orderStatusAfter.order_status === "claimed"); + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts new file mode 100644 index 000000000..0fbef5687 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts @@ -0,0 +1,249 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + GlobalTestState, + WalletCli, + setupDb, + BankService, + ExchangeService, + MerchantService, + BankApi, + BankAccessApi, +} from "./harness"; +import { + PreparePayResultType, + ExchangesListRespose, + URL, + TalerErrorCode, +} from "taler-wallet-core"; +import { + FaultInjectedExchangeService, + FaultInjectionResponseContext, +} from "./faultInjection"; +import { defaultCoinConfig } from "./denomStructures"; + +/** + * Test if the wallet handles outdated exchange versions correct.y + */ +export async function runExchangeManagementTest(t: GlobalTestState) { + // Set up test environment + + 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); + + const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); + + bank.setSuggestedExchange( + faultyExchange, + exchangeBankAccount.accountPaytoUri, + ); + + await bank.start(); + + await bank.pingUntilAvailable(); + + exchange.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: ["payto://x-taler-bank/minst1"], + }); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + console.log("setup done!"); + + /* + * ========================================================================= + * Check that the exchange can be added to the wallet + * (without any faults active). + * ========================================================================= + */ + + const wallet = new WalletCli(t); + + let exchangesList: ExchangesListRespose; + + exchangesList = await wallet.listExchanges(); + t.assertTrue(exchangesList.exchanges.length === 0); + + // Try before fault is injected + await wallet.addExchange({ + exchangeBaseUrl: faultyExchange.baseUrl, + }); + + exchangesList = await wallet.listExchanges(); + t.assertTrue(exchangesList.exchanges.length === 1); + + await wallet.addExchange({ + exchangeBaseUrl: faultyExchange.baseUrl, + }); + + console.log("listing exchanges"); + + exchangesList = await wallet.listExchanges(); + t.assertTrue(exchangesList.exchanges.length === 1); + + console.log("got list", exchangesList); + + /* + * ========================================================================= + * Check what happens if the exchange returns something totally + * bogus for /keys. + * ========================================================================= + */ + + wallet.deleteDatabase(); + + exchangesList = await wallet.listExchanges(); + t.assertTrue(exchangesList.exchanges.length === 0); + + faultyExchange.faultProxy.addFault({ + modifyResponse(ctx: FaultInjectionResponseContext) { + const url = new URL(ctx.request.requestUrl); + if (url.pathname === "/keys") { + const body = { + version: "whaaat", + }; + ctx.responseBody = Buffer.from(JSON.stringify(body), "utf-8"); + } + }, + }); + + const err1 = await t.assertThrowsOperationErrorAsync(async () => { + await wallet.addExchange({ + exchangeBaseUrl: faultyExchange.baseUrl, + }); + }); + + // Response is malformed, since it didn't even contain a version code + // in a format the wallet can understand. + t.assertTrue( + err1.operationError.code === + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, + ); + + exchangesList = await wallet.listExchanges(); + t.assertTrue(exchangesList.exchanges.length === 0); + + /* + * ========================================================================= + * Check what happens if the exchange returns an old, unsupported + * version for /keys + * ========================================================================= + */ + + wallet.deleteDatabase(); + faultyExchange.faultProxy.clearAllFaults(); + + faultyExchange.faultProxy.addFault({ + modifyResponse(ctx: FaultInjectionResponseContext) { + const url = new URL(ctx.request.requestUrl); + if (url.pathname === "/keys") { + const keys = ctx.responseBody?.toString("utf-8"); + t.assertTrue(keys != null); + const keysJson = JSON.parse(keys); + keysJson["version"] = "2:0:0"; + ctx.responseBody = Buffer.from(JSON.stringify(keysJson), "utf-8"); + } + }, + }); + + const err2 = await t.assertThrowsOperationErrorAsync(async () => { + await wallet.addExchange({ + exchangeBaseUrl: faultyExchange.baseUrl, + }); + }); + + t.assertTrue( + err2.operationError.code === + TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, + ); + + exchangesList = await wallet.listExchanges(); + t.assertTrue(exchangesList.exchanges.length === 0); + + /* + * ========================================================================= + * Check that the exchange version is also checked when + * the exchange is implicitly added via the suggested + * exchange of a bank-integrated withdrawal. + * ========================================================================= + */ + + // Fault from above is still active! + + // Create withdrawal operation + + const user = await BankApi.createRandomBankUser(bank); + const wop = await BankAccessApi.createWithdrawalOperation( + bank, + user, + "TESTKUDOS:10", + ); + + // Hand it to the wallet + + const wd = await wallet.getWithdrawalDetailsForUri({ + talerWithdrawUri: wop.taler_withdraw_uri, + }); + + // Make sure the faulty exchange isn't used for the suggestion. + t.assertTrue(wd.possibleExchanges.length === 0); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts b/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts new file mode 100644 index 000000000..c56fe7abf --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts @@ -0,0 +1,204 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + GlobalTestState, + BankService, + ExchangeService, + MerchantService, + setupDb, + WalletCli, +} from "./harness"; +import { + withdrawViaBank, + makeTestPayment, + SimpleTestEnvironment, +} from "./helpers"; + +/** + * Run a test case with a simple TESTKUDOS Taler environment, consisting + * of one exchange, one bank and one merchant. + */ +export async function createMyTestkudosEnvironment( + 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 exchangeBankAccount = await bank.createExchangeAccount( + "MyExchange", + "x", + ); + exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const coinCommon = { + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + rsaKeySize: 1024, + feeDeposit: "TESTKUDOS:0.0025", + feeWithdraw: "TESTKUDOS:0", + feeRefresh: "TESTKUDOS:0", + feeRefund: "TESTKUDOS:0", + }; + + exchange.addCoinConfigList([ + { + ...coinCommon, + name: "c1", + value: "TESTKUDOS:1.28", + }, + { + ...coinCommon, + name: "c2", + value: "TESTKUDOS:0.64", + }, + { + ...coinCommon, + name: "c3", + value: "TESTKUDOS:0.32", + }, + { + ...coinCommon, + name: "c4", + value: "TESTKUDOS:0.16", + }, + { + ...coinCommon, + name: "c5", + value: "TESTKUDOS:0.08", + }, + { + ...coinCommon, + name: "c5", + value: "TESTKUDOS:0.04", + }, + { + ...coinCommon, + name: "c6", + value: "TESTKUDOS:0.02", + }, + { + ...coinCommon, + name: "c7", + value: "TESTKUDOS:0.01", + }, + ]); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: ["payto://x-taler-bank/minst1"], + }); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + return { + commonDb: db, + exchange, + merchant, + wallet, + bank, + exchangeBankAccount, + }; +} + +/** + * Run test for basic, bank-integrated withdrawal and payment. + */ +export async function runFeeRegressionTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createMyTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { + wallet, + bank, + exchange, + amount: "TESTKUDOS:1.92", + }); + + const coins = await wallet.dumpCoins(); + + // Make sure we really withdraw one 0.64 and one 1.28 coin. + t.assertTrue(coins.coins.length === 2); + + const order = { + summary: "Buy me!", + amount: "TESTKUDOS:1.30", + fulfillment_url: "taler://fulfillment-success/thx", + }; + + await makeTestPayment(t, { wallet, merchant, order }); + + await wallet.runUntilDone(); + + const txs = await wallet.getTransactions(); + t.assertAmountEquals(txs.transactions[1].amountEffective, "TESTKUDOS:1.30"); + console.log(txs); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts new file mode 100644 index 000000000..7ceccbf62 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts @@ -0,0 +1,132 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { + PreparePayResultType, + codecForMerchantOrderStatusUnpaid, + ConfirmPayResultType, + URL, +} from "taler-wallet-core"; +import axios from "axios"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runMerchantLongpollingTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + /** + * ========================================================================= + * Create an order and let the wallet pay under a session ID + * + * We check along the way that the JSON response to /orders/{order_id} + * returns the right thing. + * ========================================================================= + */ + + let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/article42", + }, + }); + + const firstOrderId = orderResp.order_id; + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + sessionId: "mysession-one", + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + t.assertTrue(orderStatus.already_paid_order_id === undefined); + let publicOrderStatusUrl = new URL(orderStatus.order_status_url); + + // Wait for half a second seconds! + publicOrderStatusUrl.searchParams.set("timeout_ms", "500"); + + let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 402) { + throw Error( + `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, + ); + } + + let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + console.log(pubUnpaidStatus); + + /** + * ========================================================================= + * Now actually pay, but WHILE a long poll is active! + * ========================================================================= + */ + + publicOrderStatusUrl.searchParams.set("timeout_ms", "5000"); + + let publicOrderStatusPromise = axios.get(publicOrderStatusUrl.href, { + validateStatus: () => true, + }); + + let preparePayResp = await wallet.preparePay({ + talerPayUri: pubUnpaidStatus.taler_pay_uri, + }); + + t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); + + const proposalId = preparePayResp.proposalId; + + publicOrderStatusResp = await publicOrderStatusPromise; + + if (publicOrderStatusResp.status != 402) { + throw Error( + `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, + ); + } + + pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + const confirmPayRes = await wallet.confirmPay({ + proposalId: proposalId, + }); + + t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); +} 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 new file mode 100644 index 000000000..27cf34b58 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts @@ -0,0 +1,284 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + GlobalTestState, + MerchantPrivateApi, + BankServiceInterface, + MerchantServiceInterface, + WalletCli, + ExchangeServiceInterface, +} from "./harness"; +import { + createSimpleTestkudosEnvironment, + withdrawViaBank, + SimpleTestEnvironment, +} from "./helpers"; +import { durationFromSpec, PreparePayResultType, URL } from "taler-wallet-core"; +import axios from "axios"; + +async function testRefundApiWithFulfillmentUrl( + t: GlobalTestState, + env: { + merchant: MerchantServiceInterface; + bank: BankServiceInterface; + wallet: WalletCli; + exchange: ExchangeServiceInterface; + }, +): Promise { + const { wallet, bank, exchange, merchant } = env; + + // Set up order. + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/fulfillment", + }, + refund_delay: durationFromSpec({ minutes: 5 }), + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const talerPayUri = orderStatus.taler_pay_uri; + const orderId = orderResp.order_id; + + // Make wallet pay for the order + + let preparePayResult = await wallet.preparePay({ + talerPayUri, + }); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + const r2 = await wallet.apiRequest("confirmPay", { + proposalId: preparePayResult.proposalId, + }); + t.assertTrue(r2.type === "response"); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + preparePayResult = await wallet.preparePay({ + talerPayUri, + }); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.AlreadyConfirmed, + ); + + await MerchantPrivateApi.giveRefund(merchant, { + amount: "TESTKUDOS:5", + instance: "default", + justification: "foo", + orderId: orderResp.order_id, + }); + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5"); + + // Now test what the merchant gives as a response for various requests to the + // public order status URL! + + let publicOrderStatusUrl = new URL( + `orders/${orderId}`, + merchant.makeInstanceBaseUrl(), + ); + publicOrderStatusUrl.searchParams.set( + "h_contract", + preparePayResult.contractTermsHash, + ); + + let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { + validateStatus: () => true, + }); + console.log(publicOrderStatusResp.data); + t.assertTrue(publicOrderStatusResp.status === 200); + t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5"); + + publicOrderStatusUrl = new URL( + `orders/${orderId}`, + merchant.makeInstanceBaseUrl(), + ); + + publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { + validateStatus: () => true, + }); + console.log(publicOrderStatusResp.data); + // We didn't give any authentication, so we should get a fulfillment URL back + t.assertTrue(publicOrderStatusResp.status === 202); + const fu = publicOrderStatusResp.data.fulfillment_url; + t.assertTrue(typeof fu === "string" && fu.startsWith("https://example.com")); +} + +async function testRefundApiWithFulfillmentMessage( + t: GlobalTestState, + env: { + merchant: MerchantServiceInterface; + bank: BankServiceInterface; + wallet: WalletCli; + exchange: ExchangeServiceInterface; + }, +): Promise { + const { wallet, bank, exchange, merchant } = env; + + // Set up order. + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_message: "Thank you for buying foobar", + }, + refund_delay: durationFromSpec({ minutes: 5 }), + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const talerPayUri = orderStatus.taler_pay_uri; + const orderId = orderResp.order_id; + + // Make wallet pay for the order + + let preparePayResult = await wallet.preparePay({ + talerPayUri, + }); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + const r2 = await wallet.apiRequest("confirmPay", { + proposalId: preparePayResult.proposalId, + }); + t.assertTrue(r2.type === "response"); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + preparePayResult = await wallet.preparePay({ + talerPayUri, + }); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.AlreadyConfirmed, + ); + + await MerchantPrivateApi.giveRefund(merchant, { + amount: "TESTKUDOS:5", + instance: "default", + justification: "foo", + orderId: orderResp.order_id, + }); + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5"); + + // Now test what the merchant gives as a response for various requests to the + // public order status URL! + + let publicOrderStatusUrl = new URL( + `orders/${orderId}`, + merchant.makeInstanceBaseUrl(), + ); + publicOrderStatusUrl.searchParams.set( + "h_contract", + preparePayResult.contractTermsHash, + ); + + let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { + validateStatus: () => true, + }); + console.log(publicOrderStatusResp.data); + t.assertTrue(publicOrderStatusResp.status === 200); + t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5"); + + publicOrderStatusUrl = new URL( + `orders/${orderId}`, + merchant.makeInstanceBaseUrl(), + ); + + publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { + validateStatus: () => true, + }); + console.log(publicOrderStatusResp.data); + // We didn't give any authentication, so we should get a fulfillment URL back + t.assertTrue(publicOrderStatusResp.status === 403); +} + +/** + * Test case for the refund API of the merchant backend. + */ +export async function runMerchantRefundApiTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + await testRefundApiWithFulfillmentUrl(t, { + wallet, + bank, + exchange, + merchant, + }); + + await testRefundApiWithFulfillmentMessage(t, { + wallet, + bank, + exchange, + merchant, + }); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts b/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts new file mode 100644 index 000000000..4fd6edc92 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts @@ -0,0 +1,199 @@ +/* + 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 test to check aborting partial payment + * via refunds. + */ + +/** + * Imports. + */ +import { + GlobalTestState, + MerchantService, + ExchangeService, + setupDb, + BankService, + WalletCli, + MerchantPrivateApi, +} from "./harness"; +import { + FaultInjectedExchangeService, + FaultInjectionRequestContext, + FaultInjectionResponseContext, +} from "./faultInjection"; +import { PreparePayResultType, URL, TalerErrorCode } from "taler-wallet-core"; +import { defaultCoinConfig } from "./denomStructures"; +import { withdrawViaBank, makeTestPayment } from "./helpers"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runPayAbortTest(t: GlobalTestState) { + // Set up test environment + + 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 exchangeBankAccount = await bank.createExchangeAccount( + "MyExchange", + "x", + ); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + await exchange.addBankAccount("1", exchangeBankAccount); + exchange.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + merchant.addExchange(faultyExchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + // Create withdrawal operation + + await withdrawViaBank(t, { + wallet, + exchange: faultyExchange, + amount: "TESTKUDOS:20", + bank, + }); + + // faultyExchange.faultProxy.addFault({ + // modifyRequest(ctx: FaultInjectionRequestContext) { + // console.log("proxy request to", ctx.requestUrl); + // } + // }); + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:15", + fulfillment_url: "taler://fulfillment-success/thx", + }, + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const preparePayResult = await wallet.preparePay({ + talerPayUri: orderStatus.taler_pay_uri, + }); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + // We let only the first deposit through! + let firstDepositUrl: string | undefined; + + faultyExchange.faultProxy.addFault({ + modifyRequest(ctx: FaultInjectionRequestContext) { + const url = new URL(ctx.requestUrl); + if (url.pathname.endsWith("/deposit")) { + if (!firstDepositUrl) { + firstDepositUrl = url.href; + return; + } + if (url.href != firstDepositUrl) { + url.pathname = "/doesntexist"; + ctx.requestUrl = url.href; + } + } + }, + modifyResponse(ctx: FaultInjectionResponseContext) { + const url = new URL(ctx.request.requestUrl); + if (url.pathname.endsWith("/deposit") && url.href != firstDepositUrl) { + ctx.responseBody = Buffer.from("{}"); + ctx.statusCode = 500; + } + }, + }); + + await t.assertThrowsOperationErrorAsync(async () => { + await wallet.confirmPay({ + proposalId: preparePayResult.proposalId, + }); + }); + + let txr = await wallet.getTransactions(); + console.log(JSON.stringify(txr, undefined, 2)); + + t.assertDeepEqual(txr.transactions[1].type, "payment"); + t.assertDeepEqual(txr.transactions[1].pending, true); + t.assertDeepEqual( + txr.transactions[1].error?.code, + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + ); + + await wallet.abortFailedPayWithRefund({ + proposalId: preparePayResult.proposalId, + }); + + await wallet.runUntilDone(); + + txr = await wallet.getTransactions(); + console.log(JSON.stringify(txr, undefined, 2)); + + const txTypes = txr.transactions.map((x) => x.type); + + t.assertDeepEqual(txTypes, ["withdrawal", "payment", "refund"]); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts b/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts new file mode 100644 index 000000000..4d2706608 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts @@ -0,0 +1,206 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { + withdrawViaBank, + createFaultInjectedMerchantTestkudosEnvironment, +} from "./helpers"; +import { + PreparePayResultType, + codecForMerchantOrderStatusUnpaid, + ConfirmPayResultType, + URL, +} from "taler-wallet-core"; +import axios from "axios"; +import { FaultInjectionRequestContext } from "./faultInjection"; + +/** + * Run test for the wallets repurchase detection mechanism + * based on the fulfillment URL. + * + * FIXME: This test is now almost the same as test-paywall-flow, + * since we can't initiate payment via a "claimed" private order status + * response. + */ +export async function runPayPaidTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + faultyMerchant, + } = await createFaultInjectedMerchantTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + /** + * ========================================================================= + * Create an order and let the wallet pay under a session ID + * + * We check along the way that the JSON response to /orders/{order_id} + * returns the right thing. + * ========================================================================= + */ + + const merchant = faultyMerchant; + + let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/article42", + }, + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + sessionId: "mysession-one", + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + t.assertTrue(orderStatus.already_paid_order_id === undefined); + let publicOrderStatusUrl = orderStatus.order_status_url; + + let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 402) { + throw Error( + `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, + ); + } + + let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + console.log(pubUnpaidStatus); + + let preparePayResp = await wallet.preparePay({ + talerPayUri: pubUnpaidStatus.taler_pay_uri, + }); + + t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); + + const proposalId = preparePayResp.proposalId; + + publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 402) { + throw Error( + `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, + ); + } + + pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + const confirmPayRes = await wallet.confirmPay({ + proposalId: proposalId, + }); + + t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); + + publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + console.log(publicOrderStatusResp.data); + + if (publicOrderStatusResp.status != 202) { + console.log(publicOrderStatusResp.data); + throw Error( + `expected status 202 (after paying), but got ${publicOrderStatusResp.status}`, + ); + } + + /** + * ========================================================================= + * Now change up the session ID and do payment re-play! + * ========================================================================= + */ + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + sessionId: "mysession-two", + }); + + console.log( + "order status under mysession-two:", + JSON.stringify(orderStatus, undefined, 2), + ); + + // Should be claimed (not paid!) because of a new session ID + t.assertTrue(orderStatus.order_status === "claimed"); + + let numPayRequested = 0; + let numPaidRequested = 0; + + faultyMerchant.faultProxy.addFault({ + modifyRequest(ctx: FaultInjectionRequestContext) { + const url = new URL(ctx.requestUrl); + if (url.pathname.endsWith("/pay")) { + numPayRequested++; + } else if (url.pathname.endsWith("/paid")) { + numPaidRequested++; + } + }, + }); + + let orderRespTwo = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/article42", + }, + }); + + let orderStatusTwo = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderRespTwo.order_id, + sessionId: "mysession-two", + }, + ); + + t.assertTrue(orderStatusTwo.order_status === "unpaid"); + + // Pay with new taler://pay URI, which should + // have the new session ID! + // Wallet should now automatically re-play payment. + preparePayResp = await wallet.preparePay({ + talerPayUri: orderStatusTwo.taler_pay_uri, + }); + + t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); + t.assertTrue(preparePayResp.paid); + + // Make sure the wallet is actually doing the replay properly. + t.assertTrue(numPaidRequested == 1); + t.assertTrue(numPayRequested == 0); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts new file mode 100644 index 000000000..ee8a8e49c --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts @@ -0,0 +1,104 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, MerchantPrivateApi, WalletCli } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { PreparePayResultType, TalerErrorCode } from "taler-wallet-core"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runPaymentClaimTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + const walletTwo = new WalletCli(t, "two"); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Set up order. + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }, + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const talerPayUri = orderStatus.taler_pay_uri; + + // Make wallet pay for the order + + const preparePayResult = await wallet.preparePay({ + talerPayUri, + }); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + t.assertThrowsOperationErrorAsync(async () => { + await walletTwo.preparePay({ + talerPayUri, + }); + }); + + const r2 = await wallet.apiRequest("confirmPay", { + // FIXME: should be validated, don't cast! + proposalId: preparePayResult.proposalId, + }); + t.assertTrue(r2.type === "response"); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + walletTwo.deleteDatabase(); + + const err = await t.assertThrowsOperationErrorAsync(async () => { + await walletTwo.preparePay({ + talerPayUri, + }); + }); + + t.assertTrue( + err.operationError.code === TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED, + ); + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts new file mode 100644 index 000000000..55609f3fc --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts @@ -0,0 +1,209 @@ +/* + 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 + */ + +/** + * Sample fault injection test. + */ + +/** + * Imports. + */ +import { + GlobalTestState, + MerchantService, + ExchangeService, + setupDb, + BankService, + WalletCli, + MerchantPrivateApi, + BankApi, + BankAccessApi, +} from "./harness"; +import { + FaultInjectedExchangeService, + FaultInjectionRequestContext, + FaultInjectionResponseContext, +} from "./faultInjection"; +import { CoreApiResponse } from "taler-wallet-core"; +import { defaultCoinConfig } from "./denomStructures"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runPaymentFaultTest(t: GlobalTestState) { + // Set up test environment + + 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 exchangeBankAccount = await bank.createExchangeAccount( + "MyExchange", + "x", + ); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + await exchange.addBankAccount("1", exchangeBankAccount); + exchange.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); + + // Print all requests to the exchange + faultyExchange.faultProxy.addFault({ + modifyRequest(ctx: FaultInjectionRequestContext) { + console.log("got request", ctx); + }, + modifyResponse(ctx: FaultInjectionResponseContext) { + console.log("got response", ctx); + }, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + merchant.addExchange(faultyExchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + // Create withdrawal operation + + const user = await BankApi.createRandomBankUser(bank); + const wop = await BankAccessApi.createWithdrawalOperation( + bank, + user, + "TESTKUDOS:20", + ); + + // Hand it to the wallet + + const r1 = await wallet.apiRequest("getWithdrawalDetailsForUri", { + talerWithdrawUri: wop.taler_withdraw_uri, + }); + t.assertTrue(r1.type === "response"); + + await wallet.runPending(); + + // Confirm it + + await BankApi.confirmWithdrawalOperation(bank, user, wop); + + // Withdraw + + const r2 = await wallet.apiRequest("acceptBankIntegratedWithdrawal", { + exchangeBaseUrl: faultyExchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + }); + t.assertTrue(r2.type === "response"); + await wallet.runUntilDone(); + + // Check balance + + const balApiResp = await wallet.apiRequest("getBalances", {}); + t.assertTrue(balApiResp.type === "response"); + + // Set up order. + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }, + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + let apiResp: CoreApiResponse; + + apiResp = await wallet.apiRequest("preparePay", { + talerPayUri: orderStatus.taler_pay_uri, + }); + t.assertTrue(apiResp.type === "response"); + + const proposalId = (apiResp.result as any).proposalId; + + await wallet.runPending(); + + // Drop 3 responses from the exchange. + let faultCount = 0; + faultyExchange.faultProxy.addFault({ + modifyResponse(ctx: FaultInjectionResponseContext) { + if (faultCount < 3) { + faultCount++; + ctx.dropResponse = true; + } + }, + }); + + // confirmPay won't work, as the exchange is unreachable + + apiResp = await wallet.apiRequest("confirmPay", { + // FIXME: should be validated, don't cast! + proposalId: proposalId, + }); + t.assertTrue(apiResp.type === "error"); + + await wallet.runUntilDone(); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts new file mode 100644 index 000000000..4323a7c9d --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts @@ -0,0 +1,103 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { PreparePayResultType } from "taler-wallet-core"; + +/** + * Test the wallet-core payment API, especially that repeated operations + * return the expected result. + */ +export async function runPaymentIdempotencyTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Set up order. + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }, + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const talerPayUri = orderStatus.taler_pay_uri; + + // Make wallet pay for the order + + const preparePayResult = await wallet.preparePay({ + talerPayUri: orderStatus.taler_pay_uri, + }); + + const preparePayResultRep = await wallet.preparePay({ + talerPayUri: orderStatus.taler_pay_uri, + }); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + t.assertTrue( + preparePayResultRep.status === PreparePayResultType.PaymentPossible, + ); + + const proposalId = preparePayResult.proposalId; + + const r2 = await wallet.apiRequest("confirmPay", { + // FIXME: should be validated, don't cast! + proposalId: proposalId, + }); + t.assertTrue(r2.type === "response"); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + const preparePayResultAfter = await wallet.preparePay({ + talerPayUri, + }); + + t.assertTrue( + preparePayResultAfter.status === PreparePayResultType.AlreadyConfirmed, + ); + t.assertTrue(preparePayResultAfter.paid === true); + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts new file mode 100644 index 000000000..6d05df33b --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts @@ -0,0 +1,160 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + GlobalTestState, + setupDb, + BankService, + ExchangeService, + MerchantService, + WalletCli, + MerchantPrivateApi, +} from "./harness"; +import { withdrawViaBank } from "./helpers"; +import { coin_ct10, coin_u1 } from "./denomStructures"; + +async function setupTest( + t: GlobalTestState, +): Promise<{ + merchant: MerchantService; + exchange: ExchangeService; + bank: BankService; +}> { + 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 exchangeBankAccount = await bank.createExchangeAccount( + "MyExchange", + "x", + ); + + exchange.addOfferedCoins([coin_ct10, coin_u1]); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + await exchange.addBankAccount("1", exchangeBankAccount); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: ["payto://x-taler-bank/minst1"], + }); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + console.log("setup done!"); + + return { + merchant, + bank, + exchange, + }; +} + +/** + * Run test. + * + * This test uses a very sub-optimal denomination structure. + */ +export async function runPaymentMultipleTest(t: GlobalTestState) { + // Set up test environment + + const { merchant, bank, exchange } = await setupTest(t); + + const wallet = new WalletCli(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:100" }); + + // Set up order. + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:80", + fulfillment_url: "taler://fulfillment-success/thx", + }, + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const r1 = await wallet.apiRequest("preparePay", { + talerPayUri: orderStatus.taler_pay_uri, + }); + t.assertTrue(r1.type === "response"); + + const r2 = await wallet.apiRequest("confirmPay", { + // FIXME: should be validated, don't cast! + proposalId: (r1.result as any).proposalId, + }); + t.assertTrue(r2.type === "response"); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts new file mode 100644 index 000000000..73973bdc0 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts @@ -0,0 +1,172 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { + withdrawViaBank, + createFaultInjectedMerchantTestkudosEnvironment, +} from "./helpers"; +import { + PreparePayResultType, + codecForMerchantOrderStatusUnpaid, + ConfirmPayResultType, + URL, + codecForExchangeKeysJson, + TalerErrorDetails, + TalerErrorCode, +} from "taler-wallet-core"; +import axios from "axios"; +import { + FaultInjectionRequestContext, + FaultInjectionResponseContext, +} from "./faultInjection"; + +/** + * Run test for a payment where the merchant has a transient + * failure in /pay + */ +export async function runPaymentTransientTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + faultyMerchant, + } = await createFaultInjectedMerchantTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + const merchant = faultyMerchant; + + let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/article42", + }, + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + sessionId: "mysession-one", + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + t.assertTrue(orderStatus.already_paid_order_id === undefined); + let publicOrderStatusUrl = orderStatus.order_status_url; + + let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 402) { + throw Error( + `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, + ); + } + + let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + console.log(pubUnpaidStatus); + + let preparePayResp = await wallet.preparePay({ + talerPayUri: pubUnpaidStatus.taler_pay_uri, + }); + + t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); + + const proposalId = preparePayResp.proposalId; + + publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 402) { + throw Error( + `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, + ); + } + + pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + let faultInjected = false; + + faultyMerchant.faultProxy.addFault({ + modifyResponse(ctx: FaultInjectionResponseContext) { + console.log("in modifyResponse"); + const url = new URL(ctx.request.requestUrl); + console.log("pathname is", url.pathname); + if (!url.pathname.endsWith("/pay")) { + return; + } + if (faultInjected) { + console.log("not injecting pay fault"); + return; + } + faultInjected = true; + console.log("injecting pay fault"); + const err: TalerErrorDetails = { + code: TalerErrorCode.GENERIC_DB_COMMIT_FAILED, + details: {}, + hint: "huh", + message: "something went wrong", + }; + ctx.responseBody = Buffer.from(JSON.stringify(err)); + ctx.statusCode = 500; + }, + }); + + const confirmPayResp = await wallet.confirmPay({ + proposalId, + }); + + console.log(confirmPayResp); + + t.assertTrue(confirmPayResp.type === ConfirmPayResultType.Pending); + t.assertTrue(faultInjected); + + const confirmPayRespTwo = await wallet.confirmPay({ + proposalId, + }); + + t.assertTrue(confirmPayRespTwo.type === ConfirmPayResultType.Done); + + // Now ask the merchant if paid + + publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + console.log(publicOrderStatusResp.data); + + if (publicOrderStatusResp.status != 202) { + console.log(publicOrderStatusResp.data); + throw Error( + `expected status 202 (after paying), but got ${publicOrderStatusResp.status}`, + ); + } +} \ No newline at end of file diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment.ts new file mode 100644 index 000000000..68713fd9d --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-payment.ts @@ -0,0 +1,53 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState } from "./harness"; +import { + createSimpleTestkudosEnvironment, + withdrawViaBank, + makeTestPayment, +} from "./helpers"; + +/** + * Run test for basic, bank-integrated withdrawal and payment. + */ +export async function runPaymentTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + const order = { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }; + + await makeTestPayment(t, { wallet, merchant, order }); + + await wallet.runUntilDone(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts b/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts new file mode 100644 index 000000000..5eeb7d274 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts @@ -0,0 +1,233 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { + PreparePayResultType, + codecForMerchantOrderStatusUnpaid, + ConfirmPayResultType, +} from "taler-wallet-core"; +import axios from "axios"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runPaywallFlowTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + /** + * ========================================================================= + * Create an order and let the wallet pay under a session ID + * + * We check along the way that the JSON response to /orders/{order_id} + * returns the right thing. + * ========================================================================= + */ + + let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/article42", + }, + }); + + const firstOrderId = orderResp.order_id; + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + sessionId: "mysession-one", + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const talerPayUriOne = orderStatus.taler_pay_uri; + + t.assertTrue(orderStatus.already_paid_order_id === undefined); + let publicOrderStatusUrl = orderStatus.order_status_url; + + let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 402) { + throw Error( + `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, + ); + } + + let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + console.log(pubUnpaidStatus); + + let preparePayResp = await wallet.preparePay({ + talerPayUri: pubUnpaidStatus.taler_pay_uri, + }); + + t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); + + const proposalId = preparePayResp.proposalId; + + publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 402) { + throw Error( + `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, + ); + } + + pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + const confirmPayRes = await wallet.confirmPay({ + proposalId: proposalId, + }); + + t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); + + publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + console.log(publicOrderStatusResp.data); + + if (publicOrderStatusResp.status != 202) { + console.log(publicOrderStatusResp.data); + throw Error( + `expected status 202 (after paying), but got ${publicOrderStatusResp.status}`, + ); + } + + /** + * ========================================================================= + * Now change up the session ID! + * ========================================================================= + */ + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + sessionId: "mysession-two", + }); + + // Should be claimed (not paid!) because of a new session ID + t.assertTrue(orderStatus.order_status === "claimed"); + + // Pay with new taler://pay URI, which should + // have the new session ID! + // Wallet should now automatically re-play payment. + preparePayResp = await wallet.preparePay({ + talerPayUri: talerPayUriOne, + }); + + t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); + t.assertTrue(preparePayResp.paid); + + /** + * ========================================================================= + * Now we test re-purchase detection. + * ========================================================================= + */ + + orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + // Same fulfillment URL as previously! + fulfillment_url: "https://example.com/article42", + }, + }); + + const secondOrderId = orderResp.order_id; + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: secondOrderId, + sessionId: "mysession-three", + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + t.assertTrue(orderStatus.already_paid_order_id === undefined); + publicOrderStatusUrl = orderStatus.order_status_url; + + // Here the re-purchase detection should kick in, + // and the wallet should re-pay for the old order + // under the new session ID (mysession-three). + preparePayResp = await wallet.preparePay({ + talerPayUri: orderStatus.taler_pay_uri, + }); + + t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); + t.assertTrue(preparePayResp.paid); + + // The first order should now be paid under "mysession-three", + // as the wallet did re-purchase detection + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: firstOrderId, + sessionId: "mysession-three", + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + // Check that with a completely new session ID, the status would NOT + // be paid. + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: firstOrderId, + sessionId: "mysession-four", + }); + + t.assertTrue(orderStatus.order_status === "claimed"); + + // Now check if the public status of the new order is correct. + + console.log("requesting public status", publicOrderStatusUrl); + + // Ask the order status of the claimed-but-unpaid order + publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 402) { + throw Error(`expected status 402, but got ${publicOrderStatusResp.status}`); + } + + pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + console.log(publicOrderStatusResp.data); + + t.assertTrue(pubUnpaidStatus.already_paid_order_id === firstOrderId); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts new file mode 100644 index 000000000..afac993be --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts @@ -0,0 +1,100 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { durationFromSpec } from "taler-wallet-core"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runRefundAutoTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Set up order. + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + auto_refund: { + d_ms: 3000, + }, + }, + refund_delay: durationFromSpec({ minutes: 5 }), + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const r1 = await wallet.apiRequest("preparePay", { + talerPayUri: orderStatus.taler_pay_uri, + }); + t.assertTrue(r1.type === "response"); + + const r2 = await wallet.apiRequest("confirmPay", { + // FIXME: should be validated, don't cast! + proposalId: (r1.result as any).proposalId, + }); + t.assertTrue(r2.type === "response"); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + const ref = await MerchantPrivateApi.giveRefund(merchant, { + amount: "TESTKUDOS:5", + instance: "default", + justification: "foo", + orderId: orderResp.order_id, + }); + + console.log(ref); + + // The wallet should now automatically pick up the refund. + await wallet.runUntilDone(); + + const transactions = await wallet.getTransactions(); + console.log(JSON.stringify(transactions, undefined, 2)); + + const transactionTypes = transactions.transactions.map((x) => x.type); + t.assertDeepEqual(transactionTypes, ["withdrawal", "payment", "refund"]); + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts new file mode 100644 index 000000000..483e9e06d --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts @@ -0,0 +1,127 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { + createSimpleTestkudosEnvironment, + withdrawViaBank, + applyTimeTravel, +} from "./helpers"; +import { + durationFromSpec, + timestampAddDuration, + getTimestampNow, + timestampTruncateToSecond, +} from "taler-wallet-core"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runRefundGoneTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Set up order. + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + pay_deadline: timestampTruncateToSecond( + timestampAddDuration( + getTimestampNow(), + durationFromSpec({ + minutes: 10, + }), + ), + ), + }, + refund_delay: durationFromSpec({ minutes: 1 }), + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const r1 = await wallet.apiRequest("preparePay", { + talerPayUri: orderStatus.taler_pay_uri, + }); + t.assertTrue(r1.type === "response"); + + const r2 = await wallet.apiRequest("confirmPay", { + // FIXME: should be validated, don't cast! + proposalId: (r1.result as any).proposalId, + }); + t.assertTrue(r2.type === "response"); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + console.log(orderStatus); + + await applyTimeTravel(durationFromSpec({ hours: 1 }), { exchange, wallet }); + + await exchange.runAggregatorOnce(); + + const ref = await MerchantPrivateApi.giveRefund(merchant, { + amount: "TESTKUDOS:5", + instance: "default", + justification: "foo", + orderId: orderResp.order_id, + }); + + console.log(ref); + + let rr = await wallet.applyRefund({ + talerRefundUri: ref.talerRefundUri, + }); + + t.assertAmountEquals(rr.amountRefundGone, "TESTKUDOS:5"); + console.log(rr); + + await wallet.runUntilDone(); + + let r = await wallet.apiRequest("getBalances", {}); + console.log(JSON.stringify(r, undefined, 2)); + + r = await wallet.apiRequest("getTransactions", {}); + console.log(JSON.stringify(r, undefined, 2)); + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts new file mode 100644 index 000000000..d90a4b5f6 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts @@ -0,0 +1,186 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, delayMs, MerchantPrivateApi } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { TransactionType, Amounts, durationFromSpec } from "taler-wallet-core"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runRefundIncrementalTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Set up order. + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:10", + fulfillment_url: "taler://fulfillment-success/thx", + }, + refund_delay: durationFromSpec({ minutes: 5 }), + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const r1 = await wallet.apiRequest("preparePay", { + talerPayUri: orderStatus.taler_pay_uri, + }); + t.assertTrue(r1.type === "response"); + + const r2 = await wallet.apiRequest("confirmPay", { + // FIXME: should be validated, don't cast! + proposalId: (r1.result as any).proposalId, + }); + t.assertTrue(r2.type === "response"); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + let ref = await MerchantPrivateApi.giveRefund(merchant, { + amount: "TESTKUDOS:2.5", + instance: "default", + justification: "foo", + orderId: orderResp.order_id, + }); + + console.log("first refund increase response", ref); + + { + let wr = await wallet.applyRefund({ + talerRefundUri: ref.talerRefundUri, + }); + console.log(wr); + const txs = await wallet.getTransactions(); + console.log( + "transactions after applying first refund:", + JSON.stringify(txs, undefined, 2), + ); + } + + // Wait at least a second, because otherwise the increased + // refund will be grouped with the previous one. + await delayMs(1200); + + ref = await MerchantPrivateApi.giveRefund(merchant, { + amount: "TESTKUDOS:5", + instance: "default", + justification: "bar", + orderId: orderResp.order_id, + }); + + console.log("second refund increase response", ref); + + // Wait at least a second, because otherwise the increased + // refund will be grouped with the previous one. + await delayMs(1200); + + ref = await MerchantPrivateApi.giveRefund(merchant, { + amount: "TESTKUDOS:10", + instance: "default", + justification: "bar", + orderId: orderResp.order_id, + }); + + console.log("third refund increase response", ref); + + { + let wr = await wallet.applyRefund({ + talerRefundUri: ref.talerRefundUri, + }); + console.log(wr); + } + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:10"); + + console.log(JSON.stringify(orderStatus, undefined, 2)); + + await wallet.runUntilDone(); + + const bal = await wallet.getBalances(); + console.log(JSON.stringify(bal, undefined, 2)); + + { + const txs = await wallet.getTransactions(); + console.log(JSON.stringify(txs, undefined, 2)); + + const txTypes = txs.transactions.map((x) => x.type); + t.assertDeepEqual(txTypes, [ + "withdrawal", + "payment", + "refund", + "refund", + "refund", + ]); + + for (const tx of txs.transactions) { + if (tx.type !== TransactionType.Refund) { + continue; + } + t.assertAmountLeq(tx.amountEffective, tx.amountRaw); + } + + const raw = Amounts.sum( + txs.transactions + .filter((x) => x.type === TransactionType.Refund) + .map((x) => x.amountRaw), + ).amount; + + t.assertAmountEquals("TESTKUDOS:10", raw); + + const effective = Amounts.sum( + txs.transactions + .filter((x) => x.type === TransactionType.Refund) + .map((x) => x.amountEffective), + ).amount; + + t.assertAmountEquals("TESTKUDOS:8.33", effective); + } + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund.ts new file mode 100644 index 000000000..12e6b178f --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-refund.ts @@ -0,0 +1,103 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { durationFromSpec } from "taler-wallet-core"; +import { GlobalTestState, MerchantPrivateApi } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runRefundTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Set up order. + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }, + refund_delay: durationFromSpec({ minutes: 5 }), + }); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const r1 = await wallet.apiRequest("preparePay", { + talerPayUri: orderStatus.taler_pay_uri, + }); + t.assertTrue(r1.type === "response"); + + const r2 = await wallet.apiRequest("confirmPay", { + // FIXME: should be validated, don't cast! + proposalId: (r1.result as any).proposalId, + }); + t.assertTrue(r2.type === "response"); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "paid"); + + const ref = await MerchantPrivateApi.giveRefund(merchant, { + amount: "TESTKUDOS:5", + instance: "default", + justification: "foo", + orderId: orderResp.order_id, + }); + + console.log(ref); + + let r = await wallet.apiRequest("applyRefund", { + talerRefundUri: ref.talerRefundUri, + }); + t.assertTrue(r.type === "response"); + console.log(r); + + await wallet.runUntilDone(); + + r = await wallet.apiRequest("getBalances", {}); + console.log(JSON.stringify(r, undefined, 2)); + + r = await wallet.apiRequest("getTransactions", {}); + console.log(JSON.stringify(r, undefined, 2)); + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts b/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts new file mode 100644 index 000000000..ac989855c --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts @@ -0,0 +1,120 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + GlobalTestState, + ExchangeService, + MerchantService, + WalletCli, +} from "./harness"; +import { + createSimpleTestkudosEnvironment, + withdrawViaBank, + makeTestPayment, +} from "./helpers"; +import { CoinDumpJson } from "taler-wallet-core"; + +async function revokeAllWalletCoins(req: { + wallet: WalletCli; + exchange: ExchangeService; + merchant: MerchantService; +}): Promise { + const { wallet, exchange, merchant } = req; + const coinDump = await wallet.dumpCoins(); + console.log(coinDump); + const usedDenomHashes = new Set(); + for (const coin of coinDump.coins) { + usedDenomHashes.add(coin.denom_pub_hash); + } + + await exchange.stop(); + + for (const x of usedDenomHashes.values()) { + await exchange.revokeDenomination(x); + } + + await exchange.keyup(); + + await exchange.start(); + await exchange.pingUntilAvailable(); + await merchant.stop(); + await merchant.start(); + await merchant.pingUntilAvailable(); +} + +/** + * Basic time travel test. + */ +export async function runRevocationTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); + + await revokeAllWalletCoins({ wallet, exchange, merchant }); + + // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565 + // is implemented. + await wallet.forceUpdateExchange({ exchangeBaseUrl: exchange.baseUrl }); + await wallet.runUntilDone(); + await wallet.runUntilDone(); + const bal = await wallet.getBalances(); + console.log("wallet balance", bal); + + const order = { + summary: "Buy me!", + amount: "TESTKUDOS:10", + fulfillment_url: "taler://fulfillment-success/thx", + }; + + await makeTestPayment(t, { wallet, merchant, order }); + + wallet.deleteDatabase(); + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); + + const coinDump = await wallet.dumpCoins(); + console.log(coinDump); + const coinPubList = coinDump.coins.map((x) => x.coin_pub); + await wallet.forceRefresh({ + coinPubList, + }); + await wallet.runUntilDone(); + + await revokeAllWalletCoins({ wallet, exchange, merchant }); + + // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565 + // is implemented. + await wallet.forceUpdateExchange({ exchangeBaseUrl: exchange.baseUrl }); + await wallet.runUntilDone(); + await wallet.runUntilDone(); + { + const bal = await wallet.getBalances(); + console.log("wallet balance", bal); + } + + await makeTestPayment(t, { wallet, merchant, order }); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts new file mode 100644 index 000000000..747f6d75d --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts @@ -0,0 +1,203 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + ConfirmPayResultType, + Duration, + durationFromSpec, + PendingOperationsResponse, + PreparePayResultType, +} from "taler-wallet-core"; +import { makeNoFeeCoinConfig } from "./denomStructures"; +import { + BankService, + ExchangeService, + GlobalTestState, + MerchantPrivateApi, + MerchantService, + setupDb, + WalletCli, +} from "./harness"; +import { startWithdrawViaBank, withdrawViaBank } from "./helpers"; + +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); + } +} + +/** + * Basic time travel test. + */ +export async function runTimetravelAutorefreshTest(t: GlobalTestState) { + // Set up test environment + + 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(makeNoFeeCoinConfig("TESTKUDOS")); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: ["payto://x-taler-bank/minst1"], + }); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); + + // Travel into the future, the deposit expiration is two years + // into the future. + await applyTimeTravel(durationFromSpec({ days: 400 }), { + wallet, + exchange, + merchant, + }); + + await wallet.runUntilDone(); + + let p: PendingOperationsResponse; + p = await wallet.getPendingOperations(); + + console.log("pending operations after first time travel"); + console.log(JSON.stringify(p, undefined, 2)); + + await startWithdrawViaBank(t, { + wallet, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + await wallet.runUntilDone(); + + // Travel into the future, the deposit expiration is two years + // into the future. + await applyTimeTravel(durationFromSpec({ years: 2, months: 6 }), { + wallet, + exchange, + merchant, + }); + + await wallet.runUntilDone(); + + const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { + order: { + fulfillment_url: "http://example.com", + summary: "foo", + amount: "TESTKUDOS:30", + }, + }); + + const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + instance: "default", + }, + ); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const r = await wallet.preparePay({ + talerPayUri: orderStatus.taler_pay_uri, + }); + + console.log(r); + + t.assertTrue(r.status === PreparePayResultType.PaymentPossible); + + const cpr = await wallet.confirmPay({ + proposalId: r.proposalId, + }); + + t.assertTrue(cpr.type === ConfirmPayResultType.Done); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts new file mode 100644 index 000000000..b4de50b95 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts @@ -0,0 +1,90 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState } from "./harness"; +import { + createSimpleTestkudosEnvironment, + withdrawViaBank, + startWithdrawViaBank, +} from "./helpers"; +import { Duration, TransactionType } from "taler-wallet-core"; + +/** + * Basic time travel test. + */ +export async function runTimetravelWithdrawTest(t: GlobalTestState){ + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); + + // Travel 400 days into the future, + // as the deposit expiration is two years + // into the future. + const timetravelDuration: Duration = { + d_ms: 1000 * 60 * 60 * 24 * 400, + }; + + await exchange.stop(); + exchange.setTimetravel(timetravelDuration); + await exchange.keyup(); + await exchange.start(); + await exchange.pingUntilAvailable(); + + await merchant.stop(); + merchant.setTimetravel(timetravelDuration); + await merchant.start(); + await merchant.pingUntilAvailable(); + + // This should fail, as the wallet didn't time travel yet. + await startWithdrawViaBank(t, { + wallet, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + // Check that transactions are correct for the failed withdrawal + { + await wallet.runUntilDone({ maxRetries: 5 }); + const transactions = await wallet.getTransactions(); + console.log(transactions); + const types = transactions.transactions.map((x) => x.type); + t.assertDeepEqual(types, ["withdrawal", "withdrawal"]); + const wtrans = transactions.transactions[1]; + t.assertTrue(wtrans.type === TransactionType.Withdrawal); + t.assertTrue(wtrans.pending); + } + + // Now we also let the wallet time travel + + wallet.setTimetravel(timetravelDuration); + + // This doesn't work yet, see https://bugs.taler.net/n/6585 + + // await wallet.runUntilDone({ maxRetries: 5 }); +} \ No newline at end of file diff --git a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts new file mode 100644 index 000000000..01ec6c1bb --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts @@ -0,0 +1,127 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { + GlobalTestState, + MerchantPrivateApi, + BankApi, +} from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runTippingTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + merchant, + exchangeBankAccount, + } = await createSimpleTestkudosEnvironment(t); + + const mbu = await BankApi.createRandomBankUser(bank); + + const tipReserveResp = await MerchantPrivateApi.createTippingReserve( + merchant, + "default", + { + exchange_url: exchange.baseUrl, + initial_balance: "TESTKUDOS:10", + wire_method: "x-taler-bank", + }, + ); + + console.log("tipReserveResp:", tipReserveResp); + + t.assertDeepEqual( + tipReserveResp.payto_uri, + exchangeBankAccount.accountPaytoUri, + ); + + await BankApi.adminAddIncoming(bank, { + amount: "TESTKUDOS:10", + debitAccountPayto: mbu.accountPaytoUri, + exchangeBankAccount, + reservePub: tipReserveResp.reserve_pub, + }); + + await exchange.runWirewatchOnce(); + await merchant.stop(); + await merchant.start(); + await merchant.pingUntilAvailable(); + + const r = await MerchantPrivateApi.queryTippingReserves(merchant, "default"); + console.log("tipping reserves:", JSON.stringify(r, undefined, 2)); + + t.assertTrue(r.reserves.length === 1); + t.assertDeepEqual( + r.reserves[0].exchange_initial_amount, + r.reserves[0].merchant_initial_amount, + ); + + const tip = await MerchantPrivateApi.giveTip(merchant, "default", { + amount: "TESTKUDOS:5", + justification: "why not?", + next_url: "https://example.com/after-tip", + }); + + console.log("created tip", tip); + + const doTip = async (): Promise => { + const ptr = await wallet.prepareTip({ + talerTipUri: tip.taler_tip_uri, + }); + + console.log(ptr); + + t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5"); + t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85"); + + await wallet.acceptTip({ + walletTipId: ptr.walletTipId, + }); + + await wallet.runUntilDone(); + + const bal = await wallet.getBalances(); + + console.log(bal); + + t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:4.85"); + + const txns = await wallet.getTransactions(); + + console.log("Transactions:", JSON.stringify(txns, undefined, 2)); + + t.assertDeepEqual(txns.transactions[0].type, "tip"); + t.assertDeepEqual(txns.transactions[0].pending, false); + t.assertAmountEquals( + txns.transactions[0].amountEffective, + "TESTKUDOS:4.85", + ); + t.assertAmountEquals(txns.transactions[0].amountRaw, "TESTKUDOS:5.0"); + }; + + // Check twice so make sure tip handling is idempotent + await doTip(); + await doTip(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts new file mode 100644 index 000000000..cdb954858 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts @@ -0,0 +1,87 @@ +/* + 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 + */ + +/** + * Integration test for the wallet testing functionality used by the exchange + * test cases. + */ + +/** + * Imports. + */ +import { GlobalTestState } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runWallettestingTest(t: GlobalTestState) { + const { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + + await wallet.runIntegrationTest({ + amountToSpend: "TESTKUDOS:5", + amountToWithdraw: "TESTKUDOS:10", + bankBaseUrl: bank.baseUrl, + exchangeBaseUrl: exchange.baseUrl, + merchantApiKey: "sandbox", + merchantBaseUrl: merchant.makeInstanceBaseUrl(), + }); + + let txns = await wallet.getTransactions(); + console.log(JSON.stringify(txns, undefined, 2)); + let txTypes = txns.transactions.map((x) => x.type); + + t.assertDeepEqual(txTypes, [ + "withdrawal", + "payment", + "withdrawal", + "payment", + "refund", + "payment", + ]); + + wallet.deleteDatabase(); + + await wallet.withdrawTestBalance({ + amount: "TESTKUDOS:10", + bankBaseUrl: bank.baseUrl, + exchangeBaseUrl: exchange.baseUrl, + }); + + await wallet.runUntilDone(); + + await wallet.testPay({ + amount: "TESTKUDOS:5", + merchantApiKey: "sandbox", + merchantBaseUrl: merchant.makeInstanceBaseUrl(), + summary: "foo", + }); + + await wallet.runUntilDone(); + + txns = await wallet.getTransactions(); + console.log(JSON.stringify(txns, undefined, 2)); + txTypes = txns.transactions.map((x) => x.type); + + t.assertDeepEqual(txTypes, ["withdrawal", "payment"]); + + await t.shutdown(); +} 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 new file mode 100644 index 000000000..9a4e6004b --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts @@ -0,0 +1,67 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, BankApi, BankAccessApi } from "./harness"; +import { createSimpleTestkudosEnvironment } from "./helpers"; +import { codecForBalancesResponse, TalerErrorCode } from "taler-wallet-core"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runWithdrawalAbortBankTest(t: GlobalTestState) { + // Set up test environment + + const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t); + + // Create a withdrawal operation + + const user = await BankApi.createRandomBankUser(bank); + const wop = await BankAccessApi.createWithdrawalOperation( + bank, + user, + "TESTKUDOS:10", + ); + + // Hand it to the wallet + + const r1 = await wallet.apiRequest("getWithdrawalDetailsForUri", { + talerWithdrawUri: wop.taler_withdraw_uri, + }); + t.assertTrue(r1.type === "response"); + + await wallet.runPending(); + + // Confirm it + + await BankApi.abortWithdrawalOperation(bank, user, wop); + + // Withdraw + + const r2 = await wallet.apiRequest("acceptBankIntegratedWithdrawal", { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + }); + t.assertTrue(r2.type === "error"); + t.assertTrue( + r2.error.code === + TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK, + ); + + await t.shutdown(); +} 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 new file mode 100644 index 000000000..89f5104a9 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts @@ -0,0 +1,71 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, BankApi, BankAccessApi } from "./harness"; +import { createSimpleTestkudosEnvironment } from "./helpers"; +import { codecForBalancesResponse } from "taler-wallet-core"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { + // Set up test environment + + const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t); + + // Create a withdrawal operation + + const user = await BankApi.createRandomBankUser(bank); + const wop = await BankAccessApi.createWithdrawalOperation( + bank, + user, + "TESTKUDOS:10", + ); + + // Hand it to the wallet + + const r1 = await wallet.apiRequest("getWithdrawalDetailsForUri", { + talerWithdrawUri: wop.taler_withdraw_uri, + }); + t.assertTrue(r1.type === "response"); + + await wallet.runPending(); + + // Confirm it + + await BankApi.confirmWithdrawalOperation(bank, user, wop); + + // Withdraw + + const r2 = await wallet.apiRequest("acceptBankIntegratedWithdrawal", { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + }); + t.assertTrue(r2.type === "response"); + await wallet.runUntilDone(); + + // Check balance + + const balApiResp = await wallet.apiRequest("getBalances", {}); + t.assertTrue(balApiResp.type === "response"); + const balResp = codecForBalancesResponse().decode(balApiResp.result); + t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts new file mode 100644 index 000000000..ef0db4a19 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts @@ -0,0 +1,78 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * Imports. + */ +import { GlobalTestState, BankApi } from "./harness"; +import { createSimpleTestkudosEnvironment } from "./helpers"; +import { CoreApiResponse } from "taler-wallet-core"; +import { codecForBalancesResponse } from "taler-wallet-core"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +export async function runTestWithdrawalManualTest(t: GlobalTestState) { + // Set up test environment + + const { + wallet, + bank, + exchange, + exchangeBankAccount, + } = await createSimpleTestkudosEnvironment(t); + + // Create a withdrawal operation + + const user = await BankApi.createRandomBankUser(bank); + + let wresp: CoreApiResponse; + + wresp = await wallet.apiRequest("addExchange", { + exchangeBaseUrl: exchange.baseUrl, + }); + + t.assertTrue(wresp.type === "response"); + + wresp = await wallet.apiRequest("acceptManualWithdrawal", { + exchangeBaseUrl: exchange.baseUrl, + amount: "TESTKUDOS:10", + }); + + t.assertTrue(wresp.type === "response"); + + const reservePub: string = (wresp.result as any).reservePub; + + await BankApi.adminAddIncoming(bank, { + exchangeBankAccount, + amount: "TESTKUDOS:10", + debitAccountPayto: user.accountPaytoUri, + reservePub: reservePub, + }); + + await exchange.runWirewatchOnce(); + + await wallet.runUntilDone(); + + // Check balance + + const balApiResp = await wallet.apiRequest("getBalances", {}); + t.assertTrue(balApiResp.type === "response"); + const balResp = codecForBalancesResponse().decode(balApiResp.result); + t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); + + await t.shutdown(); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts new file mode 100644 index 000000000..d9804562e --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts @@ -0,0 +1,176 @@ +/* + 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 + */ + +import { GlobalTestState, runTestWithState, TestRunResult } from "./harness"; +import { runPaymentTest } from "./test-payment"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { runBankApiTest } from "./test-bank-api"; +import { runClaimLoopTest } from "./test-claim-loop"; +import { runExchangeManagementTest } from "./test-exchange-management"; +import { runFeeRegressionTest } from "./test-fee-regression"; +import { runMerchantLongpollingTest } from "./test-merchant-longpolling"; +import { runMerchantRefundApiTest } from "./test-merchant-refund-api"; +import { runPayAbortTest } from "./test-pay-abort"; +import { runPayPaidTest } from "./test-pay-paid"; +import { runPaymentClaimTest } from "./test-payment-claim"; +import { runPaymentFaultTest } from "./test-payment-fault"; +import { runPaymentIdempotencyTest } from "./test-payment-idempotency"; +import { runPaymentMultipleTest } from "./test-payment-multiple"; +import { runPaymentTransientTest } from "./test-payment-transient"; +import { runPaywallFlowTest } from "./test-paywall-flow"; +import { runRefundAutoTest } from "./test-refund-auto"; +import { runRefundGoneTest } from "./test-refund-gone"; +import { runRefundIncrementalTest } from "./test-refund-incremental"; +import { runRefundTest } from "./test-refund"; +import { runRevocationTest } from "./test-revocation"; +import { runTimetravelAutorefreshTest } from "./test-timetravel-autorefresh"; +import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw"; +import { runTippingTest } from "./test-tipping"; +import { runWallettestingTest } from "./test-wallettesting"; +import { runTestWithdrawalManualTest } from "./test-withdrawal-manual"; +import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank"; +import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated"; +import M from "minimatch"; + +/** + * Test runner. + */ + +/** + * Spec for one test. + */ +interface TestMainFunction { + (t: GlobalTestState): Promise; +} + +const allTests: TestMainFunction[] = [ + runBankApiTest, + runClaimLoopTest, + runExchangeManagementTest, + runFeeRegressionTest, + runMerchantLongpollingTest, + runMerchantRefundApiTest, + runPayAbortTest, + runPayPaidTest, + runPaymentClaimTest, + runPaymentFaultTest, + runPaymentIdempotencyTest, + runPaymentMultipleTest, + runPaymentTransientTest, + runPaywallFlowTest, + runRefundAutoTest, + runRefundGoneTest, + runRefundIncrementalTest, + runRefundTest, + runRevocationTest, + runTimetravelAutorefreshTest, + runTimetravelWithdrawTest, + runTippingTest, + runWallettestingTest, + runWithdrawalAbortBankTest, + runWithdrawalBankIntegratedTest, + runWallettestingTest, + runPaymentTest, +]; + +export interface TestRunSpec { + include_pattern?: string; +} + +export interface TestInfo { + name: string; +} + +function updateCurrentSymlink(testDir: string): void { + const currLink = path.join(os.tmpdir(), "taler-integrationtests-current"); + try { + fs.unlinkSync(currLink); + } catch (e) { + // Ignore + } + try { + fs.symlinkSync(testDir, currLink); + } catch (e) { + console.log(e); + // Ignore + } +} + +export function getTestName(tf: TestMainFunction): string { + const res = tf.name.match(/run([a-zA-Z0-9]*)Test/); + if (!res) { + throw Error("invalid test name, must be 'run${NAME}Test'"); + } + return res[1] + .replace(/[a-z0-9][A-Z]/, (x) => { + return x[0] + "-" + x[1]; + }) + .toLowerCase(); +} + +export async function runTests(spec: TestRunSpec) { + const testRootDir = fs.mkdtempSync( + path.join(os.tmpdir(), "taler-integrationtests-"), + ); + updateCurrentSymlink(testRootDir); + console.log("testsuite root directory: ", testRootDir); + + let numTotal = 0; + let numFail = 0; + let numSkip = 0; + let numPass = 0; + + const testResults: TestRunResult[] = []; + + for (const [n, testCase] of allTests.entries()) { + const testName = getTestName(testCase); + if (spec.include_pattern && !M(testName, spec.include_pattern)) { + continue; + } + const testDir = path.join(testRootDir, testName); + fs.mkdirSync(testDir); + console.log(`running test ${testName}`); + const gc = new GlobalTestState({ + testDir, + }); + const result = await runTestWithState(gc, testCase, testName); + testResults.push(result); + console.log(result); + numTotal++; + if (result.status === "fail") { + numFail++; + } else if (result.status === "skip") { + numSkip++; + } else if (result.status === "pass") { + numPass++; + } + } + const resultsFile = path.join(testRootDir, "results.json"); + fs.writeFileSync( + path.join(testRootDir, "results.json"), + JSON.stringify({ testResults }, undefined, 2), + ); + console.log(`See ${resultsFile} for details`); + console.log(`Passed: ${numPass}/${numTotal}`); +} + +export function getTestInfo(): TestInfo[] { + return allTests.map((x) => ({ + name: getTestName(x), + })); +} -- cgit v1.2.3