2020-08-05 21:00:36 +02:00
|
|
|
/*
|
|
|
|
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 <http://www.gnu.org/licenses/>
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Test harness for various GNU Taler components.
|
|
|
|
* Also provides a fault-injection proxy.
|
|
|
|
*
|
|
|
|
* @author Florian Dold <dold@taler.net>
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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";
|
2020-08-14 12:23:50 +02:00
|
|
|
import { deepStrictEqual } from "assert";
|
2020-08-05 21:00:36 +02:00
|
|
|
import { ChildProcess, spawn } from "child_process";
|
|
|
|
import {
|
|
|
|
Configuration,
|
|
|
|
AmountJson,
|
|
|
|
Amounts,
|
2020-08-12 09:11:00 +02:00
|
|
|
Codec,
|
2020-08-12 12:32:58 +02:00
|
|
|
buildCodecForObject,
|
2020-08-12 09:11:00 +02:00
|
|
|
codecForString,
|
|
|
|
Duration,
|
|
|
|
CoreApiResponse,
|
2020-08-12 12:18:02 +02:00
|
|
|
PreparePayResult,
|
|
|
|
PreparePayRequest,
|
2020-08-12 13:02:07 +02:00
|
|
|
codecForPreparePayResultPaymentPossible,
|
|
|
|
codecForPreparePayResult,
|
|
|
|
OperationFailedError,
|
2020-08-12 16:15:34 +02:00
|
|
|
AddExchangeRequest,
|
|
|
|
ExchangesListRespose,
|
|
|
|
codecForExchangesListResponse,
|
|
|
|
GetWithdrawalDetailsForUriRequest,
|
|
|
|
WithdrawUriInfoResponse,
|
|
|
|
codecForWithdrawUriInfoResponse,
|
2020-08-12 20:56:55 +02:00
|
|
|
ConfirmPayRequest,
|
|
|
|
ConfirmPayResult,
|
|
|
|
codecForConfirmPayResult,
|
2020-08-14 12:23:50 +02:00
|
|
|
IntegrationTestArgs,
|
|
|
|
TestPayArgs,
|
|
|
|
BalancesResponse,
|
|
|
|
codecForBalancesResponse,
|
|
|
|
encodeCrock,
|
|
|
|
getRandomBytes,
|
|
|
|
EddsaKeyPair,
|
|
|
|
eddsaGetPublic,
|
|
|
|
createEddsaKeyPair,
|
|
|
|
TransactionsResponse,
|
|
|
|
codecForTransactionsResponse,
|
|
|
|
WithdrawTestBalanceRequest,
|
2020-08-20 10:25:03 +02:00
|
|
|
AmountString,
|
2020-08-05 21:00:36 +02:00
|
|
|
} from "taler-wallet-core";
|
|
|
|
import { URL } from "url";
|
2020-08-20 10:25:03 +02:00
|
|
|
import axios, { AxiosError } from "axios";
|
2020-08-06 12:22:35 +02:00
|
|
|
import {
|
|
|
|
codecForMerchantOrderPrivateStatusResponse,
|
|
|
|
codecForPostOrderResponse,
|
|
|
|
PostOrderRequest,
|
|
|
|
PostOrderResponse,
|
2020-08-12 20:56:55 +02:00
|
|
|
MerchantOrderPrivateStatusResponse,
|
2020-08-06 12:22:35 +02:00
|
|
|
} from "./merchantApiTypes";
|
2020-08-05 21:00:36 +02:00
|
|
|
|
|
|
|
const exec = util.promisify(require("child_process").exec);
|
|
|
|
|
2020-08-10 13:18:38 +02:00
|
|
|
export async function delayMs(ms: number): Promise<void> {
|
2020-08-05 21:00:36 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
setTimeout(() => resolve(), ms);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
interface WaitResult {
|
|
|
|
code: number | null;
|
|
|
|
signal: NodeJS.Signals | null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Run a shell command, return stdout.
|
|
|
|
*/
|
2020-08-06 17:50:47 +02:00
|
|
|
export async function sh(
|
|
|
|
t: GlobalTestState,
|
|
|
|
logName: string,
|
|
|
|
command: string,
|
|
|
|
): Promise<string> {
|
2020-08-08 18:57:26 +02:00
|
|
|
console.log("runing command", command);
|
2020-08-05 21:00:36 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const stdoutChunks: Buffer[] = [];
|
|
|
|
const proc = spawn(command, {
|
2020-08-06 17:50:47 +02:00
|
|
|
stdio: ["inherit", "pipe", "pipe"],
|
2020-08-05 21:00:36 +02:00
|
|
|
shell: true,
|
|
|
|
});
|
|
|
|
proc.stdout.on("data", (x) => {
|
|
|
|
if (x instanceof Buffer) {
|
|
|
|
stdoutChunks.push(x);
|
|
|
|
} else {
|
|
|
|
throw Error("unexpected data chunk type");
|
|
|
|
}
|
|
|
|
});
|
2020-08-06 17:50:47 +02:00
|
|
|
const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`);
|
|
|
|
const stderrLog = fs.createWriteStream(stderrLogFileName, {
|
|
|
|
flags: "a",
|
|
|
|
});
|
|
|
|
proc.stderr.pipe(stderrLog);
|
2020-08-08 18:57:26 +02:00
|
|
|
proc.on("exit", (code, signal) => {
|
|
|
|
console.log(`child process exited (${code} / ${signal})`);
|
2020-08-05 21:00:36 +02:00
|
|
|
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"));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-09-01 14:30:46 +02:00
|
|
|
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<string> {
|
|
|
|
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"));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
export class ProcessWrapper {
|
|
|
|
private waitPromise: Promise<WaitResult>;
|
|
|
|
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<WaitResult> {
|
|
|
|
return this.waitPromise;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
|
2020-08-06 12:22:35 +02:00
|
|
|
export const coin_ct1 = (curr: string): CoinConfig => ({
|
2020-08-05 21:00:36 +02:00
|
|
|
...coinCommon,
|
|
|
|
name: `${curr}_ct1`,
|
|
|
|
value: `${curr}:0.01`,
|
|
|
|
feeDeposit: `${curr}:0.00`,
|
|
|
|
feeRefresh: `${curr}:0.01`,
|
|
|
|
feeRefund: `${curr}:0.00`,
|
|
|
|
feeWithdraw: `${curr}:0.01`,
|
|
|
|
});
|
|
|
|
|
2020-08-06 12:22:35 +02:00
|
|
|
export const coin_ct10 = (curr: string): CoinConfig => ({
|
2020-08-05 21:00:36 +02:00
|
|
|
...coinCommon,
|
|
|
|
name: `${curr}_ct10`,
|
|
|
|
value: `${curr}:0.10`,
|
|
|
|
feeDeposit: `${curr}:0.01`,
|
|
|
|
feeRefresh: `${curr}:0.01`,
|
|
|
|
feeRefund: `${curr}:0.00`,
|
|
|
|
feeWithdraw: `${curr}:0.01`,
|
|
|
|
});
|
|
|
|
|
2020-08-06 12:22:35 +02:00
|
|
|
export const coin_u1 = (curr: string): CoinConfig => ({
|
2020-08-05 21:00:36 +02:00
|
|
|
...coinCommon,
|
|
|
|
name: `${curr}_u1`,
|
|
|
|
value: `${curr}:1`,
|
|
|
|
feeDeposit: `${curr}:0.02`,
|
|
|
|
feeRefresh: `${curr}:0.02`,
|
|
|
|
feeRefund: `${curr}:0.02`,
|
|
|
|
feeWithdraw: `${curr}:0.02`,
|
|
|
|
});
|
|
|
|
|
2020-08-06 12:22:35 +02:00
|
|
|
export const coin_u2 = (curr: string): CoinConfig => ({
|
2020-08-05 21:00:36 +02:00
|
|
|
...coinCommon,
|
|
|
|
name: `${curr}_u2`,
|
|
|
|
value: `${curr}:2`,
|
|
|
|
feeDeposit: `${curr}:0.02`,
|
|
|
|
feeRefresh: `${curr}:0.02`,
|
|
|
|
feeRefund: `${curr}:0.02`,
|
|
|
|
feeWithdraw: `${curr}:0.02`,
|
|
|
|
});
|
|
|
|
|
2020-08-06 12:22:35 +02:00
|
|
|
export const coin_u4 = (curr: string): CoinConfig => ({
|
2020-08-05 21:00:36 +02:00
|
|
|
...coinCommon,
|
|
|
|
name: `${curr}_u4`,
|
|
|
|
value: `${curr}:4`,
|
|
|
|
feeDeposit: `${curr}:0.02`,
|
|
|
|
feeRefresh: `${curr}:0.02`,
|
|
|
|
feeRefund: `${curr}:0.02`,
|
|
|
|
feeWithdraw: `${curr}:0.02`,
|
|
|
|
});
|
|
|
|
|
2020-08-06 12:22:35 +02:00
|
|
|
export const coin_u8 = (curr: string): CoinConfig => ({
|
2020-08-05 21:00:36 +02:00
|
|
|
...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 class GlobalTestParams {
|
|
|
|
testDir: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class GlobalTestState {
|
|
|
|
testDir: string;
|
|
|
|
procs: ProcessWrapper[];
|
|
|
|
servers: http.Server[];
|
2020-08-07 08:39:32 +02:00
|
|
|
inShutdown: boolean = false;
|
2020-08-05 21:00:36 +02:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
2020-08-12 16:15:34 +02:00
|
|
|
async assertThrowsOperationErrorAsync(
|
|
|
|
block: () => Promise<void>,
|
|
|
|
): Promise<OperationFailedError> {
|
|
|
|
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`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-20 10:25:03 +02:00
|
|
|
async assertThrowsAsync(block: () => Promise<void>): Promise<any> {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
assertTrue(b: boolean): asserts b {
|
|
|
|
if (!b) {
|
|
|
|
throw Error("test assertion failed");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-01 14:30:46 +02:00
|
|
|
assertDeepEqual<T>(actual: any, expected: T): asserts actual is T {
|
2020-08-14 12:23:50 +02:00
|
|
|
deepStrictEqual(actual, expected);
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
assertAmountEquals(
|
|
|
|
amtExpected: string | AmountJson,
|
|
|
|
amtActual: string | AmountJson,
|
|
|
|
): void {
|
|
|
|
let ja1: AmountJson;
|
|
|
|
let ja2: AmountJson;
|
|
|
|
if (typeof amtExpected === "string") {
|
|
|
|
ja1 = Amounts.parseOrThrow(amtExpected);
|
|
|
|
} else {
|
|
|
|
ja1 = amtExpected;
|
|
|
|
}
|
|
|
|
if (typeof amtActual === "string") {
|
|
|
|
ja2 = Amounts.parseOrThrow(amtActual);
|
|
|
|
} else {
|
|
|
|
ja2 = amtActual;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Amounts.cmp(ja1, ja2) != 0) {
|
|
|
|
throw Error(
|
|
|
|
`test assertion failed: expected ${Amounts.stringify(
|
|
|
|
ja1,
|
|
|
|
)} but got ${Amounts.stringify(ja2)}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-08-07 09:33:31 +02:00
|
|
|
spawnService(
|
|
|
|
command: string,
|
|
|
|
args: string[],
|
|
|
|
logName: string,
|
|
|
|
): ProcessWrapper {
|
2020-09-01 14:30:46 +02:00
|
|
|
console.log(
|
|
|
|
`spawning process (${logName}): ${shellescape([command, ...args])}`,
|
|
|
|
);
|
2020-08-07 09:33:31 +02:00
|
|
|
const proc = spawn(command, args, {
|
2020-08-05 21:00:36 +02:00
|
|
|
stdio: ["inherit", "pipe", "pipe"],
|
|
|
|
});
|
2020-08-06 14:23:13 +02:00
|
|
|
console.log(`spawned process (${logName}) with pid ${proc.pid}`);
|
2020-08-06 12:22:35 +02:00
|
|
|
proc.on("error", (err) => {
|
|
|
|
console.log(`could not start process (${command})`, err);
|
|
|
|
});
|
|
|
|
proc.on("exit", (code, signal) => {
|
|
|
|
console.log(`process ${logName} exited`);
|
|
|
|
});
|
2020-08-05 21:00:36 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-08-07 08:03:48 +02:00
|
|
|
async shutdown(): Promise<void> {
|
|
|
|
if (this.inShutdown) {
|
|
|
|
return;
|
|
|
|
}
|
2020-08-07 08:39:32 +02:00
|
|
|
this.inShutdown = true;
|
2020-08-07 08:03:48 +02:00
|
|
|
console.log("shutting down");
|
2020-08-07 08:48:17 +02:00
|
|
|
if (shouldLingerAlways()) {
|
|
|
|
console.log("*** test finished, but requested to linger");
|
|
|
|
console.log("*** test state can be found under", this.testDir);
|
|
|
|
return;
|
|
|
|
}
|
2020-08-05 21:00:36 +02:00
|
|
|
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<string, string | undefined>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface TalerConfig {
|
|
|
|
sections: Record<string, TalerConfigSection>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface DbInfo {
|
|
|
|
connStr: string;
|
|
|
|
dbname: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function setupDb(gc: GlobalTestState): Promise<DbInfo> {
|
|
|
|
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;
|
2020-08-20 10:25:03 +02:00
|
|
|
maxDebt?: string;
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
function setPaths(config: Configuration, home: string) {
|
|
|
|
config.setString("paths", "taler_home", home);
|
|
|
|
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}`);
|
|
|
|
}
|
|
|
|
|
2020-08-07 19:36:52 +02:00
|
|
|
async function pingProc(
|
|
|
|
proc: ProcessWrapper | undefined,
|
|
|
|
url: string,
|
|
|
|
serviceName: string,
|
|
|
|
): Promise<void> {
|
|
|
|
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());
|
2020-08-10 13:18:38 +02:00
|
|
|
await delayMs(1000);
|
2020-08-07 19:36:52 +02:00
|
|
|
}
|
|
|
|
if (!proc || proc.proc.exitCode !== null) {
|
|
|
|
throw Error(`service process ${serviceName} stopped unexpectedly`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-08 18:57:26 +02:00
|
|
|
export interface ExchangeBankAccount {
|
|
|
|
accountName: string;
|
|
|
|
accountPassword: string;
|
|
|
|
accountPaytoUri: string;
|
|
|
|
wireGatewayApiBaseUrl: string;
|
|
|
|
}
|
|
|
|
|
2020-08-20 10:25:03 +02:00
|
|
|
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<BankAccountBalanceResponse> {
|
2020-08-24 10:31:03 +02:00
|
|
|
const url = new URL(`accounts/${bankUser.username}/balance`, bank.baseUrl);
|
|
|
|
const resp = await axios.get(url.href, {
|
|
|
|
auth: bankUser,
|
|
|
|
});
|
2020-08-20 10:25:03 +02:00
|
|
|
return resp.data;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function createWithdrawalOperation(
|
|
|
|
bank: BankServiceInterface,
|
|
|
|
bankUser: BankUser,
|
|
|
|
amount: string,
|
|
|
|
): Promise<WithdrawalOperationInfo> {
|
|
|
|
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<BankUser> {
|
|
|
|
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<BankUser> {
|
|
|
|
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<void> {
|
|
|
|
const url = new URL(
|
|
|
|
`accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`,
|
|
|
|
bank.baseUrl,
|
|
|
|
);
|
|
|
|
await axios.post(
|
|
|
|
url.href,
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
auth: bankUser,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2020-08-20 11:04:56 +02:00
|
|
|
|
|
|
|
export async function abortWithdrawalOperation(
|
|
|
|
bank: BankServiceInterface,
|
|
|
|
bankUser: BankUser,
|
|
|
|
wopi: WithdrawalOperationInfo,
|
|
|
|
): Promise<void> {
|
|
|
|
const url = new URL(
|
|
|
|
`accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/abort`,
|
|
|
|
bank.baseUrl,
|
|
|
|
);
|
|
|
|
await axios.post(
|
|
|
|
url.href,
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
auth: bankUser,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
2020-08-20 10:25:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export class BankService implements BankServiceInterface {
|
2020-08-05 21:00:36 +02:00
|
|
|
proc: ProcessWrapper | undefined;
|
2020-08-07 19:36:52 +02:00
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
static async create(
|
|
|
|
gc: GlobalTestState,
|
|
|
|
bc: BankConfig,
|
|
|
|
): Promise<BankService> {
|
|
|
|
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", "max_debt_bank", `${bc.currency}:999999`);
|
2020-08-20 10:25:03 +02:00
|
|
|
config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`);
|
2020-08-05 21:00:36 +02:00
|
|
|
config.setString(
|
|
|
|
"bank",
|
|
|
|
"allow_registrations",
|
|
|
|
bc.allowRegistrations ? "yes" : "no",
|
|
|
|
);
|
|
|
|
const cfgFilename = gc.testDir + "/bank.conf";
|
|
|
|
config.write(cfgFilename);
|
2020-08-08 18:57:26 +02:00
|
|
|
|
|
|
|
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`,
|
|
|
|
);
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
return new BankService(gc, bc, cfgFilename);
|
|
|
|
}
|
|
|
|
|
2020-08-12 16:15:34 +02:00
|
|
|
setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) {
|
2020-08-07 19:36:52 +02:00
|
|
|
const config = Configuration.load(this.configFile);
|
|
|
|
config.setString("bank", "suggested_exchange", e.baseUrl);
|
|
|
|
config.setString("bank", "suggested_exchange_payto", exchangePayto);
|
|
|
|
}
|
|
|
|
|
2020-08-14 12:23:50 +02:00
|
|
|
get baseUrl(): string {
|
|
|
|
return `http://localhost:${this.bankConfig.httpPort}/`;
|
|
|
|
}
|
|
|
|
|
2020-08-08 18:57:26 +02:00
|
|
|
async createExchangeAccount(
|
|
|
|
accountName: string,
|
|
|
|
password: string,
|
|
|
|
): Promise<ExchangeBankAccount> {
|
|
|
|
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}/`,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
get port() {
|
|
|
|
return this.bankConfig.httpPort;
|
|
|
|
}
|
|
|
|
|
|
|
|
private constructor(
|
|
|
|
private globalTestState: GlobalTestState,
|
|
|
|
private bankConfig: BankConfig,
|
|
|
|
private configFile: string,
|
|
|
|
) {}
|
|
|
|
|
|
|
|
async start(): Promise<void> {
|
|
|
|
this.proc = this.globalTestState.spawnService(
|
2020-08-07 09:33:31 +02:00
|
|
|
"taler-bank-manage",
|
|
|
|
["-c", this.configFile, "serve-http"],
|
2020-08-05 21:00:36 +02:00
|
|
|
"bank",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async pingUntilAvailable(): Promise<void> {
|
|
|
|
const url = `http://localhost:${this.bankConfig.httpPort}/config`;
|
2020-08-07 19:36:52 +02:00
|
|
|
await pingProc(this.proc, url, "bank");
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface BankUser {
|
|
|
|
username: string;
|
|
|
|
password: string;
|
2020-08-08 18:57:26 +02:00
|
|
|
accountPaytoUri: string;
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface WithdrawalOperationInfo {
|
|
|
|
withdrawal_id: string;
|
|
|
|
taler_withdraw_uri: string;
|
|
|
|
}
|
|
|
|
|
2020-08-12 09:11:00 +02:00
|
|
|
const codecForWithdrawalOperationInfo = (): Codec<WithdrawalOperationInfo> =>
|
2020-08-12 12:32:58 +02:00
|
|
|
buildCodecForObject<WithdrawalOperationInfo>()
|
|
|
|
.property("withdrawal_id", codecForString())
|
|
|
|
.property("taler_withdraw_uri", codecForString())
|
2020-08-05 21:00:36 +02:00
|
|
|
.build("WithdrawalOperationInfo");
|
|
|
|
|
2020-08-06 12:22:35 +02:00
|
|
|
export const defaultCoinConfig = [
|
|
|
|
coin_ct1,
|
|
|
|
coin_ct10,
|
|
|
|
coin_u1,
|
|
|
|
coin_u10,
|
|
|
|
coin_u2,
|
|
|
|
coin_u4,
|
|
|
|
coin_u8,
|
|
|
|
];
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
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 {
|
2020-08-07 19:36:52 +02:00
|
|
|
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(),
|
|
|
|
};
|
2020-08-08 13:22:45 +02:00
|
|
|
const privFile = config.getPath("exchange", "master_priv_file").required();
|
2020-08-07 19:36:52 +02:00
|
|
|
const eddsaPriv = fs.readFileSync(privFile);
|
|
|
|
const keyPair: EddsaKeyPair = {
|
|
|
|
eddsaPriv,
|
2020-08-12 09:11:00 +02:00
|
|
|
eddsaPub: eddsaGetPublic(eddsaPriv),
|
2020-08-07 19:36:52 +02:00
|
|
|
};
|
|
|
|
return new ExchangeService(gc, ec, cfgFilename, keyPair);
|
|
|
|
}
|
|
|
|
|
2020-08-24 10:31:03 +02:00
|
|
|
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 [];
|
|
|
|
}
|
|
|
|
|
2020-08-08 18:57:26 +02:00
|
|
|
async runWirewatchOnce() {
|
|
|
|
await sh(
|
|
|
|
this.globalState,
|
|
|
|
"wirewatch-test",
|
2020-08-24 10:31:03 +02:00
|
|
|
`taler-exchange-wirewatch ${this.timetravelArg} -c '${this.configFilename}' -t`,
|
2020-08-08 18:57:26 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
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",
|
|
|
|
"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");
|
|
|
|
|
|
|
|
for (let i = 2020; i < 2029; i++) {
|
|
|
|
config.setString(
|
|
|
|
"fees-x-taler-bank",
|
|
|
|
`wire-fee-${i}`,
|
|
|
|
`${e.currency}:0.01`,
|
|
|
|
);
|
|
|
|
config.setString(
|
|
|
|
"fees-x-taler-bank",
|
|
|
|
`closing-fee-${i}`,
|
|
|
|
`${e.currency}:0.01`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
config.setString("exchangedb-postgres", "config", e.database);
|
|
|
|
|
2020-08-12 09:11:00 +02:00
|
|
|
const exchangeMasterKey = createEddsaKeyPair();
|
2020-08-05 21:00:36 +02:00
|
|
|
|
|
|
|
config.setString(
|
|
|
|
"exchange",
|
|
|
|
"master_public_key",
|
2020-08-12 09:11:00 +02:00
|
|
|
encodeCrock(exchangeMasterKey.eddsaPub),
|
2020-08-05 21:00:36 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
const masterPrivFile = config
|
|
|
|
.getPath("exchange", "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);
|
|
|
|
}
|
|
|
|
|
2020-08-07 19:36:52 +02:00
|
|
|
addOfferedCoins(offeredCoins: ((curr: string) => CoinConfig)[]) {
|
|
|
|
const config = Configuration.load(this.configFilename);
|
|
|
|
offeredCoins.forEach((cc) =>
|
|
|
|
setCoin(config, cc(this.exchangeConfig.currency)),
|
|
|
|
);
|
|
|
|
config.write(this.configFilename);
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
get masterPub() {
|
2020-08-12 09:11:00 +02:00
|
|
|
return encodeCrock(this.keyPair.eddsaPub);
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
get port() {
|
|
|
|
return this.exchangeConfig.httpPort;
|
|
|
|
}
|
|
|
|
|
2020-08-08 18:57:26 +02:00
|
|
|
async addBankAccount(
|
2020-08-05 21:00:36 +02:00
|
|
|
localName: string,
|
2020-08-08 18:57:26 +02:00
|
|
|
exchangeBankAccount: ExchangeBankAccount,
|
2020-08-05 21:00:36 +02:00
|
|
|
): Promise<void> {
|
|
|
|
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",
|
2020-08-08 18:57:26 +02:00
|
|
|
exchangeBankAccount.accountPaytoUri,
|
2020-08-05 21:00:36 +02:00
|
|
|
);
|
|
|
|
config.setString(`exchange-account-${localName}`, "enable_credit", "yes");
|
|
|
|
config.setString(`exchange-account-${localName}`, "enable_debit", "yes");
|
|
|
|
config.setString(
|
|
|
|
`exchange-account-${localName}`,
|
|
|
|
"wire_gateway_url",
|
2020-08-08 18:57:26 +02:00
|
|
|
exchangeBankAccount.wireGatewayApiBaseUrl,
|
2020-08-05 21:00:36 +02:00
|
|
|
);
|
|
|
|
config.setString(
|
|
|
|
`exchange-account-${localName}`,
|
|
|
|
"wire_gateway_auth_method",
|
|
|
|
"basic",
|
|
|
|
);
|
2020-08-08 18:57:26 +02:00
|
|
|
config.setString(
|
|
|
|
`exchange-account-${localName}`,
|
|
|
|
"username",
|
|
|
|
exchangeBankAccount.accountName,
|
|
|
|
);
|
|
|
|
config.setString(
|
|
|
|
`exchange-account-${localName}`,
|
|
|
|
"password",
|
|
|
|
exchangeBankAccount.accountPassword,
|
|
|
|
);
|
2020-08-05 21:00:36 +02:00
|
|
|
config.write(this.configFilename);
|
|
|
|
}
|
|
|
|
|
|
|
|
exchangeHttpProc: ProcessWrapper | undefined;
|
|
|
|
exchangeWirewatchProc: ProcessWrapper | undefined;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
private globalState: GlobalTestState,
|
|
|
|
private exchangeConfig: ExchangeConfig,
|
|
|
|
private configFilename: string,
|
2020-08-12 09:11:00 +02:00
|
|
|
private keyPair: EddsaKeyPair,
|
2020-08-05 21:00:36 +02:00
|
|
|
) {}
|
|
|
|
|
|
|
|
get name() {
|
|
|
|
return this.exchangeConfig.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
get baseUrl() {
|
|
|
|
return `http://localhost:${this.exchangeConfig.httpPort}/`;
|
|
|
|
}
|
|
|
|
|
2020-08-24 10:31:03 +02:00
|
|
|
isRunning(): boolean {
|
|
|
|
return !!this.exchangeWirewatchProc || !!this.exchangeHttpProc;
|
|
|
|
}
|
|
|
|
|
|
|
|
async stop(): Promise<void> {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async keyup(): Promise<void> {
|
|
|
|
await sh(
|
|
|
|
this.globalState,
|
|
|
|
"exchange-keyup",
|
|
|
|
`taler-exchange-keyup ${this.timetravelArg} -c "${this.configFilename}"`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
async start(): Promise<void> {
|
2020-08-24 10:31:03 +02:00
|
|
|
if (this.isRunning()) {
|
|
|
|
throw Error("exchange is already running");
|
|
|
|
}
|
|
|
|
await sh(
|
|
|
|
this.globalState,
|
|
|
|
"exchange-dbinit",
|
|
|
|
`taler-exchange-dbinit -c "${this.configFilename}"`,
|
|
|
|
);
|
|
|
|
await this.keyup();
|
|
|
|
await sh(
|
|
|
|
this.globalState,
|
|
|
|
"exchange-wire",
|
2020-09-01 14:30:46 +02:00
|
|
|
`taler-exchange-wire ${this.timetravelArg} -c "${this.configFilename}"`,
|
|
|
|
);
|
2020-08-05 21:00:36 +02:00
|
|
|
|
|
|
|
this.exchangeWirewatchProc = this.globalState.spawnService(
|
2020-08-07 09:33:31 +02:00
|
|
|
"taler-exchange-wirewatch",
|
2020-08-24 10:31:03 +02:00
|
|
|
["-c", this.configFilename, ...this.timetravelArgArr],
|
2020-08-05 21:00:36 +02:00
|
|
|
`exchange-wirewatch-${this.name}`,
|
|
|
|
);
|
|
|
|
|
|
|
|
this.exchangeHttpProc = this.globalState.spawnService(
|
2020-08-07 09:33:31 +02:00
|
|
|
"taler-exchange-httpd",
|
2020-08-24 10:31:03 +02:00
|
|
|
[
|
|
|
|
"-c",
|
|
|
|
this.configFilename,
|
|
|
|
"--num-threads",
|
|
|
|
"1",
|
|
|
|
...this.timetravelArgArr,
|
|
|
|
],
|
2020-08-05 21:00:36 +02:00
|
|
|
`exchange-httpd-${this.name}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async pingUntilAvailable(): Promise<void> {
|
|
|
|
const url = `http://localhost:${this.exchangeConfig.httpPort}/keys`;
|
2020-08-07 19:36:52 +02:00
|
|
|
await pingProc(this.exchangeHttpProc, url, `exchange (${this.name})`);
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface MerchantConfig {
|
|
|
|
name: string;
|
|
|
|
currency: string;
|
|
|
|
httpPort: number;
|
|
|
|
database: string;
|
|
|
|
}
|
|
|
|
|
2020-08-12 20:56:55 +02:00
|
|
|
export interface PrivateOrderStatusQuery {
|
2020-08-14 12:23:50 +02:00
|
|
|
instance?: string;
|
|
|
|
orderId: string;
|
|
|
|
sessionId?: string;
|
2020-08-12 20:56:55 +02:00
|
|
|
}
|
|
|
|
|
2020-08-19 17:26:10 +02:00
|
|
|
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<PostOrderResponse> {
|
|
|
|
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<MerchantOrderPrivateStatusResponse> {
|
|
|
|
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: {
|
2020-08-20 10:25:03 +02:00
|
|
|
instance: string;
|
|
|
|
orderId: string;
|
|
|
|
amount: string;
|
|
|
|
justification: string;
|
|
|
|
},
|
|
|
|
): Promise<{ talerRefundUri: string }> {
|
2020-08-19 17:26:10 +02:00
|
|
|
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 class MerchantService implements MerchantServiceInterface {
|
2020-08-07 19:36:52 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
proc: ProcessWrapper | undefined;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
private globalState: GlobalTestState,
|
|
|
|
private merchantConfig: MerchantConfig,
|
|
|
|
private configFilename: string,
|
|
|
|
) {}
|
|
|
|
|
2020-08-24 10:31:03 +02:00
|
|
|
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 [];
|
|
|
|
}
|
|
|
|
|
2020-08-19 17:26:10 +02:00
|
|
|
get port(): number {
|
|
|
|
return this.merchantConfig.httpPort;
|
|
|
|
}
|
|
|
|
|
|
|
|
get name(): string {
|
|
|
|
return this.merchantConfig.name;
|
|
|
|
}
|
|
|
|
|
2020-08-24 10:31:03 +02:00
|
|
|
async stop(): Promise<void> {
|
|
|
|
const httpd = this.proc;
|
|
|
|
if (httpd) {
|
|
|
|
httpd.proc.kill("SIGTERM");
|
|
|
|
await httpd.wait();
|
|
|
|
this.proc = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
async start(): Promise<void> {
|
|
|
|
await exec(`taler-merchant-dbinit -c "${this.configFilename}"`);
|
|
|
|
|
|
|
|
this.proc = this.globalState.spawnService(
|
2020-08-07 09:33:31 +02:00
|
|
|
"taler-merchant-httpd",
|
2020-08-24 10:31:03 +02:00
|
|
|
["-LINFO", "-c", this.configFilename, ...this.timetravelArgArr],
|
2020-08-05 21:00:36 +02:00
|
|
|
`merchant-${this.merchantConfig.name}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static async create(
|
|
|
|
gc: GlobalTestState,
|
|
|
|
mc: MerchantConfig,
|
|
|
|
): Promise<MerchantService> {
|
|
|
|
const config = new Configuration();
|
|
|
|
config.setString("taler", "currency", mc.currency);
|
|
|
|
|
2020-08-06 12:22:35 +02:00
|
|
|
const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`;
|
|
|
|
setPaths(config, gc.testDir + "/talerhome");
|
2020-08-05 21:00:36 +02:00
|
|
|
config.setString("merchant", "serve", "tcp");
|
|
|
|
config.setString("merchant", "port", `${mc.httpPort}`);
|
2020-08-06 12:22:35 +02:00
|
|
|
config.setString(
|
|
|
|
"merchant",
|
|
|
|
"keyfile",
|
|
|
|
"${TALER_DATA_HOME}/merchant/merchant.priv",
|
|
|
|
);
|
2020-08-06 13:46:52 +02:00
|
|
|
config.setString("merchantdb-postgres", "config", mc.database);
|
2020-08-05 21:00:36 +02:00
|
|
|
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<void> {
|
|
|
|
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" },
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-08-12 20:56:55 +02:00
|
|
|
makeInstanceBaseUrl(instanceName?: string): string {
|
|
|
|
if (instanceName === undefined || instanceName === "default") {
|
2020-08-10 13:18:38 +02:00
|
|
|
return `http://localhost:${this.merchantConfig.httpPort}/`;
|
2020-08-05 21:00:36 +02:00
|
|
|
} else {
|
2020-08-10 13:18:38 +02:00
|
|
|
return `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
async pingUntilAvailable(): Promise<void> {
|
|
|
|
const url = `http://localhost:${this.merchantConfig.httpPort}/config`;
|
2020-08-07 19:36:52 +02:00
|
|
|
await pingProc(this.proc, url, `merchant (${this.merchantConfig.name})`);
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface MerchantInstanceConfig {
|
|
|
|
id: string;
|
|
|
|
name: string;
|
|
|
|
paytoUris: string[];
|
|
|
|
address?: unknown;
|
|
|
|
jurisdiction?: unknown;
|
|
|
|
defaultMaxWireFee?: string;
|
|
|
|
defaultMaxDepositFee?: string;
|
|
|
|
defaultWireFeeAmortization?: number;
|
2020-08-12 09:11:00 +02:00
|
|
|
defaultWireTransferDelay?: Duration;
|
|
|
|
defaultPayDelay?: Duration;
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
|
2020-08-07 08:43:35 +02:00
|
|
|
/**
|
|
|
|
* Check if the test should hang around after it failed.
|
|
|
|
*/
|
2020-08-06 14:46:04 +02:00
|
|
|
function shouldLinger(): boolean {
|
2020-08-07 08:44:17 +02:00
|
|
|
return (
|
|
|
|
process.env["TALER_TEST_LINGER"] == "1" ||
|
|
|
|
process.env["TALER_TEST_LINGER_ALWAYS"] == "1"
|
|
|
|
);
|
2020-08-07 08:43:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the test should hang around even after it finished
|
|
|
|
* successfully.
|
|
|
|
*/
|
|
|
|
function shouldLingerAlways(): boolean {
|
|
|
|
return process.env["TALER_TEST_LINGER_ALWAYS"] == "1";
|
2020-08-06 14:46:04 +02:00
|
|
|
}
|
|
|
|
|
2020-08-06 17:50:47 +02:00
|
|
|
function updateCurrentSymlink(testDir: string): void {
|
|
|
|
const currLink = path.join(os.tmpdir(), "taler-integrationtest-current");
|
|
|
|
try {
|
|
|
|
fs.unlinkSync(currLink);
|
|
|
|
} catch (e) {
|
|
|
|
// Ignore
|
|
|
|
}
|
|
|
|
try {
|
2020-08-07 08:24:47 +02:00
|
|
|
fs.symlinkSync(testDir, currLink);
|
2020-08-06 17:50:47 +02:00
|
|
|
} catch (e) {
|
2020-08-07 08:24:47 +02:00
|
|
|
console.log(e);
|
2020-08-06 17:50:47 +02:00
|
|
|
// Ignore
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-07 19:36:52 +02:00
|
|
|
export function runTestWithState(
|
|
|
|
gc: GlobalTestState,
|
|
|
|
testMain: (t: GlobalTestState) => Promise<void>,
|
|
|
|
) {
|
2020-08-05 21:00:36 +02:00
|
|
|
const main = async () => {
|
2020-08-06 14:46:04 +02:00
|
|
|
let ret = 0;
|
2020-08-05 21:00:36 +02:00
|
|
|
try {
|
2020-08-06 17:50:47 +02:00
|
|
|
updateCurrentSymlink(gc.testDir);
|
2020-08-07 09:54:55 +02:00
|
|
|
console.log("running test in directory", gc.testDir);
|
2020-08-05 21:00:36 +02:00
|
|
|
await testMain(gc);
|
2020-08-06 14:46:04 +02:00
|
|
|
} catch (e) {
|
|
|
|
console.error("FATAL: test failed with exception", e);
|
|
|
|
ret = 1;
|
2020-08-05 21:00:36 +02:00
|
|
|
} finally {
|
2020-08-06 14:46:04 +02:00
|
|
|
if (gc) {
|
|
|
|
if (shouldLinger()) {
|
|
|
|
console.log("test logs and config can be found under", gc.testDir);
|
|
|
|
console.log("keeping test environment running");
|
|
|
|
} else {
|
2020-08-07 08:03:48 +02:00
|
|
|
await gc.shutdown();
|
2020-08-06 14:46:04 +02:00
|
|
|
console.log("test logs and config can be found under", gc.testDir);
|
|
|
|
process.exit(ret);
|
|
|
|
}
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-08-06 14:46:04 +02:00
|
|
|
main();
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
|
2020-08-07 19:36:52 +02:00
|
|
|
export function runTest(
|
|
|
|
testMain: (gc: GlobalTestState) => Promise<void>,
|
|
|
|
): void {
|
|
|
|
const gc = new GlobalTestState({
|
|
|
|
testDir: fs.mkdtempSync(path.join(os.tmpdir(), "taler-integrationtest-")),
|
|
|
|
});
|
|
|
|
runTestWithState(gc, testMain);
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
function shellWrap(s: string) {
|
|
|
|
return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'";
|
|
|
|
}
|
|
|
|
|
|
|
|
export class WalletCli {
|
2020-08-24 10:31:03 +02:00
|
|
|
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",
|
|
|
|
) {}
|
2020-08-24 08:22:12 +02:00
|
|
|
|
|
|
|
get dbfile(): string {
|
|
|
|
return this.globalTestState.testDir + `/walletdb-${this.name}.json`;
|
|
|
|
}
|
2020-08-05 21:00:36 +02:00
|
|
|
|
2020-08-08 13:22:45 +02:00
|
|
|
deleteDatabase() {
|
2020-08-24 08:22:12 +02:00
|
|
|
fs.unlinkSync(this.dbfile);
|
2020-08-08 13:22:45 +02:00
|
|
|
}
|
|
|
|
|
2020-09-01 14:30:46 +02:00
|
|
|
private get timetravelArgArr(): string[] {
|
|
|
|
const tta = this.timetravelArg;
|
|
|
|
if (tta) {
|
|
|
|
return [tta];
|
|
|
|
}
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2020-08-05 21:00:36 +02:00
|
|
|
async apiRequest(
|
|
|
|
request: string,
|
2020-08-12 13:02:07 +02:00
|
|
|
payload: unknown,
|
2020-08-12 09:11:00 +02:00
|
|
|
): Promise<CoreApiResponse> {
|
2020-08-05 21:00:36 +02:00
|
|
|
const resp = await sh(
|
2020-08-06 17:50:47 +02:00
|
|
|
this.globalTestState,
|
2020-08-24 08:22:12 +02:00
|
|
|
`wallet-${this.name}`,
|
2020-08-24 10:31:03 +02:00
|
|
|
`taler-wallet-cli ${
|
|
|
|
this.timetravelArg ?? ""
|
|
|
|
} --no-throttle --wallet-db '${this.dbfile}' api '${request}' ${shellWrap(
|
2020-08-05 21:00:36 +02:00
|
|
|
JSON.stringify(payload),
|
|
|
|
)}`,
|
|
|
|
);
|
|
|
|
console.log(resp);
|
2020-08-12 09:11:00 +02:00
|
|
|
return JSON.parse(resp) as CoreApiResponse;
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
|
2020-09-01 14:30:46 +02:00
|
|
|
async runUntilDone(args: { maxRetries?: number } = {}): Promise<void> {
|
|
|
|
await runCommand(
|
2020-08-06 17:50:47 +02:00
|
|
|
this.globalTestState,
|
2020-08-24 08:22:12 +02:00
|
|
|
`wallet-${this.name}`,
|
2020-09-01 14:30:46 +02:00
|
|
|
"taler-wallet-cli",
|
|
|
|
[
|
|
|
|
"--no-throttle",
|
|
|
|
...this.timetravelArgArr,
|
|
|
|
"--wallet-db",
|
|
|
|
this.dbfile,
|
|
|
|
"run-until-done",
|
|
|
|
...(args.maxRetries ? ["--max-retries", `${args.maxRetries}`] : []),
|
|
|
|
],
|
2020-08-06 12:22:35 +02:00
|
|
|
);
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async runPending(): Promise<void> {
|
2020-08-06 17:50:47 +02:00
|
|
|
await sh(
|
|
|
|
this.globalTestState,
|
2020-08-24 08:22:12 +02:00
|
|
|
`wallet-${this.name}`,
|
2020-08-24 10:31:03 +02:00
|
|
|
`taler-wallet-cli ${this.timetravelArg ?? ""} --no-throttle --wallet-db ${
|
|
|
|
this.dbfile
|
|
|
|
} run-pending`,
|
2020-08-06 17:50:47 +02:00
|
|
|
);
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|
2020-08-12 12:18:02 +02:00
|
|
|
|
|
|
|
async preparePay(req: PreparePayRequest): Promise<PreparePayResult> {
|
2020-08-12 13:02:07 +02:00
|
|
|
const resp = await this.apiRequest("preparePay", req);
|
|
|
|
if (resp.type === "response") {
|
|
|
|
return codecForPreparePayResult().decode(resp.result);
|
|
|
|
}
|
|
|
|
throw new OperationFailedError(resp.error);
|
2020-08-12 12:18:02 +02:00
|
|
|
}
|
2020-08-12 16:15:34 +02:00
|
|
|
|
2020-08-12 20:56:55 +02:00
|
|
|
async confirmPay(req: ConfirmPayRequest): Promise<ConfirmPayResult> {
|
|
|
|
const resp = await this.apiRequest("confirmPay", req);
|
|
|
|
if (resp.type === "response") {
|
|
|
|
return codecForConfirmPayResult().decode(resp.result);
|
|
|
|
}
|
|
|
|
throw new OperationFailedError(resp.error);
|
|
|
|
}
|
|
|
|
|
2020-08-12 16:15:34 +02:00
|
|
|
async addExchange(req: AddExchangeRequest): Promise<void> {
|
|
|
|
const resp = await this.apiRequest("addExchange", req);
|
|
|
|
if (resp.type === "response") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new OperationFailedError(resp.error);
|
|
|
|
}
|
|
|
|
|
|
|
|
async listExchanges(): Promise<ExchangesListRespose> {
|
|
|
|
const resp = await this.apiRequest("listExchanges", {});
|
|
|
|
if (resp.type === "response") {
|
|
|
|
return codecForExchangesListResponse().decode(resp.result);
|
|
|
|
}
|
|
|
|
throw new OperationFailedError(resp.error);
|
|
|
|
}
|
|
|
|
|
2020-08-14 12:23:50 +02:00
|
|
|
async getBalances(): Promise<BalancesResponse> {
|
|
|
|
const resp = await this.apiRequest("getBalances", {});
|
|
|
|
if (resp.type === "response") {
|
|
|
|
return codecForBalancesResponse().decode(resp.result);
|
|
|
|
}
|
|
|
|
throw new OperationFailedError(resp.error);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getTransactions(): Promise<TransactionsResponse> {
|
|
|
|
const resp = await this.apiRequest("getTransactions", {});
|
|
|
|
if (resp.type === "response") {
|
|
|
|
return codecForTransactionsResponse().decode(resp.result);
|
|
|
|
}
|
|
|
|
throw new OperationFailedError(resp.error);
|
|
|
|
}
|
|
|
|
|
2020-08-14 12:48:48 +02:00
|
|
|
async runIntegrationTest(args: IntegrationTestArgs): Promise<void> {
|
|
|
|
const resp = await this.apiRequest("runIntegrationTest", args);
|
2020-08-14 12:23:50 +02:00
|
|
|
if (resp.type === "response") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new OperationFailedError(resp.error);
|
|
|
|
}
|
|
|
|
|
|
|
|
async testPay(args: TestPayArgs): Promise<void> {
|
|
|
|
const resp = await this.apiRequest("testPay", args);
|
|
|
|
if (resp.type === "response") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new OperationFailedError(resp.error);
|
|
|
|
}
|
|
|
|
|
|
|
|
async withdrawTestBalance(args: WithdrawTestBalanceRequest): Promise<void> {
|
|
|
|
const resp = await this.apiRequest("withdrawTestBalance", args);
|
|
|
|
if (resp.type === "response") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new OperationFailedError(resp.error);
|
|
|
|
}
|
|
|
|
|
2020-08-12 16:15:34 +02:00
|
|
|
async getWithdrawalDetailsForUri(
|
|
|
|
req: GetWithdrawalDetailsForUriRequest,
|
|
|
|
): Promise<WithdrawUriInfoResponse> {
|
|
|
|
const resp = await this.apiRequest("getWithdrawalDetailsForUri", req);
|
|
|
|
if (resp.type === "response") {
|
|
|
|
return codecForWithdrawUriInfoResponse().decode(resp.result);
|
|
|
|
}
|
2020-08-14 12:23:50 +02:00
|
|
|
throw new OperationFailedError(resp.error);
|
2020-08-12 16:15:34 +02:00
|
|
|
}
|
2020-08-05 21:00:36 +02:00
|
|
|
}
|