towards integration tests with fault injection
This commit is contained in:
parent
a8f03d3dd1
commit
82a2437c09
4
.gitignore
vendored
4
.gitignore
vendored
@ -7,7 +7,8 @@ tsconfig.tsbuildinfo
|
||||
|
||||
# GNU-style build system
|
||||
/configure
|
||||
build-system/config.mk
|
||||
/build-system/config.mk
|
||||
/Makefile
|
||||
|
||||
# Editor files
|
||||
\#*\#
|
||||
@ -20,3 +21,4 @@ build-scripts/
|
||||
|
||||
# Git worktree of pre-built wallet files
|
||||
prebuilt/
|
||||
|
||||
|
43
packages/taler-integrationtests/package.json
Normal file
43
packages/taler-integrationtests/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "taler-integrationtests",
|
||||
"version": "0.0.1",
|
||||
"description": "Integration tests and fault injection for GNU Taler components",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"compile": "tsc",
|
||||
"test": "tsc && ava"
|
||||
},
|
||||
"author": "Florian Dold <dold@taler.net>",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@ava/typescript": "^1.1.1",
|
||||
"ava": "^3.11.1",
|
||||
"esm": "^3.2.25",
|
||||
"source-map-support": "^0.5.19",
|
||||
"ts-node": "^8.10.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"taler-wallet-core": "workspace:*",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"ava": {
|
||||
"require": [
|
||||
"esm"
|
||||
],
|
||||
"files": [
|
||||
"src/**/test-*"
|
||||
],
|
||||
"typescript": {
|
||||
"extensions": [
|
||||
"js",
|
||||
"ts",
|
||||
"tsx"
|
||||
],
|
||||
"rewritePaths": {
|
||||
"src/": "lib/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
222
packages/taler-integrationtests/src/faultInjection.ts
Normal file
222
packages/taler-integrationtests/src/faultInjection.ts
Normal file
@ -0,0 +1,222 @@
|
||||
/*
|
||||
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/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fault injection proxy.
|
||||
*
|
||||
* @author Florian Dold <dold@taler.net>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports
|
||||
*/
|
||||
import * as http from "http";
|
||||
import { URL } from "url";
|
||||
import {
|
||||
GlobalTestState,
|
||||
ExchangeService,
|
||||
BankService,
|
||||
ExchangeServiceInterface,
|
||||
} from "./harness";
|
||||
|
||||
export interface FaultProxyConfig {
|
||||
inboundPort: number;
|
||||
targetPort: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fault injection context. Modified by fault injection functions.
|
||||
*/
|
||||
export interface FaultInjectionRequestContext {
|
||||
requestUrl: string;
|
||||
method: string;
|
||||
requestHeaders: Record<string, string | string[] | undefined>;
|
||||
requestBody?: Buffer;
|
||||
dropRequest: boolean;
|
||||
}
|
||||
|
||||
export interface FaultInjectionResponseContext {
|
||||
request: FaultInjectionRequestContext;
|
||||
statusCode: number;
|
||||
responseHeaders: Record<string, string | string[] | undefined>;
|
||||
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://locahost:${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);
|
||||
}
|
||||
|
||||
clearFault() {
|
||||
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;
|
||||
}
|
||||
}
|
907
packages/taler-integrationtests/src/harness.ts
Normal file
907
packages/taler-integrationtests/src/harness.ts
Normal file
@ -0,0 +1,907 @@
|
||||
/*
|
||||
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";
|
||||
import { ChildProcess, spawn } from "child_process";
|
||||
import {
|
||||
Configuration,
|
||||
walletCoreApi,
|
||||
codec,
|
||||
AmountJson,
|
||||
Amounts,
|
||||
} from "taler-wallet-core";
|
||||
import { URL } from "url";
|
||||
import axios from "axios";
|
||||
import { talerCrypto, time } from "taler-wallet-core";
|
||||
import { codecForMerchantOrderPrivateStatusResponse, codecForPostOrderResponse, PostOrderRequest, PostOrderResponse } from "./merchantApiTypes";
|
||||
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
|
||||
async function delay(ms: number): Promise<void> {
|
||||
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(command: string): Promise<string> {
|
||||
console.log("runing command");
|
||||
console.log(command);
|
||||
return new Promise((resolve, reject) => {
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
const proc = spawn(command, {
|
||||
stdio: ["inherit", "pipe", "inherit"],
|
||||
shell: true,
|
||||
});
|
||||
proc.stdout.on("data", (x) => {
|
||||
console.log("child process got data chunk");
|
||||
if (x instanceof Buffer) {
|
||||
stdoutChunks.push(x);
|
||||
} else {
|
||||
throw Error("unexpected data chunk type");
|
||||
}
|
||||
});
|
||||
proc.on("exit", (code) => {
|
||||
console.log("child process exited");
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
export function makeTempDir(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.mkdtemp(
|
||||
path.join(os.tmpdir(), "taler-integrationtest-"),
|
||||
(err, directory) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(directory);
|
||||
console.log(directory);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
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`,
|
||||
});
|
||||
|
||||
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`,
|
||||
});
|
||||
|
||||
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`,
|
||||
});
|
||||
|
||||
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`,
|
||||
});
|
||||
|
||||
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`,
|
||||
});
|
||||
|
||||
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 class GlobalTestParams {
|
||||
testDir: string;
|
||||
}
|
||||
|
||||
export class GlobalTestState {
|
||||
testDir: string;
|
||||
procs: ProcessWrapper[];
|
||||
servers: http.Server[];
|
||||
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());
|
||||
}
|
||||
|
||||
assertTrue(b: boolean): asserts b {
|
||||
if (!b) {
|
||||
throw Error("test assertion failed");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
spawnService(command: string, logName: string): ProcessWrapper {
|
||||
const proc = spawn(command, {
|
||||
shell: true,
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
});
|
||||
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 terminate(): Promise<void> {
|
||||
console.log("terminating");
|
||||
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;
|
||||
suggestedExchange: string | undefined;
|
||||
suggestedExchangePayto: string | undefined;
|
||||
allowRegistrations: boolean;
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
|
||||
export class BankService {
|
||||
proc: ProcessWrapper | undefined;
|
||||
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`);
|
||||
config.setString(
|
||||
"bank",
|
||||
"allow_registrations",
|
||||
bc.allowRegistrations ? "yes" : "no",
|
||||
);
|
||||
if (bc.suggestedExchange) {
|
||||
config.setString("bank", "suggested_exchange", bc.suggestedExchange);
|
||||
}
|
||||
if (bc.suggestedExchangePayto) {
|
||||
config.setString(
|
||||
"bank",
|
||||
"suggested_exchange_payto",
|
||||
bc.suggestedExchangePayto,
|
||||
);
|
||||
}
|
||||
const cfgFilename = gc.testDir + "/bank.conf";
|
||||
config.write(cfgFilename);
|
||||
return new BankService(gc, bc, cfgFilename);
|
||||
}
|
||||
|
||||
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(
|
||||
`taler-bank-manage -c "${this.configFile}" serve-http`,
|
||||
"bank",
|
||||
);
|
||||
}
|
||||
|
||||
async pingUntilAvailable(): Promise<void> {
|
||||
const url = `http://localhost:${this.bankConfig.httpPort}/config`;
|
||||
while (true) {
|
||||
try {
|
||||
console.log("pinging bank");
|
||||
const resp = await axios.get(url);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.log("bank not ready:", e.toString());
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createAccount(username: string, password: string): Promise<void> {
|
||||
const url = `http://localhost:${this.bankConfig.httpPort}/testing/register`;
|
||||
await axios.post(url, {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
async createRandomBankUser(): Promise<BankUser> {
|
||||
const bankUser: BankUser = {
|
||||
username:
|
||||
"user-" + talerCrypto.encodeCrock(talerCrypto.getRandomBytes(10)),
|
||||
password: "pw-" + talerCrypto.encodeCrock(talerCrypto.getRandomBytes(10)),
|
||||
};
|
||||
await this.createAccount(bankUser.username, bankUser.password);
|
||||
return bankUser;
|
||||
}
|
||||
|
||||
async createWithdrawalOperation(
|
||||
bankUser: BankUser,
|
||||
amount: string,
|
||||
): Promise<WithdrawalOperationInfo> {
|
||||
const url = `http://localhost:${this.bankConfig.httpPort}/accounts/${bankUser.username}/withdrawals`;
|
||||
const resp = await axios.post(
|
||||
url,
|
||||
{
|
||||
amount,
|
||||
},
|
||||
{
|
||||
auth: bankUser,
|
||||
},
|
||||
);
|
||||
return codecForWithdrawalOperationInfo().decode(resp.data);
|
||||
}
|
||||
|
||||
async confirmWithdrawalOperation(
|
||||
bankUser: BankUser,
|
||||
wopi: WithdrawalOperationInfo,
|
||||
): Promise<void> {
|
||||
const url = `http://localhost:${this.bankConfig.httpPort}/accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`;
|
||||
await axios.post(
|
||||
url,
|
||||
{},
|
||||
{
|
||||
auth: bankUser,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface BankUser {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface WithdrawalOperationInfo {
|
||||
withdrawal_id: string;
|
||||
taler_withdraw_uri: string;
|
||||
}
|
||||
|
||||
const codecForWithdrawalOperationInfo = (): codec.Codec<
|
||||
WithdrawalOperationInfo
|
||||
> =>
|
||||
codec
|
||||
.makeCodecForObject<WithdrawalOperationInfo>()
|
||||
.property("withdrawal_id", codec.codecForString)
|
||||
.property("taler_withdraw_uri", codec.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 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", "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);
|
||||
|
||||
setCoin(config, coin_ct1(e.currency));
|
||||
setCoin(config, coin_ct10(e.currency));
|
||||
setCoin(config, coin_u1(e.currency));
|
||||
setCoin(config, coin_u2(e.currency));
|
||||
setCoin(config, coin_u4(e.currency));
|
||||
setCoin(config, coin_u8(e.currency));
|
||||
setCoin(config, coin_u10(e.currency));
|
||||
|
||||
const exchangeMasterKey = talerCrypto.createEddsaKeyPair();
|
||||
|
||||
config.setString(
|
||||
"exchange",
|
||||
"master_public_key",
|
||||
talerCrypto.encodeCrock(exchangeMasterKey.eddsaPub),
|
||||
);
|
||||
|
||||
const masterPrivFile = config
|
||||
.getPath("exchange", "master_priv_file")
|
||||
.required();
|
||||
|
||||
fs.mkdirSync(path.dirname(masterPrivFile), { recursive: true });
|
||||
|
||||
fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv));
|
||||
|
||||
console.log("writing key to", masterPrivFile);
|
||||
console.log("pub is", talerCrypto.encodeCrock(exchangeMasterKey.eddsaPub));
|
||||
console.log(
|
||||
"priv is",
|
||||
talerCrypto.encodeCrock(exchangeMasterKey.eddsaPriv),
|
||||
);
|
||||
|
||||
const cfgFilename = gc.testDir + `/exchange-${e.name}.conf`;
|
||||
config.write(cfgFilename);
|
||||
return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey);
|
||||
}
|
||||
|
||||
get masterPub() {
|
||||
return talerCrypto.encodeCrock(this.keyPair.eddsaPub);
|
||||
}
|
||||
|
||||
get port() {
|
||||
return this.exchangeConfig.httpPort;
|
||||
}
|
||||
|
||||
async setupTestBankAccount(
|
||||
bc: BankService,
|
||||
localName: string,
|
||||
accountName: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
await bc.createAccount(accountName, password);
|
||||
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",
|
||||
`payto://x-taler-bank/localhost/${accountName}`,
|
||||
);
|
||||
config.setString(`exchange-account-${localName}`, "enable_credit", "yes");
|
||||
config.setString(`exchange-account-${localName}`, "enable_debit", "yes");
|
||||
config.setString(
|
||||
`exchange-account-${localName}`,
|
||||
"wire_gateway_url",
|
||||
`http://localhost:${bc.port}/taler-wire-gateway/${accountName}/`,
|
||||
);
|
||||
config.setString(
|
||||
`exchange-account-${localName}`,
|
||||
"wire_gateway_auth_method",
|
||||
"basic",
|
||||
);
|
||||
config.setString(`exchange-account-${localName}`, "username", accountName);
|
||||
config.setString(`exchange-account-${localName}`, "password", password);
|
||||
config.write(this.configFilename);
|
||||
}
|
||||
|
||||
exchangeHttpProc: ProcessWrapper | undefined;
|
||||
exchangeWirewatchProc: ProcessWrapper | undefined;
|
||||
|
||||
constructor(
|
||||
private globalState: GlobalTestState,
|
||||
private exchangeConfig: ExchangeConfig,
|
||||
private configFilename: string,
|
||||
private keyPair: talerCrypto.EddsaKeyPair,
|
||||
) {}
|
||||
|
||||
get name() {
|
||||
return this.exchangeConfig.name;
|
||||
}
|
||||
|
||||
get baseUrl() {
|
||||
return `http://localhost:${this.exchangeConfig.httpPort}/`;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await exec(`taler-exchange-dbinit -c "${this.configFilename}"`);
|
||||
await exec(`taler-exchange-wire -c "${this.configFilename}"`);
|
||||
await exec(`taler-exchange-keyup -c "${this.configFilename}"`);
|
||||
|
||||
this.exchangeWirewatchProc = this.globalState.spawnService(
|
||||
`taler-exchange-wirewatch -c "${this.configFilename}"`,
|
||||
`exchange-wirewatch-${this.name}`,
|
||||
);
|
||||
|
||||
this.exchangeHttpProc = this.globalState.spawnService(
|
||||
`taler-exchange-httpd -c "${this.configFilename}"`,
|
||||
`exchange-httpd-${this.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
async pingUntilAvailable(): Promise<void> {
|
||||
const url = `http://localhost:${this.exchangeConfig.httpPort}/keys`;
|
||||
while (true) {
|
||||
try {
|
||||
console.log("pinging exchange");
|
||||
const resp = await axios.get(url);
|
||||
console.log(resp.data);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.log("exchange not ready:", e.toString());
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface MerchantConfig {
|
||||
name: string;
|
||||
currency: string;
|
||||
httpPort: number;
|
||||
database: string;
|
||||
}
|
||||
|
||||
export class MerchantService {
|
||||
proc: ProcessWrapper | undefined;
|
||||
|
||||
constructor(
|
||||
private globalState: GlobalTestState,
|
||||
private merchantConfig: MerchantConfig,
|
||||
private configFilename: string,
|
||||
) {}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await exec(`taler-merchant-dbinit -c "${this.configFilename}"`);
|
||||
|
||||
this.proc = this.globalState.spawnService(
|
||||
`taler-merchant-httpd -c "${this.configFilename}"`,
|
||||
`merchant-${this.merchantConfig.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
static async create(
|
||||
gc: GlobalTestState,
|
||||
mc: MerchantConfig,
|
||||
): Promise<MerchantService> {
|
||||
const config = new Configuration();
|
||||
config.setString("taler", "currency", mc.currency);
|
||||
|
||||
config.setString("merchant", "serve", "tcp");
|
||||
config.setString("merchant", "port", `${mc.httpPort}`);
|
||||
config.setString("merchant", "db", "postgres");
|
||||
config.setString("exchangedb-postgres", "config", mc.database);
|
||||
|
||||
const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`;
|
||||
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" },
|
||||
});
|
||||
}
|
||||
|
||||
async queryPrivateOrderStatus(instanceName: string, orderId: string) {
|
||||
let url;
|
||||
if (instanceName === "default") {
|
||||
url = `http://localhost:${this.merchantConfig.httpPort}/private/orders/${orderId}`
|
||||
} else {
|
||||
url = `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/private/orders/${orderId}`;
|
||||
}
|
||||
const resp = await axios.get(url);
|
||||
return codecForMerchantOrderPrivateStatusResponse().decode(resp.data);
|
||||
}
|
||||
|
||||
async createOrder(
|
||||
instanceName: string,
|
||||
req: PostOrderRequest,
|
||||
): Promise<PostOrderResponse> {
|
||||
let url;
|
||||
if (instanceName === "default") {
|
||||
url = `http://localhost:${this.merchantConfig.httpPort}/private/orders`;
|
||||
} else {
|
||||
url = `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/private/orders`;
|
||||
}
|
||||
const resp = await axios.post(url, req);
|
||||
return codecForPostOrderResponse().decode(resp.data);
|
||||
}
|
||||
|
||||
async pingUntilAvailable(): Promise<void> {
|
||||
const url = `http://localhost:${this.merchantConfig.httpPort}/config`;
|
||||
while (true) {
|
||||
try {
|
||||
console.log("pinging merchant");
|
||||
const resp = await axios.get(url);
|
||||
console.log(resp.data);
|
||||
return;
|
||||
} catch (e) {
|
||||
console.log("merchant not ready", e.toString());
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface MerchantInstanceConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
paytoUris: string[];
|
||||
address?: unknown;
|
||||
jurisdiction?: unknown;
|
||||
defaultMaxWireFee?: string;
|
||||
defaultMaxDepositFee?: string;
|
||||
defaultWireFeeAmortization?: number;
|
||||
defaultWireTransferDelay?: time.Duration;
|
||||
defaultPayDelay?: time.Duration;
|
||||
}
|
||||
|
||||
export function runTest(testMain: (gc: GlobalTestState) => Promise<void>) {
|
||||
const main = async () => {
|
||||
const gc = new GlobalTestState({
|
||||
testDir: await makeTempDir(),
|
||||
});
|
||||
try {
|
||||
await testMain(gc);
|
||||
} finally {
|
||||
if (process.env["TALER_TEST_KEEP"] !== "1") {
|
||||
await gc.terminate();
|
||||
console.log("test logs and config can be found under", gc.testDir);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("FATAL: test failed with exception");
|
||||
if (e instanceof Error) {
|
||||
console.error(e);
|
||||
} else {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (process.env["TALER_TEST_KEEP"] !== "1") {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function shellWrap(s: string) {
|
||||
return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'";
|
||||
}
|
||||
|
||||
export class WalletCli {
|
||||
constructor(private globalTestState: GlobalTestState) {}
|
||||
|
||||
async apiRequest(
|
||||
request: string,
|
||||
payload: Record<string, unknown>,
|
||||
): Promise<walletCoreApi.CoreApiResponse> {
|
||||
const wdb = this.globalTestState.testDir + "/walletdb.json";
|
||||
const resp = await sh(
|
||||
`taler-wallet-cli --no-throttle --wallet-db '${wdb}' api '${request}' ${shellWrap(
|
||||
JSON.stringify(payload),
|
||||
)}`,
|
||||
);
|
||||
console.log(resp);
|
||||
return JSON.parse(resp) as walletCoreApi.CoreApiResponse;
|
||||
}
|
||||
|
||||
async runUntilDone(): Promise<void> {
|
||||
const wdb = this.globalTestState.testDir + "/walletdb.json";
|
||||
await sh(`taler-wallet-cli --no-throttle --wallet-db ${wdb} run-until-done`);
|
||||
}
|
||||
|
||||
async runPending(): Promise<void> {
|
||||
const wdb = this.globalTestState.testDir + "/walletdb.json";
|
||||
await sh(`taler-wallet-cli --no-throttle --wallet-db ${wdb} run-pending`);
|
||||
}
|
||||
}
|
157
packages/taler-integrationtests/src/helpers.ts
Normal file
157
packages/taler-integrationtests/src/helpers.ts
Normal file
@ -0,0 +1,157 @@
|
||||
/*
|
||||
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/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helpers to create typical test environments.
|
||||
*
|
||||
* @author Florian Dold <dold@taler.net>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports
|
||||
*/
|
||||
import {
|
||||
GlobalTestState,
|
||||
DbInfo,
|
||||
ExchangeService,
|
||||
WalletCli,
|
||||
MerchantService,
|
||||
setupDb,
|
||||
BankService,
|
||||
} from "./harness";
|
||||
import { AmountString } from "taler-wallet-core/lib/types/talerTypes";
|
||||
|
||||
export interface SimpleTestEnvironment {
|
||||
commonDb: DbInfo;
|
||||
bank: BankService;
|
||||
exchange: ExchangeService;
|
||||
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<SimpleTestEnvironment> {
|
||||
const db = await setupDb(t);
|
||||
|
||||
const bank = await BankService.create(t, {
|
||||
allowRegistrations: true,
|
||||
currency: "TESTKUDOS",
|
||||
database: db.connStr,
|
||||
httpPort: 8082,
|
||||
suggestedExchange: "http://localhost:8081/",
|
||||
suggestedExchangePayto: "payto://x-taler-bank/MyExchange",
|
||||
});
|
||||
|
||||
await bank.start();
|
||||
|
||||
await bank.pingUntilAvailable();
|
||||
|
||||
const exchange = ExchangeService.create(t, {
|
||||
name: "testexchange-1",
|
||||
currency: "TESTKUDOS",
|
||||
httpPort: 8081,
|
||||
database: db.connStr,
|
||||
});
|
||||
|
||||
await exchange.setupTestBankAccount(bank, "1", "MyExchange", "x");
|
||||
|
||||
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!");
|
||||
|
||||
const wallet = new WalletCli(t);
|
||||
|
||||
return {
|
||||
commonDb: db,
|
||||
exchange,
|
||||
merchant,
|
||||
wallet,
|
||||
bank,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw balance.
|
||||
*/
|
||||
export async function withdrawViaBank(t: GlobalTestState, p: {
|
||||
wallet: WalletCli;
|
||||
bank: BankService;
|
||||
exchange: ExchangeService;
|
||||
amount: AmountString;
|
||||
}): Promise<void> {
|
||||
|
||||
const { wallet, bank, exchange, amount } = p;
|
||||
|
||||
const user = await bank.createRandomBankUser();
|
||||
const wop = await bank.createWithdrawalOperation(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 bank.confirmWithdrawalOperation(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");
|
||||
}
|
217
packages/taler-integrationtests/src/merchantApiTypes.ts
Normal file
217
packages/taler-integrationtests/src/merchantApiTypes.ts
Normal file
@ -0,0 +1,217 @@
|
||||
/*
|
||||
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 {
|
||||
codec,
|
||||
talerTypes,
|
||||
time,
|
||||
} from "taler-wallet-core";
|
||||
|
||||
|
||||
export interface PostOrderRequest {
|
||||
// The order must at least contain the minimal
|
||||
// order detail, but can override all
|
||||
order: Partial<talerTypes.ContractTerms>;
|
||||
|
||||
// if set, the backend will then set the refund deadline to the current
|
||||
// time plus the specified delay.
|
||||
refund_delay?: time.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.Codec<PostOrderResponse> =>
|
||||
codec
|
||||
.makeCodecForObject<PostOrderResponse>()
|
||||
.property("order_id", codec.codecForString)
|
||||
.property("token", codec.makeCodecOptional(codec.codecForString))
|
||||
.build("PostOrderResponse");
|
||||
|
||||
export const codecForCheckPaymentPaidResponse = (): codec.Codec<
|
||||
CheckPaymentPaidResponse
|
||||
> =>
|
||||
codec
|
||||
.makeCodecForObject<CheckPaymentPaidResponse>()
|
||||
.property("order_status", codec.makeCodecForConstString("paid"))
|
||||
.property("refunded", codec.codecForBoolean)
|
||||
.property("wired", codec.codecForBoolean)
|
||||
.property("deposit_total", codec.codecForString)
|
||||
.property("exchange_ec", codec.codecForNumber)
|
||||
.property("exchange_hc", codec.codecForNumber)
|
||||
.property("refund_amount", codec.codecForString)
|
||||
.property("contract_terms", talerTypes.codecForContractTerms())
|
||||
// FIXME: specify
|
||||
.property("wire_details", codec.codecForAny)
|
||||
.property("wire_reports", codec.codecForAny)
|
||||
.property("refund_details", codec.codecForAny)
|
||||
.build("CheckPaymentPaidResponse");
|
||||
|
||||
export const codecForCheckPaymentUnpaidResponse = (): codec.Codec<
|
||||
CheckPaymentUnpaidResponse
|
||||
> =>
|
||||
codec
|
||||
.makeCodecForObject<CheckPaymentUnpaidResponse>()
|
||||
.property("order_status", codec.makeCodecForConstString("unpaid"))
|
||||
.property("taler_pay_uri", codec.codecForString)
|
||||
.property(
|
||||
"already_paid_order_id",
|
||||
codec.makeCodecOptional(codec.codecForString),
|
||||
)
|
||||
.build("CheckPaymentPaidResponse");
|
||||
|
||||
export const codecForMerchantOrderPrivateStatusResponse = (): codec.Codec<
|
||||
MerchantOrderPrivateStatusResponse
|
||||
> =>
|
||||
codec
|
||||
.makeCodecForUnion<MerchantOrderPrivateStatusResponse>()
|
||||
.discriminateOn("order_status")
|
||||
.alternative("paid", codecForCheckPaymentPaidResponse())
|
||||
.alternative("unpaid", codecForCheckPaymentUnpaidResponse())
|
||||
.build("MerchantOrderPrivateStatusResponse");
|
||||
|
||||
export type MerchantOrderPrivateStatusResponse =
|
||||
| CheckPaymentPaidResponse
|
||||
| CheckPaymentUnpaidResponse;
|
||||
|
||||
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: talerTypes.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: talerTypes.AmountString;
|
||||
|
||||
// Contract terms
|
||||
contract_terms: talerTypes.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[];
|
||||
}
|
||||
|
||||
export interface CheckPaymentUnpaidResponse {
|
||||
order_status: "unpaid";
|
||||
|
||||
// URI that the wallet must process to complete the payment.
|
||||
taler_pay_uri: 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: time.Timestamp;
|
||||
|
||||
// Total amount that was refunded (minus a refund fee).
|
||||
amount: talerTypes.AmountString;
|
||||
}
|
||||
|
||||
export interface TransactionWireTransfer {
|
||||
// Responsible exchange
|
||||
exchange_url: string;
|
||||
|
||||
// 32-byte wire transfer identifier
|
||||
wtid: string;
|
||||
|
||||
// execution time of the wire transfer
|
||||
execution_time: time.Timestamp;
|
||||
|
||||
// Total amount that has been wire transfered
|
||||
// to the merchant
|
||||
amount: talerTypes.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: talerTypes.CoinPublicKeyString;
|
||||
}
|
194
packages/taler-integrationtests/src/test-payment-fault.ts
Normal file
194
packages/taler-integrationtests/src/test-payment-fault.ts
Normal file
@ -0,0 +1,194 @@
|
||||
/*
|
||||
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/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sample fault injection test.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import {
|
||||
runTest,
|
||||
GlobalTestState,
|
||||
MerchantService,
|
||||
ExchangeService,
|
||||
setupDb,
|
||||
BankService,
|
||||
WalletCli,
|
||||
} from "./harness";
|
||||
import { FaultInjectedExchangeService, FaultInjectionRequestContext, FaultInjectionResponseContext } from "./faultInjection";
|
||||
import { CoreApiResponse } from "taler-wallet-core/lib/walletCoreApiHandler";
|
||||
|
||||
/**
|
||||
* 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,
|
||||
suggestedExchange: "http://localhost:8091/",
|
||||
suggestedExchangePayto: "payto://x-taler-bank/MyExchange",
|
||||
});
|
||||
|
||||
await bank.start();
|
||||
|
||||
await bank.pingUntilAvailable();
|
||||
|
||||
const exchange = ExchangeService.create(t, {
|
||||
name: "testexchange-1",
|
||||
currency: "TESTKUDOS",
|
||||
httpPort: 8081,
|
||||
database: db.connStr,
|
||||
});
|
||||
|
||||
await exchange.setupTestBankAccount(bank, "1", "MyExchange", "x");
|
||||
|
||||
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 bank.createRandomBankUser();
|
||||
const wop = await bank.createWithdrawalOperation(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 bank.confirmWithdrawalOperation(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 merchant.createOrder("default", {
|
||||
order: {
|
||||
summary: "Buy me!",
|
||||
amount: "TESTKUDOS:5",
|
||||
fulfillment_url: "taler://fulfillment-success/thx",
|
||||
},
|
||||
});
|
||||
|
||||
let orderStatus = await merchant.queryPrivateOrderStatus(
|
||||
"default",
|
||||
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 10 responses from the exchange.
|
||||
let faultCount = 0;
|
||||
faultyExchange.faultProxy.addFault({
|
||||
modifyResponse(ctx: FaultInjectionResponseContext) {
|
||||
if (faultCount < 10) {
|
||||
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 merchant.queryPrivateOrderStatus(
|
||||
"default",
|
||||
orderResp.order_id,
|
||||
);
|
||||
|
||||
t.assertTrue(orderStatus.order_status === "paid");
|
||||
});
|
80
packages/taler-integrationtests/src/test-payment.ts
Normal file
80
packages/taler-integrationtests/src/test-payment.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/*
|
||||
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/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { runTest, GlobalTestState } 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 merchant.createOrder("default", {
|
||||
order: {
|
||||
summary: "Buy me!",
|
||||
amount: "TESTKUDOS:5",
|
||||
fulfillment_url: "taler://fulfillment-success/thx",
|
||||
},
|
||||
});
|
||||
|
||||
let orderStatus = await merchant.queryPrivateOrderStatus(
|
||||
"default",
|
||||
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 merchant.queryPrivateOrderStatus(
|
||||
"default",
|
||||
orderResp.order_id,
|
||||
);
|
||||
|
||||
t.assertTrue(orderStatus.order_status === "paid");
|
||||
|
||||
await t.terminate();
|
||||
});
|
68
packages/taler-integrationtests/src/test-withdrawal.ts
Normal file
68
packages/taler-integrationtests/src/test-withdrawal.ts
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
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/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { runTest, GlobalTestState } from "./harness";
|
||||
import { createSimpleTestkudosEnvironment } from "./helpers";
|
||||
import { walletTypes } 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 bank.createRandomBankUser();
|
||||
const wop = await bank.createWithdrawalOperation(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 bank.confirmWithdrawalOperation(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 = walletTypes.codecForBalancesResponse().decode(balApiResp.result);
|
||||
t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available)
|
||||
|
||||
await t.terminate();
|
||||
});
|
63
packages/taler-integrationtests/testrunner
Executable file
63
packages/taler-integrationtests/testrunner
Executable file
@ -0,0 +1,63 @@
|
||||
#!/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 TESTGLOB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
|
||||
cd $DIR
|
||||
|
||||
./node_modules/.bin/tsc
|
||||
|
||||
export ESM_OPTIONS='{"sourceMap": true}'
|
||||
|
||||
shopt -s extglob
|
||||
|
||||
num_exec=0
|
||||
num_fail=0
|
||||
num_succ=0
|
||||
|
||||
# Glob tests
|
||||
for file in lib/$1?(.js); do
|
||||
case "$file" in
|
||||
*.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))
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "-----------------------------------"
|
||||
echo "Tests finished"
|
||||
echo "$num_succ/$num_exec tests succeeded"
|
||||
echo "-----------------------------------"
|
||||
|
||||
if [[ $num_fail = 0 ]]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
32
packages/taler-integrationtests/tsconfig.json
Normal file
32
packages/taler-integrationtests/tsconfig.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"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": "../idb-bridge/"
|
||||
}
|
||||
],
|
||||
"include": ["src/**/*"]
|
||||
}
|
@ -113,6 +113,7 @@ export class AndroidHttpLib implements httpLib.HttpRequestLibrary {
|
||||
requestUrl: "",
|
||||
headers,
|
||||
status: msg.status,
|
||||
requestMethod: "FIXME",
|
||||
json: async () => JSON.parse(msg.responseText),
|
||||
text: async () => msg.responseText,
|
||||
};
|
||||
|
@ -4,4 +4,4 @@ try {
|
||||
} catch (e) {
|
||||
// Do nothing.
|
||||
}
|
||||
require('../dist/taler-wallet-cli.js')
|
||||
require('../dist/taler-wallet-cli.js').walletCli.run();
|
||||
|
@ -34,6 +34,12 @@ import {
|
||||
NodeHttpLib,
|
||||
} from "taler-wallet-core";
|
||||
import * as clk from "./clk";
|
||||
import { NodeThreadCryptoWorkerFactory } from "taler-wallet-core/lib/crypto/workers/nodeThreadWorker";
|
||||
import { CryptoApi } from "taler-wallet-core/lib/crypto/workers/cryptoApi";
|
||||
|
||||
// This module also serves as the entry point for the crypto
|
||||
// thread worker, and thus must expose these two handlers.
|
||||
export { handleWorkerError, handleWorkerMessage } from "taler-wallet-core";
|
||||
|
||||
const logger = new Logger("taler-wallet-cli.ts");
|
||||
|
||||
@ -109,7 +115,7 @@ function printVersion(): void {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const walletCli = clk
|
||||
export const walletCli = clk
|
||||
.program("wallet", {
|
||||
help: "Command line interface for the GNU Taler wallet.",
|
||||
})
|
||||
@ -637,4 +643,9 @@ testCli.subcommand("vectors", "vectors").action(async (args) => {
|
||||
testvectors.printTestVectors();
|
||||
});
|
||||
|
||||
walletCli.run();
|
||||
testCli.subcommand("cryptoworker", "cryptoworker").action(async (args) => {
|
||||
const workerFactory = new NodeThreadCryptoWorkerFactory();
|
||||
const cryptoApi = new CryptoApi(workerFactory);
|
||||
const res = await cryptoApi.hashString("foo");
|
||||
console.log(res);
|
||||
});
|
||||
|
@ -34,25 +34,27 @@
|
||||
"@typescript-eslint/eslint-plugin": "^3.6.1",
|
||||
"@typescript-eslint/parser": "^3.6.1",
|
||||
"ava": "^3.10.1",
|
||||
"dts-bundle-generator": "^5.3.0",
|
||||
"eslint": "^7.4.0",
|
||||
"eslint-config-airbnb-typescript": "^8.0.2",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-react": "^7.20.3",
|
||||
"eslint-plugin-react-hooks": "^4.0.8",
|
||||
"esm": "^3.2.25",
|
||||
"jed": "^1.1.1",
|
||||
"moment": "^2.27.0",
|
||||
"nyc": "^15.1.0",
|
||||
"po2json": "^0.4.5",
|
||||
"pogen": "workspace:*",
|
||||
"prettier": "^2.0.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^2.23.0",
|
||||
"rollup-plugin-sourcemaps": "^0.6.2",
|
||||
"source-map-resolve": "^0.6.0",
|
||||
"structured-clone": "^0.2.2",
|
||||
"typedoc": "^0.17.8",
|
||||
"typescript": "^3.9.7",
|
||||
"rollup": "^2.23.0",
|
||||
"esm": "^3.2.25",
|
||||
"rimraf": "^3.0.2"
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^14.0.27",
|
||||
@ -63,7 +65,9 @@
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"ava": {
|
||||
"require": ["esm"],
|
||||
"require": [
|
||||
"esm"
|
||||
],
|
||||
"files": [
|
||||
"src/**/*-test.*"
|
||||
],
|
||||
|
@ -4,13 +4,14 @@ import nodeResolve from "@rollup/plugin-node-resolve";
|
||||
import json from "@rollup/plugin-json";
|
||||
import builtins from "builtin-modules";
|
||||
import pkg from "./package.json";
|
||||
import sourcemaps from 'rollup-plugin-sourcemaps';
|
||||
|
||||
export default {
|
||||
input: "lib/index.js",
|
||||
output: {
|
||||
file: pkg.main,
|
||||
format: "cjs",
|
||||
sourcemap: false,
|
||||
sourcemap: true,
|
||||
},
|
||||
external: builtins,
|
||||
plugins: [
|
||||
@ -18,11 +19,13 @@ export default {
|
||||
preferBuiltins: true,
|
||||
}),
|
||||
|
||||
sourcemaps(),
|
||||
|
||||
commonjs({
|
||||
include: [/node_modules/, /dist/],
|
||||
extensions: [".js"],
|
||||
ignoreGlobal: false,
|
||||
sourceMap: false,
|
||||
sourceMap: true,
|
||||
}),
|
||||
|
||||
json(),
|
||||
|
@ -1,17 +1,17 @@
|
||||
/*
|
||||
This file is part of TALER
|
||||
This file is part of GNU Taler
|
||||
(C) 2016 GNUnet e.V.
|
||||
|
||||
TALER is free software; you can redistribute it and/or modify it under the
|
||||
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.
|
||||
|
||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
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
|
||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -46,6 +46,7 @@ import {
|
||||
|
||||
import * as timer from "../../util/timer";
|
||||
import { Logger } from "../../util/logging";
|
||||
import { walletCoreApi } from "../..";
|
||||
|
||||
const logger = new Logger("cryptoApi.ts");
|
||||
|
||||
@ -182,7 +183,7 @@ export class CryptoApi {
|
||||
};
|
||||
this.resetWorkerTimeout(ws);
|
||||
work.startTime = timer.performanceNow();
|
||||
setTimeout(() => worker.postMessage(msg), 0);
|
||||
timer.after(0, () => worker.postMessage(msg));
|
||||
}
|
||||
|
||||
resetWorkerTimeout(ws: WorkerState): void {
|
||||
@ -198,6 +199,7 @@ export class CryptoApi {
|
||||
}
|
||||
};
|
||||
ws.terminationTimerHandle = timer.after(15 * 1000, destroy);
|
||||
//ws.terminationTimerHandle.unref();
|
||||
}
|
||||
|
||||
handleWorkerError(ws: WorkerState, e: any): void {
|
||||
|
@ -21,6 +21,9 @@ import { CryptoWorkerFactory } from "./cryptoApi";
|
||||
import { CryptoWorker } from "./cryptoWorker";
|
||||
import os from "os";
|
||||
import { CryptoImplementation } from "./cryptoImplementation";
|
||||
import { Logger } from "../../util/logging";
|
||||
|
||||
const logger = new Logger("nodeThreadWorker.ts");
|
||||
|
||||
const f = __filename;
|
||||
|
||||
@ -37,16 +40,22 @@ const workerCode = `
|
||||
try {
|
||||
tw = require("${f}");
|
||||
} catch (e) {
|
||||
console.log("could not load from ${f}");
|
||||
console.warn("could not load from ${f}");
|
||||
}
|
||||
if (!tw) {
|
||||
try {
|
||||
tw = require("taler-wallet-android");
|
||||
} catch (e) {
|
||||
console.log("could not load taler-wallet-android either");
|
||||
console.warn("could not load taler-wallet-android either");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (typeof tw.handleWorkerMessage !== "function") {
|
||||
throw Error("module loaded for crypto worker lacks handleWorkerMessage");
|
||||
}
|
||||
if (typeof tw.handleWorkerError !== "function") {
|
||||
throw Error("module loaded for crypto worker lacks handleWorkerError");
|
||||
}
|
||||
parentPort.on("message", tw.handleWorkerMessage);
|
||||
parentPort.on("error", tw.handleWorkerError);
|
||||
`;
|
||||
@ -138,6 +147,9 @@ class NodeThreadCryptoWorker implements CryptoWorker {
|
||||
constructor() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const worker_threads = require("worker_threads");
|
||||
|
||||
logger.trace("starting node crypto worker");
|
||||
|
||||
this.nodeWorker = new worker_threads.Worker(workerCode, { eval: true });
|
||||
this.nodeWorker.on("error", (err: Error) => {
|
||||
console.error("error in node worker:", err);
|
||||
@ -145,6 +157,9 @@ class NodeThreadCryptoWorker implements CryptoWorker {
|
||||
this.onerror(err);
|
||||
}
|
||||
});
|
||||
this.nodeWorker.on("exit", (err) => {
|
||||
logger.trace(`worker exited with code ${err}`);
|
||||
});
|
||||
this.nodeWorker.on("message", (v: any) => {
|
||||
if (this.onmessage) {
|
||||
this.onmessage(v);
|
||||
|
@ -45,7 +45,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
||||
}
|
||||
|
||||
private async req(
|
||||
method: "post" | "get",
|
||||
method: "POST" | "GET",
|
||||
url: string,
|
||||
body: any,
|
||||
opt?: HttpRequestOptions,
|
||||
@ -72,6 +72,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
||||
{
|
||||
httpStatusCode: resp.status,
|
||||
requestUrl: url,
|
||||
requestMethod: method,
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -88,6 +89,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
||||
{
|
||||
httpStatusCode: resp.status,
|
||||
requestUrl: url,
|
||||
requestMethod: method,
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -100,6 +102,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
||||
{
|
||||
httpStatusCode: resp.status,
|
||||
requestUrl: url,
|
||||
requestMethod: method,
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -112,6 +115,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
||||
}
|
||||
return {
|
||||
requestUrl: url,
|
||||
requestMethod: method,
|
||||
headers,
|
||||
status: resp.status,
|
||||
text: async () => resp.data,
|
||||
@ -120,7 +124,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
||||
}
|
||||
|
||||
async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||
return this.req("get", url, undefined, opt);
|
||||
return this.req("GET", url, undefined, opt);
|
||||
}
|
||||
|
||||
async postJson(
|
||||
@ -128,6 +132,6 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
||||
body: any,
|
||||
opt?: HttpRequestOptions,
|
||||
): Promise<HttpResponse> {
|
||||
return this.req("post", url, body, opt);
|
||||
return this.req("POST", url, body, opt);
|
||||
}
|
||||
}
|
||||
|
@ -73,3 +73,10 @@ export * as i18n from "./i18n";
|
||||
export * as nodeThreadWorker from "./crypto/workers/nodeThreadWorker";
|
||||
|
||||
export * as walletNotifications from "./types/notifications";
|
||||
|
||||
export { Configuration } from "./util/talerconfig";
|
||||
|
||||
export {
|
||||
handleWorkerMessage,
|
||||
handleWorkerError,
|
||||
} from "./crypto/workers/nodeThreadWorker";
|
||||
|
@ -112,6 +112,8 @@ async function updateExchangeWithKeys(
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("updating exchange /keys info");
|
||||
|
||||
const keysUrl = new URL("keys", baseUrl);
|
||||
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
||||
|
||||
@ -121,6 +123,8 @@ async function updateExchangeWithKeys(
|
||||
codecForExchangeKeysJson(),
|
||||
);
|
||||
|
||||
logger.info("received /keys response");
|
||||
|
||||
if (exchangeKeysJson.denoms.length === 0) {
|
||||
const opErr = makeErrorDetails(
|
||||
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
|
||||
@ -152,12 +156,16 @@ async function updateExchangeWithKeys(
|
||||
const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
|
||||
.currency;
|
||||
|
||||
logger.trace("processing denominations");
|
||||
|
||||
const newDenominations = await Promise.all(
|
||||
exchangeKeysJson.denoms.map((d) =>
|
||||
denominationRecordFromKeys(ws, baseUrl, d),
|
||||
),
|
||||
);
|
||||
|
||||
logger.trace("done with processing denominations");
|
||||
|
||||
const lastUpdateTimestamp = getTimestampNow();
|
||||
|
||||
const recoupGroupId: string | undefined = undefined;
|
||||
@ -241,6 +249,8 @@ async function updateExchangeWithKeys(
|
||||
console.log("error while recouping coins:", e);
|
||||
});
|
||||
}
|
||||
|
||||
logger.trace("done updating exchange /keys");
|
||||
}
|
||||
|
||||
async function updateExchangeFinalize(
|
||||
|
@ -781,7 +781,7 @@ export async function submitPay(
|
||||
}
|
||||
const sessionId = purchase.lastSessionId;
|
||||
|
||||
console.log("paying with session ID", sessionId);
|
||||
logger.trace("paying with session ID", sessionId);
|
||||
|
||||
const payUrl = new URL(
|
||||
`orders/${purchase.contractData.orderId}/pay`,
|
||||
|
@ -712,7 +712,9 @@ export async function getWithdrawalDetailsForUri(
|
||||
ws: InternalWalletState,
|
||||
talerWithdrawUri: string,
|
||||
): Promise<WithdrawUriInfoResponse> {
|
||||
logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
|
||||
const info = await getBankWithdrawalInfo(ws, talerWithdrawUri);
|
||||
logger.trace(`got bank info`);
|
||||
if (info.suggestedExchange) {
|
||||
// FIXME: right now the exchange gets permanently added,
|
||||
// we might want to only temporarily add it.
|
||||
|
@ -40,8 +40,11 @@ import {
|
||||
codecForString,
|
||||
makeCodecOptional,
|
||||
Codec,
|
||||
makeCodecForList,
|
||||
codecForBoolean,
|
||||
} from "../util/codec";
|
||||
import { AmountString } from "./talerTypes";
|
||||
import { codec } from "..";
|
||||
|
||||
/**
|
||||
* Response for the create reserve request to the wallet.
|
||||
@ -164,6 +167,20 @@ export interface BalancesResponse {
|
||||
balances: Balance[];
|
||||
}
|
||||
|
||||
export const codecForBalance = (): Codec<Balance> =>
|
||||
makeCodecForObject<Balance>()
|
||||
.property("available", codecForString)
|
||||
.property("hasPendingTransactions", codecForBoolean)
|
||||
.property("pendingIncoming", codecForString)
|
||||
.property("pendingOutgoing", codecForString)
|
||||
.property("requiresUserInput", codecForBoolean)
|
||||
.build("Balance");
|
||||
|
||||
export const codecForBalancesResponse = (): Codec<BalancesResponse> =>
|
||||
makeCodecForObject<BalancesResponse>()
|
||||
.property("balances", makeCodecForList(codecForBalance()))
|
||||
.build("BalancesResponse");
|
||||
|
||||
/**
|
||||
* For terseness.
|
||||
*/
|
||||
|
@ -34,6 +34,7 @@ const logger = new Logger("http.ts");
|
||||
*/
|
||||
export interface HttpResponse {
|
||||
requestUrl: string;
|
||||
requestMethod: string;
|
||||
status: number;
|
||||
headers: Headers;
|
||||
json(): Promise<any>;
|
||||
@ -118,6 +119,8 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
|
||||
"Error response did not contain error code",
|
||||
{
|
||||
requestUrl: httpResponse.requestUrl,
|
||||
requestMethod: httpResponse.requestMethod,
|
||||
httpStatusCode: httpResponse.status,
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -188,7 +191,9 @@ export async function readSuccessResponseTextOrErrorCode<T>(
|
||||
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||
"Error response did not contain error code",
|
||||
{
|
||||
httpStatusCode: httpResponse.status,
|
||||
requestUrl: httpResponse.requestUrl,
|
||||
requestMethod: httpResponse.requestMethod,
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -217,7 +222,9 @@ export async function checkSuccessResponseOrThrow(
|
||||
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||
"Error response did not contain error code",
|
||||
{
|
||||
httpStatusCode: httpResponse.status,
|
||||
requestUrl: httpResponse.requestUrl,
|
||||
requestMethod: httpResponse.requestMethod,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
124
packages/taler-wallet-core/src/util/talerconfig-test.ts
Normal file
124
packages/taler-wallet-core/src/util/talerconfig-test.ts
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
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/>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Imports
|
||||
*/
|
||||
import test from "ava";
|
||||
import { pathsub, Configuration } from "./talerconfig";
|
||||
|
||||
test("pathsub", (t) => {
|
||||
t.assert("foo" === pathsub("foo", () => undefined));
|
||||
|
||||
t.assert("fo${bla}o" === pathsub("fo${bla}o", () => undefined));
|
||||
|
||||
const d: Record<string, string> = {
|
||||
w: "world",
|
||||
f: "foo",
|
||||
"1foo": "x",
|
||||
"foo_bar": "quux",
|
||||
};
|
||||
|
||||
t.is(
|
||||
pathsub("hello ${w}!", (v) => d[v]),
|
||||
"hello world!",
|
||||
);
|
||||
|
||||
t.is(
|
||||
pathsub("hello ${w} ${w}!", (v) => d[v]),
|
||||
"hello world world!",
|
||||
);
|
||||
|
||||
t.is(
|
||||
pathsub("hello ${x:-blabla}!", (v) => d[v]),
|
||||
"hello blabla!",
|
||||
);
|
||||
|
||||
// No braces
|
||||
t.is(
|
||||
pathsub("hello $w!", (v) => d[v]),
|
||||
"hello world!",
|
||||
);
|
||||
t.is(
|
||||
pathsub("hello $foo!", (v) => d[v]),
|
||||
"hello $foo!",
|
||||
);
|
||||
t.is(
|
||||
pathsub("hello $1foo!", (v) => d[v]),
|
||||
"hello $1foo!",
|
||||
);
|
||||
t.is(
|
||||
pathsub("hello $$ world!", (v) => d[v]),
|
||||
"hello $$ world!",
|
||||
);
|
||||
t.is(
|
||||
pathsub("hello $$ world!", (v) => d[v]),
|
||||
"hello $$ world!",
|
||||
);
|
||||
|
||||
t.is(
|
||||
pathsub("hello $foo_bar!", (v) => d[v]),
|
||||
"hello quux!",
|
||||
);
|
||||
|
||||
// Recursive lookup in default
|
||||
t.is(
|
||||
pathsub("hello ${x:-${w}}!", (v) => d[v]),
|
||||
"hello world!",
|
||||
);
|
||||
|
||||
// No variables in variable name part
|
||||
t.is(
|
||||
pathsub("hello ${${w}:-x}!", (v) => d[v]),
|
||||
"hello ${${w}:-x}!",
|
||||
);
|
||||
|
||||
// Missing closing brace
|
||||
t.is(
|
||||
pathsub("hello ${w!", (v) => d[v]),
|
||||
"hello ${w!",
|
||||
);
|
||||
});
|
||||
|
||||
test("path expansion", (t) => {
|
||||
const config = new Configuration();
|
||||
config.setString("paths", "taler_home", "foo/bar");
|
||||
config.setString(
|
||||
"paths",
|
||||
"taler_data_home",
|
||||
"$TALER_HOME/.local/share/taler/",
|
||||
);
|
||||
config.setString(
|
||||
"exchange",
|
||||
"master_priv_file",
|
||||
"${TALER_DATA_HOME}/exchange/offline-keys/master.priv",
|
||||
);
|
||||
t.is(
|
||||
config.getPath("exchange", "MaStER_priv_file").required(),
|
||||
"foo/bar/.local/share/taler//exchange/offline-keys/master.priv",
|
||||
);
|
||||
});
|
||||
|
||||
test("recursive path resolution", (t) => {
|
||||
console.log("recursive test");
|
||||
const config = new Configuration();
|
||||
config.setString("paths", "a", "x${b}");
|
||||
config.setString("paths", "b", "y${a}");
|
||||
config.setString("foo", "x", "z${a}");
|
||||
t.throws(() => {
|
||||
config.getPath("foo", "a").required();
|
||||
});
|
||||
});
|
@ -25,6 +25,8 @@
|
||||
*/
|
||||
import { AmountJson } from "./amounts";
|
||||
import * as Amounts from "./amounts";
|
||||
import fs from "fs";
|
||||
import { acceptExchangeTermsOfService } from "../operations/exchanges";
|
||||
|
||||
export class ConfigError extends Error {
|
||||
constructor(message: string) {
|
||||
@ -56,6 +58,89 @@ export class ConfigValue<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shell-style path substitution.
|
||||
*
|
||||
* Supported patterns:
|
||||
* "$x" (look up "x")
|
||||
* "${x}" (look up "x")
|
||||
* "${x:-y}" (look up "x", fall back to expanded y)
|
||||
*/
|
||||
export function pathsub(
|
||||
x: string,
|
||||
lookup: (s: string, depth: number) => string | undefined,
|
||||
depth = 0,
|
||||
): string {
|
||||
if (depth >= 10) {
|
||||
throw Error("recursion in path substitution");
|
||||
}
|
||||
let s = x;
|
||||
let l = 0;
|
||||
while (l < s.length) {
|
||||
if (s[l] === "$") {
|
||||
if (s[l + 1] === "{") {
|
||||
let depth = 1;
|
||||
const start = l;
|
||||
let p = start + 2;
|
||||
let insideNamePart = true;
|
||||
let hasDefault = false;
|
||||
for (; p < s.length; p++) {
|
||||
if (s[p] == "}") {
|
||||
insideNamePart = false;
|
||||
depth--;
|
||||
} else if (s[p] === "$" && s[p + 1] === "{") {
|
||||
insideNamePart = false;
|
||||
depth++;
|
||||
}
|
||||
if (insideNamePart && s[p] === ":" && s[p + 1] === "-") {
|
||||
hasDefault = true;
|
||||
}
|
||||
if (depth == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (depth == 0) {
|
||||
const inner = s.slice(start + 2, p);
|
||||
let varname: string;
|
||||
let defaultValue: string | undefined;
|
||||
if (hasDefault) {
|
||||
[varname, defaultValue] = inner.split(":-", 2);
|
||||
} else {
|
||||
varname = inner;
|
||||
defaultValue = undefined;
|
||||
}
|
||||
|
||||
const r = lookup(inner, depth + 1);
|
||||
if (r !== undefined) {
|
||||
s = s.substr(0, start) + r + s.substr(p + 1);
|
||||
l = start + r.length;
|
||||
continue;
|
||||
} else if (defaultValue !== undefined) {
|
||||
const resolvedDefault = pathsub(defaultValue, lookup, depth + 1);
|
||||
s = s.substr(0, start) + resolvedDefault + s.substr(p + 1);
|
||||
l = start + resolvedDefault.length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
l = p;
|
||||
continue;
|
||||
} else {
|
||||
const m = /^[a-zA-Z-_][a-zA-Z0-9-_]*/.exec(s.substring(l + 1));
|
||||
if (m && m[0]) {
|
||||
const r = lookup(m[0], depth + 1);
|
||||
if (r !== undefined) {
|
||||
s = s.substr(0, l) + r + s.substr(l + 1 + m[0].length);
|
||||
l = l + r.length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
l++;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export class Configuration {
|
||||
private sectionMap: SectionMap = {};
|
||||
|
||||
@ -69,7 +154,6 @@ export class Configuration {
|
||||
|
||||
const lines = s.split("\n");
|
||||
for (const line of lines) {
|
||||
console.log("parsing line", JSON.stringify(line));
|
||||
if (reEmptyLine.test(line)) {
|
||||
continue;
|
||||
}
|
||||
@ -79,15 +163,15 @@ export class Configuration {
|
||||
const secMatch = line.match(reSection);
|
||||
if (secMatch) {
|
||||
currentSection = secMatch[1];
|
||||
console.log("setting section to", currentSection);
|
||||
continue;
|
||||
}
|
||||
if (currentSection === undefined) {
|
||||
throw Error("invalid configuration, expected section header");
|
||||
}
|
||||
currentSection = currentSection.toUpperCase();
|
||||
const paramMatch = line.match(reParam);
|
||||
if (paramMatch) {
|
||||
const optName = paramMatch[1];
|
||||
const optName = paramMatch[1].toUpperCase();
|
||||
let val = paramMatch[2];
|
||||
if (val.startsWith('"') && val.endsWith('"')) {
|
||||
val = val.slice(1, val.length - 1);
|
||||
@ -102,13 +186,44 @@ export class Configuration {
|
||||
"invalid configuration, expected section header or option assignment",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("parsed config", JSON.stringify(this.sectionMap, undefined, 2));
|
||||
setString(section: string, option: string, value: string): void {
|
||||
const secNorm = section.toUpperCase();
|
||||
const sec = this.sectionMap[secNorm] ?? (this.sectionMap[secNorm] = {});
|
||||
sec[option.toUpperCase()] = value;
|
||||
}
|
||||
|
||||
getString(section: string, option: string): ConfigValue<string> {
|
||||
const val = (this.sectionMap[section] ?? {})[option];
|
||||
return new ConfigValue(section, option, val, (x) => x);
|
||||
const secNorm = section.toUpperCase();
|
||||
const optNorm = option.toUpperCase();
|
||||
const val = (this.sectionMap[section] ?? {})[optNorm];
|
||||
return new ConfigValue(secNorm, optNorm, val, (x) => x);
|
||||
}
|
||||
|
||||
getPath(section: string, option: string): ConfigValue<string> {
|
||||
const secNorm = section.toUpperCase();
|
||||
const optNorm = option.toUpperCase();
|
||||
const val = (this.sectionMap[secNorm] ?? {})[optNorm];
|
||||
return new ConfigValue(secNorm, optNorm, val, (x) =>
|
||||
pathsub(x, (v, d) => this.lookupVariable(v, d + 1)),
|
||||
);
|
||||
}
|
||||
|
||||
lookupVariable(x: string, depth: number = 0): string | undefined {
|
||||
console.log("looking up", x);
|
||||
// We loop up options in PATHS in upper case, as option names
|
||||
// are case insensitive
|
||||
const val = (this.sectionMap["PATHS"] ?? {})[x.toUpperCase()];
|
||||
if (val !== undefined) {
|
||||
return pathsub(val, (v, d) => this.lookupVariable(v, d), depth);
|
||||
}
|
||||
// Environment variables can be case sensitive, respect that.
|
||||
const envVal = process.env[x];
|
||||
if (envVal !== undefined) {
|
||||
return envVal;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
getAmount(section: string, option: string): ConfigValue<AmountJson> {
|
||||
@ -117,4 +232,28 @@ export class Configuration {
|
||||
Amounts.parseOrThrow(x),
|
||||
);
|
||||
}
|
||||
|
||||
static load(filename: string): Configuration {
|
||||
const s = fs.readFileSync(filename, "utf-8");
|
||||
const cfg = new Configuration();
|
||||
cfg.loadFromString(s);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
write(filename: string): void {
|
||||
let s = "";
|
||||
for (const sectionName of Object.keys(this.sectionMap)) {
|
||||
s += `[${sectionName}]\n`;
|
||||
for (const optionName of Object.keys(
|
||||
this.sectionMap[sectionName] ?? {},
|
||||
)) {
|
||||
const val = this.sectionMap[sectionName][optionName];
|
||||
if (val !== undefined) {
|
||||
s += `${optionName} = ${val}\n`;
|
||||
}
|
||||
}
|
||||
s += "\n";
|
||||
}
|
||||
fs.writeFileSync(filename, s);
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,12 @@ const logger = new Logger("timer.ts");
|
||||
*/
|
||||
export interface TimerHandle {
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Make sure the event loop exits when the timer is the
|
||||
* only event left. Has no effect in the browser.
|
||||
*/
|
||||
unref(): void;
|
||||
}
|
||||
|
||||
class IntervalHandle {
|
||||
@ -42,6 +48,16 @@ class IntervalHandle {
|
||||
clear(): void {
|
||||
clearInterval(this.h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the event loop exits when the timer is the
|
||||
* only event left. Has no effect in the browser.
|
||||
*/
|
||||
unref(): void {
|
||||
if (typeof this.h === "object") {
|
||||
this.h.unref();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TimeoutHandle {
|
||||
@ -50,6 +66,16 @@ class TimeoutHandle {
|
||||
clear(): void {
|
||||
clearTimeout(this.h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the event loop exits when the timer is the
|
||||
* only event left. Has no effect in the browser.
|
||||
*/
|
||||
unref(): void {
|
||||
if (typeof this.h === "object") {
|
||||
this.h.unref();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,6 +118,10 @@ const nullTimerHandle = {
|
||||
// do nothing
|
||||
return;
|
||||
},
|
||||
unref() {
|
||||
// do nothing
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -141,6 +171,9 @@ export class TimerGroup {
|
||||
h.clear();
|
||||
delete tm[myId];
|
||||
},
|
||||
unref() {
|
||||
h.unref();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -160,6 +193,9 @@ export class TimerGroup {
|
||||
h.clear();
|
||||
delete tm[myId];
|
||||
},
|
||||
unref() {
|
||||
h.unref();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -102,6 +102,7 @@ export class BrowserHttpLib implements httpLib.HttpRequestLibrary {
|
||||
requestUrl: url,
|
||||
status: myRequest.status,
|
||||
headers: headerMap,
|
||||
requestMethod: method,
|
||||
json: makeJson,
|
||||
text: async () => myRequest.responseText,
|
||||
};
|
||||
@ -112,7 +113,7 @@ export class BrowserHttpLib implements httpLib.HttpRequestLibrary {
|
||||
}
|
||||
|
||||
get(url: string, opt?: httpLib.HttpRequestOptions): Promise<httpLib.HttpResponse> {
|
||||
return this.req("get", url, undefined, opt);
|
||||
return this.req("GET", url, undefined, opt);
|
||||
}
|
||||
|
||||
postJson(
|
||||
@ -120,7 +121,7 @@ export class BrowserHttpLib implements httpLib.HttpRequestLibrary {
|
||||
body: unknown,
|
||||
opt?: httpLib.HttpRequestOptions,
|
||||
): Promise<httpLib.HttpResponse> {
|
||||
return this.req("post", url, JSON.stringify(body), opt);
|
||||
return this.req("POST", url, JSON.stringify(body), opt);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
|
142
pnpm-lock.yaml
142
pnpm-lock.yaml
@ -29,6 +29,28 @@ importers:
|
||||
specifiers:
|
||||
'@types/node': ^11.12.0
|
||||
typescript: ^3.3.4000
|
||||
packages/taler-integrationtests:
|
||||
dependencies:
|
||||
axios: 0.19.2
|
||||
taler-wallet-core: 'link:../taler-wallet-core'
|
||||
tslib: 2.0.0
|
||||
typescript: 3.9.7
|
||||
devDependencies:
|
||||
'@ava/typescript': 1.1.1
|
||||
ava: 3.11.1
|
||||
esm: 3.2.25
|
||||
source-map-support: 0.5.19
|
||||
ts-node: 8.10.2_typescript@3.9.7
|
||||
specifiers:
|
||||
'@ava/typescript': ^1.1.1
|
||||
ava: ^3.11.1
|
||||
axios: ^0.19.2
|
||||
esm: ^3.2.25
|
||||
source-map-support: ^0.5.19
|
||||
taler-wallet-core: 'workspace:*'
|
||||
ts-node: ^8.10.2
|
||||
tslib: ^2.0.0
|
||||
typescript: ^3.9.7
|
||||
packages/taler-wallet-android:
|
||||
dependencies:
|
||||
taler-wallet-core: 'link:../taler-wallet-core'
|
||||
@ -100,6 +122,7 @@ importers:
|
||||
'@typescript-eslint/eslint-plugin': 3.7.1_98f5354ad0bbc327ab4925c12674a6b1
|
||||
'@typescript-eslint/parser': 3.7.1_eslint@7.6.0+typescript@3.9.7
|
||||
ava: 3.11.0
|
||||
dts-bundle-generator: 5.3.0
|
||||
eslint: 7.6.0
|
||||
eslint-config-airbnb-typescript: 8.0.2_de36c6f68d63a4142de06a31bab9d790
|
||||
eslint-plugin-import: 2.22.0_eslint@7.6.0
|
||||
@ -115,6 +138,7 @@ importers:
|
||||
prettier: 2.0.5
|
||||
rimraf: 3.0.2
|
||||
rollup: 2.23.0
|
||||
rollup-plugin-sourcemaps: 0.6.2_1bb4f16ce5b550396581a296af208cfa
|
||||
source-map-resolve: 0.6.0
|
||||
structured-clone: 0.2.2
|
||||
typedoc: 0.17.8_typescript@3.9.7
|
||||
@ -127,6 +151,7 @@ importers:
|
||||
ava: ^3.10.1
|
||||
axios: ^0.19.2
|
||||
big-integer: ^1.6.48
|
||||
dts-bundle-generator: ^5.3.0
|
||||
eslint: ^7.4.0
|
||||
eslint-config-airbnb-typescript: ^8.0.2
|
||||
eslint-plugin-import: ^2.22.0
|
||||
@ -143,6 +168,7 @@ importers:
|
||||
prettier: ^2.0.5
|
||||
rimraf: ^3.0.2
|
||||
rollup: ^2.23.0
|
||||
rollup-plugin-sourcemaps: ^0.6.2
|
||||
source-map-resolve: ^0.6.0
|
||||
source-map-support: ^0.5.19
|
||||
structured-clone: ^0.2.2
|
||||
@ -861,6 +887,10 @@ packages:
|
||||
dev: true
|
||||
resolution:
|
||||
integrity: sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
|
||||
/arg/4.1.3:
|
||||
dev: true
|
||||
resolution:
|
||||
integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
/argparse/1.0.10:
|
||||
dependencies:
|
||||
sprintf-js: 1.0.3
|
||||
@ -1032,6 +1062,69 @@ packages:
|
||||
hasBin: true
|
||||
resolution:
|
||||
integrity: sha512-y5U8BGeSRjs/OypsC4CJxr+L1KtLKU5kUyHr5hcghXn7HNr2f4LE/4gvl0Q5lNkLX1obdRW1oODphNdU/glwmA==
|
||||
/ava/3.11.1:
|
||||
dependencies:
|
||||
'@concordance/react': 2.0.0
|
||||
acorn: 7.3.1
|
||||
acorn-walk: 7.2.0
|
||||
ansi-styles: 4.2.1
|
||||
arrgv: 1.0.2
|
||||
arrify: 2.0.1
|
||||
callsites: 3.1.0
|
||||
chalk: 4.1.0
|
||||
chokidar: 3.4.1
|
||||
chunkd: 2.0.1
|
||||
ci-info: 2.0.0
|
||||
ci-parallel-vars: 1.0.1
|
||||
clean-yaml-object: 0.1.0
|
||||
cli-cursor: 3.1.0
|
||||
cli-truncate: 2.1.0
|
||||
code-excerpt: 3.0.0
|
||||
common-path-prefix: 3.0.0
|
||||
concordance: 5.0.0
|
||||
convert-source-map: 1.7.0
|
||||
currently-unhandled: 0.4.1
|
||||
debug: 4.1.1
|
||||
del: 5.1.0
|
||||
emittery: 0.7.1
|
||||
equal-length: 1.0.1
|
||||
figures: 3.2.0
|
||||
globby: 11.0.1
|
||||
ignore-by-default: 2.0.0
|
||||
import-local: 3.0.2
|
||||
indent-string: 4.0.0
|
||||
is-error: 2.2.2
|
||||
is-plain-object: 4.1.1
|
||||
is-promise: 4.0.0
|
||||
lodash: 4.17.19
|
||||
matcher: 3.0.0
|
||||
md5-hex: 3.0.1
|
||||
mem: 6.1.0
|
||||
ms: 2.1.2
|
||||
ora: 4.0.5
|
||||
p-map: 4.0.0
|
||||
picomatch: 2.2.2
|
||||
pkg-conf: 3.1.0
|
||||
plur: 4.0.0
|
||||
pretty-ms: 7.0.0
|
||||
read-pkg: 5.2.0
|
||||
resolve-cwd: 3.0.0
|
||||
slash: 3.0.0
|
||||
source-map-support: 0.5.19
|
||||
stack-utils: 2.0.2
|
||||
strip-ansi: 6.0.0
|
||||
supertap: 1.0.0
|
||||
temp-dir: 2.0.0
|
||||
trim-off-newlines: 1.0.1
|
||||
update-notifier: 4.1.0
|
||||
write-file-atomic: 3.0.3
|
||||
yargs: 15.4.1
|
||||
dev: true
|
||||
engines:
|
||||
node: '>=10.18.0 <11 || >=12.14.0 <12.17.0 || >=12.17.0 <13 || >=14.0.0'
|
||||
hasBin: true
|
||||
resolution:
|
||||
integrity: sha512-yGPD0msa5Qronw7GHDNlLaB7oU5zryYtXeuvny40YV6TMskSghqK7Ky3NisM/sr+aqI3DY7sfmORx8dIWQgMoQ==
|
||||
/axe-core/3.5.5:
|
||||
dev: true
|
||||
engines:
|
||||
@ -1541,6 +1634,12 @@ packages:
|
||||
node: '>=8'
|
||||
resolution:
|
||||
integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==
|
||||
/diff/4.0.2:
|
||||
dev: true
|
||||
engines:
|
||||
node: '>=0.3.1'
|
||||
resolution:
|
||||
integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
/dir-glob/3.0.1:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
@ -1617,6 +1716,16 @@ packages:
|
||||
node: '>=8'
|
||||
resolution:
|
||||
integrity: sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==
|
||||
/dts-bundle-generator/5.3.0:
|
||||
dependencies:
|
||||
typescript: 3.9.7
|
||||
yargs: 15.4.1
|
||||
dev: true
|
||||
engines:
|
||||
node: '>=12.0.0'
|
||||
hasBin: true
|
||||
resolution:
|
||||
integrity: sha512-PevcqtUQDsVs1FoXNEEvBgXWP2pNXT/booL+ufNcKSynEP8l01ebI9MgamECljThi+MHyjxYEbwGx+95TvigMQ==
|
||||
/duplexer3/0.1.4:
|
||||
dev: true
|
||||
resolution:
|
||||
@ -3070,6 +3179,10 @@ packages:
|
||||
node: '>=8'
|
||||
resolution:
|
||||
integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
|
||||
/make-error/1.3.6:
|
||||
dev: true
|
||||
resolution:
|
||||
integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
/map-age-cleaner/0.1.3:
|
||||
dependencies:
|
||||
p-defer: 1.0.0
|
||||
@ -3353,14 +3466,14 @@ packages:
|
||||
dev: true
|
||||
resolution:
|
||||
integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
/onetime/5.1.0:
|
||||
/onetime/5.1.1:
|
||||
dependencies:
|
||||
mimic-fn: 2.1.0
|
||||
dev: true
|
||||
engines:
|
||||
node: '>=6'
|
||||
resolution:
|
||||
integrity: sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==
|
||||
integrity: sha512-ZpZpjcJeugQfWsfyQlshVoowIIQ1qBGSVll4rfDq6JJVO//fesjoX808hXWfBjY+ROZgpKDI5TRSRBSoJiZ8eg==
|
||||
/optionator/0.9.1:
|
||||
dependencies:
|
||||
deep-is: 0.1.3
|
||||
@ -3944,7 +4057,7 @@ packages:
|
||||
integrity: sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
|
||||
/restore-cursor/3.1.0:
|
||||
dependencies:
|
||||
onetime: 5.1.0
|
||||
onetime: 5.1.1
|
||||
signal-exit: 3.0.3
|
||||
dev: true
|
||||
engines:
|
||||
@ -4449,6 +4562,22 @@ packages:
|
||||
node: '>=0.10.0'
|
||||
resolution:
|
||||
integrity: sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
|
||||
/ts-node/8.10.2_typescript@3.9.7:
|
||||
dependencies:
|
||||
arg: 4.1.3
|
||||
diff: 4.0.2
|
||||
make-error: 1.3.6
|
||||
source-map-support: 0.5.19
|
||||
typescript: 3.9.7
|
||||
yn: 3.1.1
|
||||
dev: true
|
||||
engines:
|
||||
node: '>=6.0.0'
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>=2.7'
|
||||
resolution:
|
||||
integrity: sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==
|
||||
/tsconfig-paths/3.9.0:
|
||||
dependencies:
|
||||
'@types/json5': 0.0.29
|
||||
@ -4539,7 +4668,6 @@ packages:
|
||||
resolution:
|
||||
integrity: sha512-/OyrHCJ8jtzu+QZ+771YaxQ9s4g5Z3XsQE3Ma7q+BL392xxBn4UMvvCdVnqKC2T/dz03/VXSLVKOP3lHmDdc/w==
|
||||
/typescript/3.9.7:
|
||||
dev: true
|
||||
engines:
|
||||
node: '>=4.2.0'
|
||||
hasBin: true
|
||||
@ -4736,3 +4864,9 @@ packages:
|
||||
node: '>=8'
|
||||
resolution:
|
||||
integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
||||
/yn/3.1.1:
|
||||
dev: true
|
||||
engines:
|
||||
node: '>=6'
|
||||
resolution:
|
||||
integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||
|
Loading…
Reference in New Issue
Block a user