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
|
# GNU-style build system
|
||||||
/configure
|
/configure
|
||||||
build-system/config.mk
|
/build-system/config.mk
|
||||||
|
/Makefile
|
||||||
|
|
||||||
# Editor files
|
# Editor files
|
||||||
\#*\#
|
\#*\#
|
||||||
@ -20,3 +21,4 @@ build-scripts/
|
|||||||
|
|
||||||
# Git worktree of pre-built wallet files
|
# Git worktree of pre-built wallet files
|
||||||
prebuilt/
|
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: "",
|
requestUrl: "",
|
||||||
headers,
|
headers,
|
||||||
status: msg.status,
|
status: msg.status,
|
||||||
|
requestMethod: "FIXME",
|
||||||
json: async () => JSON.parse(msg.responseText),
|
json: async () => JSON.parse(msg.responseText),
|
||||||
text: async () => msg.responseText,
|
text: async () => msg.responseText,
|
||||||
};
|
};
|
||||||
|
@ -4,4 +4,4 @@ try {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
require('../dist/taler-wallet-cli.js')
|
require('../dist/taler-wallet-cli.js').walletCli.run();
|
||||||
|
@ -34,6 +34,12 @@ import {
|
|||||||
NodeHttpLib,
|
NodeHttpLib,
|
||||||
} from "taler-wallet-core";
|
} from "taler-wallet-core";
|
||||||
import * as clk from "./clk";
|
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");
|
const logger = new Logger("taler-wallet-cli.ts");
|
||||||
|
|
||||||
@ -109,7 +115,7 @@ function printVersion(): void {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const walletCli = clk
|
export const walletCli = clk
|
||||||
.program("wallet", {
|
.program("wallet", {
|
||||||
help: "Command line interface for the GNU Taler wallet.",
|
help: "Command line interface for the GNU Taler wallet.",
|
||||||
})
|
})
|
||||||
@ -637,4 +643,9 @@ testCli.subcommand("vectors", "vectors").action(async (args) => {
|
|||||||
testvectors.printTestVectors();
|
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/eslint-plugin": "^3.6.1",
|
||||||
"@typescript-eslint/parser": "^3.6.1",
|
"@typescript-eslint/parser": "^3.6.1",
|
||||||
"ava": "^3.10.1",
|
"ava": "^3.10.1",
|
||||||
|
"dts-bundle-generator": "^5.3.0",
|
||||||
"eslint": "^7.4.0",
|
"eslint": "^7.4.0",
|
||||||
"eslint-config-airbnb-typescript": "^8.0.2",
|
"eslint-config-airbnb-typescript": "^8.0.2",
|
||||||
"eslint-plugin-import": "^2.22.0",
|
"eslint-plugin-import": "^2.22.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||||
"eslint-plugin-react": "^7.20.3",
|
"eslint-plugin-react": "^7.20.3",
|
||||||
"eslint-plugin-react-hooks": "^4.0.8",
|
"eslint-plugin-react-hooks": "^4.0.8",
|
||||||
|
"esm": "^3.2.25",
|
||||||
"jed": "^1.1.1",
|
"jed": "^1.1.1",
|
||||||
"moment": "^2.27.0",
|
"moment": "^2.27.0",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"po2json": "^0.4.5",
|
"po2json": "^0.4.5",
|
||||||
"pogen": "workspace:*",
|
"pogen": "workspace:*",
|
||||||
"prettier": "^2.0.5",
|
"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-resolve": "^0.6.0",
|
||||||
"structured-clone": "^0.2.2",
|
"structured-clone": "^0.2.2",
|
||||||
"typedoc": "^0.17.8",
|
"typedoc": "^0.17.8",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^3.9.7"
|
||||||
"rollup": "^2.23.0",
|
|
||||||
"esm": "^3.2.25",
|
|
||||||
"rimraf": "^3.0.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^14.0.27",
|
"@types/node": "^14.0.27",
|
||||||
@ -63,7 +65,9 @@
|
|||||||
"tslib": "^2.0.0"
|
"tslib": "^2.0.0"
|
||||||
},
|
},
|
||||||
"ava": {
|
"ava": {
|
||||||
"require": ["esm"],
|
"require": [
|
||||||
|
"esm"
|
||||||
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"src/**/*-test.*"
|
"src/**/*-test.*"
|
||||||
],
|
],
|
||||||
|
@ -4,13 +4,14 @@ import nodeResolve from "@rollup/plugin-node-resolve";
|
|||||||
import json from "@rollup/plugin-json";
|
import json from "@rollup/plugin-json";
|
||||||
import builtins from "builtin-modules";
|
import builtins from "builtin-modules";
|
||||||
import pkg from "./package.json";
|
import pkg from "./package.json";
|
||||||
|
import sourcemaps from 'rollup-plugin-sourcemaps';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: "lib/index.js",
|
input: "lib/index.js",
|
||||||
output: {
|
output: {
|
||||||
file: pkg.main,
|
file: pkg.main,
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
sourcemap: false,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
external: builtins,
|
external: builtins,
|
||||||
plugins: [
|
plugins: [
|
||||||
@ -18,11 +19,13 @@ export default {
|
|||||||
preferBuiltins: true,
|
preferBuiltins: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
sourcemaps(),
|
||||||
|
|
||||||
commonjs({
|
commonjs({
|
||||||
include: [/node_modules/, /dist/],
|
include: [/node_modules/, /dist/],
|
||||||
extensions: [".js"],
|
extensions: [".js"],
|
||||||
ignoreGlobal: false,
|
ignoreGlobal: false,
|
||||||
sourceMap: false,
|
sourceMap: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
json(),
|
json(),
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of TALER
|
This file is part of GNU Taler
|
||||||
(C) 2016 GNUnet e.V.
|
(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
|
terms of the GNU General Public License as published by the Free Software
|
||||||
Foundation; either version 3, or (at your option) any later version.
|
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
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
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
|
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 * as timer from "../../util/timer";
|
||||||
import { Logger } from "../../util/logging";
|
import { Logger } from "../../util/logging";
|
||||||
|
import { walletCoreApi } from "../..";
|
||||||
|
|
||||||
const logger = new Logger("cryptoApi.ts");
|
const logger = new Logger("cryptoApi.ts");
|
||||||
|
|
||||||
@ -182,7 +183,7 @@ export class CryptoApi {
|
|||||||
};
|
};
|
||||||
this.resetWorkerTimeout(ws);
|
this.resetWorkerTimeout(ws);
|
||||||
work.startTime = timer.performanceNow();
|
work.startTime = timer.performanceNow();
|
||||||
setTimeout(() => worker.postMessage(msg), 0);
|
timer.after(0, () => worker.postMessage(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
resetWorkerTimeout(ws: WorkerState): void {
|
resetWorkerTimeout(ws: WorkerState): void {
|
||||||
@ -198,6 +199,7 @@ export class CryptoApi {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
ws.terminationTimerHandle = timer.after(15 * 1000, destroy);
|
ws.terminationTimerHandle = timer.after(15 * 1000, destroy);
|
||||||
|
//ws.terminationTimerHandle.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWorkerError(ws: WorkerState, e: any): void {
|
handleWorkerError(ws: WorkerState, e: any): void {
|
||||||
|
@ -21,6 +21,9 @@ import { CryptoWorkerFactory } from "./cryptoApi";
|
|||||||
import { CryptoWorker } from "./cryptoWorker";
|
import { CryptoWorker } from "./cryptoWorker";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { CryptoImplementation } from "./cryptoImplementation";
|
import { CryptoImplementation } from "./cryptoImplementation";
|
||||||
|
import { Logger } from "../../util/logging";
|
||||||
|
|
||||||
|
const logger = new Logger("nodeThreadWorker.ts");
|
||||||
|
|
||||||
const f = __filename;
|
const f = __filename;
|
||||||
|
|
||||||
@ -37,16 +40,22 @@ const workerCode = `
|
|||||||
try {
|
try {
|
||||||
tw = require("${f}");
|
tw = require("${f}");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("could not load from ${f}");
|
console.warn("could not load from ${f}");
|
||||||
}
|
}
|
||||||
if (!tw) {
|
if (!tw) {
|
||||||
try {
|
try {
|
||||||
tw = require("taler-wallet-android");
|
tw = require("taler-wallet-android");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("could not load taler-wallet-android either");
|
console.warn("could not load taler-wallet-android either");
|
||||||
throw e;
|
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("message", tw.handleWorkerMessage);
|
||||||
parentPort.on("error", tw.handleWorkerError);
|
parentPort.on("error", tw.handleWorkerError);
|
||||||
`;
|
`;
|
||||||
@ -138,6 +147,9 @@ class NodeThreadCryptoWorker implements CryptoWorker {
|
|||||||
constructor() {
|
constructor() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const worker_threads = require("worker_threads");
|
const worker_threads = require("worker_threads");
|
||||||
|
|
||||||
|
logger.trace("starting node crypto worker");
|
||||||
|
|
||||||
this.nodeWorker = new worker_threads.Worker(workerCode, { eval: true });
|
this.nodeWorker = new worker_threads.Worker(workerCode, { eval: true });
|
||||||
this.nodeWorker.on("error", (err: Error) => {
|
this.nodeWorker.on("error", (err: Error) => {
|
||||||
console.error("error in node worker:", err);
|
console.error("error in node worker:", err);
|
||||||
@ -145,6 +157,9 @@ class NodeThreadCryptoWorker implements CryptoWorker {
|
|||||||
this.onerror(err);
|
this.onerror(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.nodeWorker.on("exit", (err) => {
|
||||||
|
logger.trace(`worker exited with code ${err}`);
|
||||||
|
});
|
||||||
this.nodeWorker.on("message", (v: any) => {
|
this.nodeWorker.on("message", (v: any) => {
|
||||||
if (this.onmessage) {
|
if (this.onmessage) {
|
||||||
this.onmessage(v);
|
this.onmessage(v);
|
||||||
|
@ -45,7 +45,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async req(
|
private async req(
|
||||||
method: "post" | "get",
|
method: "POST" | "GET",
|
||||||
url: string,
|
url: string,
|
||||||
body: any,
|
body: any,
|
||||||
opt?: HttpRequestOptions,
|
opt?: HttpRequestOptions,
|
||||||
@ -72,6 +72,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
{
|
{
|
||||||
httpStatusCode: resp.status,
|
httpStatusCode: resp.status,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
|
requestMethod: method,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -88,6 +89,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
{
|
{
|
||||||
httpStatusCode: resp.status,
|
httpStatusCode: resp.status,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
|
requestMethod: method,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -100,6 +102,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
{
|
{
|
||||||
httpStatusCode: resp.status,
|
httpStatusCode: resp.status,
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
|
requestMethod: method,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -112,6 +115,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
|
requestMethod: method,
|
||||||
headers,
|
headers,
|
||||||
status: resp.status,
|
status: resp.status,
|
||||||
text: async () => resp.data,
|
text: async () => resp.data,
|
||||||
@ -120,7 +124,7 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
|
||||||
return this.req("get", url, undefined, opt);
|
return this.req("GET", url, undefined, opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
async postJson(
|
async postJson(
|
||||||
@ -128,6 +132,6 @@ export class NodeHttpLib implements HttpRequestLibrary {
|
|||||||
body: any,
|
body: any,
|
||||||
opt?: HttpRequestOptions,
|
opt?: HttpRequestOptions,
|
||||||
): Promise<HttpResponse> {
|
): 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 nodeThreadWorker from "./crypto/workers/nodeThreadWorker";
|
||||||
|
|
||||||
export * as walletNotifications from "./types/notifications";
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("updating exchange /keys info");
|
||||||
|
|
||||||
const keysUrl = new URL("keys", baseUrl);
|
const keysUrl = new URL("keys", baseUrl);
|
||||||
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
|
||||||
|
|
||||||
@ -121,6 +123,8 @@ async function updateExchangeWithKeys(
|
|||||||
codecForExchangeKeysJson(),
|
codecForExchangeKeysJson(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logger.info("received /keys response");
|
||||||
|
|
||||||
if (exchangeKeysJson.denoms.length === 0) {
|
if (exchangeKeysJson.denoms.length === 0) {
|
||||||
const opErr = makeErrorDetails(
|
const opErr = makeErrorDetails(
|
||||||
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
|
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
|
||||||
@ -152,12 +156,16 @@ async function updateExchangeWithKeys(
|
|||||||
const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
|
const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
|
||||||
.currency;
|
.currency;
|
||||||
|
|
||||||
|
logger.trace("processing denominations");
|
||||||
|
|
||||||
const newDenominations = await Promise.all(
|
const newDenominations = await Promise.all(
|
||||||
exchangeKeysJson.denoms.map((d) =>
|
exchangeKeysJson.denoms.map((d) =>
|
||||||
denominationRecordFromKeys(ws, baseUrl, d),
|
denominationRecordFromKeys(ws, baseUrl, d),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
logger.trace("done with processing denominations");
|
||||||
|
|
||||||
const lastUpdateTimestamp = getTimestampNow();
|
const lastUpdateTimestamp = getTimestampNow();
|
||||||
|
|
||||||
const recoupGroupId: string | undefined = undefined;
|
const recoupGroupId: string | undefined = undefined;
|
||||||
@ -241,6 +249,8 @@ async function updateExchangeWithKeys(
|
|||||||
console.log("error while recouping coins:", e);
|
console.log("error while recouping coins:", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.trace("done updating exchange /keys");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateExchangeFinalize(
|
async function updateExchangeFinalize(
|
||||||
|
@ -781,7 +781,7 @@ export async function submitPay(
|
|||||||
}
|
}
|
||||||
const sessionId = purchase.lastSessionId;
|
const sessionId = purchase.lastSessionId;
|
||||||
|
|
||||||
console.log("paying with session ID", sessionId);
|
logger.trace("paying with session ID", sessionId);
|
||||||
|
|
||||||
const payUrl = new URL(
|
const payUrl = new URL(
|
||||||
`orders/${purchase.contractData.orderId}/pay`,
|
`orders/${purchase.contractData.orderId}/pay`,
|
||||||
|
@ -712,7 +712,9 @@ export async function getWithdrawalDetailsForUri(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
talerWithdrawUri: string,
|
talerWithdrawUri: string,
|
||||||
): Promise<WithdrawUriInfoResponse> {
|
): Promise<WithdrawUriInfoResponse> {
|
||||||
|
logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
|
||||||
const info = await getBankWithdrawalInfo(ws, talerWithdrawUri);
|
const info = await getBankWithdrawalInfo(ws, talerWithdrawUri);
|
||||||
|
logger.trace(`got bank info`);
|
||||||
if (info.suggestedExchange) {
|
if (info.suggestedExchange) {
|
||||||
// FIXME: right now the exchange gets permanently added,
|
// FIXME: right now the exchange gets permanently added,
|
||||||
// we might want to only temporarily add it.
|
// we might want to only temporarily add it.
|
||||||
|
@ -40,8 +40,11 @@ import {
|
|||||||
codecForString,
|
codecForString,
|
||||||
makeCodecOptional,
|
makeCodecOptional,
|
||||||
Codec,
|
Codec,
|
||||||
|
makeCodecForList,
|
||||||
|
codecForBoolean,
|
||||||
} from "../util/codec";
|
} from "../util/codec";
|
||||||
import { AmountString } from "./talerTypes";
|
import { AmountString } from "./talerTypes";
|
||||||
|
import { codec } from "..";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response for the create reserve request to the wallet.
|
* Response for the create reserve request to the wallet.
|
||||||
@ -164,6 +167,20 @@ export interface BalancesResponse {
|
|||||||
balances: Balance[];
|
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.
|
* For terseness.
|
||||||
*/
|
*/
|
||||||
|
@ -34,6 +34,7 @@ const logger = new Logger("http.ts");
|
|||||||
*/
|
*/
|
||||||
export interface HttpResponse {
|
export interface HttpResponse {
|
||||||
requestUrl: string;
|
requestUrl: string;
|
||||||
|
requestMethod: string;
|
||||||
status: number;
|
status: number;
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
json(): Promise<any>;
|
json(): Promise<any>;
|
||||||
@ -118,6 +119,8 @@ export async function readSuccessResponseJsonOrErrorCode<T>(
|
|||||||
"Error response did not contain error code",
|
"Error response did not contain error code",
|
||||||
{
|
{
|
||||||
requestUrl: httpResponse.requestUrl,
|
requestUrl: httpResponse.requestUrl,
|
||||||
|
requestMethod: httpResponse.requestMethod,
|
||||||
|
httpStatusCode: httpResponse.status,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -188,7 +191,9 @@ export async function readSuccessResponseTextOrErrorCode<T>(
|
|||||||
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
"Error response did not contain error code",
|
"Error response did not contain error code",
|
||||||
{
|
{
|
||||||
|
httpStatusCode: httpResponse.status,
|
||||||
requestUrl: httpResponse.requestUrl,
|
requestUrl: httpResponse.requestUrl,
|
||||||
|
requestMethod: httpResponse.requestMethod,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -217,7 +222,9 @@ export async function checkSuccessResponseOrThrow(
|
|||||||
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||||
"Error response did not contain error code",
|
"Error response did not contain error code",
|
||||||
{
|
{
|
||||||
|
httpStatusCode: httpResponse.status,
|
||||||
requestUrl: httpResponse.requestUrl,
|
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 { AmountJson } from "./amounts";
|
||||||
import * as Amounts from "./amounts";
|
import * as Amounts from "./amounts";
|
||||||
|
import fs from "fs";
|
||||||
|
import { acceptExchangeTermsOfService } from "../operations/exchanges";
|
||||||
|
|
||||||
export class ConfigError extends Error {
|
export class ConfigError extends Error {
|
||||||
constructor(message: string) {
|
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 {
|
export class Configuration {
|
||||||
private sectionMap: SectionMap = {};
|
private sectionMap: SectionMap = {};
|
||||||
|
|
||||||
@ -69,7 +154,6 @@ export class Configuration {
|
|||||||
|
|
||||||
const lines = s.split("\n");
|
const lines = s.split("\n");
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
console.log("parsing line", JSON.stringify(line));
|
|
||||||
if (reEmptyLine.test(line)) {
|
if (reEmptyLine.test(line)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -79,15 +163,15 @@ export class Configuration {
|
|||||||
const secMatch = line.match(reSection);
|
const secMatch = line.match(reSection);
|
||||||
if (secMatch) {
|
if (secMatch) {
|
||||||
currentSection = secMatch[1];
|
currentSection = secMatch[1];
|
||||||
console.log("setting section to", currentSection);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (currentSection === undefined) {
|
if (currentSection === undefined) {
|
||||||
throw Error("invalid configuration, expected section header");
|
throw Error("invalid configuration, expected section header");
|
||||||
}
|
}
|
||||||
|
currentSection = currentSection.toUpperCase();
|
||||||
const paramMatch = line.match(reParam);
|
const paramMatch = line.match(reParam);
|
||||||
if (paramMatch) {
|
if (paramMatch) {
|
||||||
const optName = paramMatch[1];
|
const optName = paramMatch[1].toUpperCase();
|
||||||
let val = paramMatch[2];
|
let val = paramMatch[2];
|
||||||
if (val.startsWith('"') && val.endsWith('"')) {
|
if (val.startsWith('"') && val.endsWith('"')) {
|
||||||
val = val.slice(1, val.length - 1);
|
val = val.slice(1, val.length - 1);
|
||||||
@ -102,13 +186,44 @@ export class Configuration {
|
|||||||
"invalid configuration, expected section header or option assignment",
|
"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> {
|
getString(section: string, option: string): ConfigValue<string> {
|
||||||
const val = (this.sectionMap[section] ?? {})[option];
|
const secNorm = section.toUpperCase();
|
||||||
return new ConfigValue(section, option, val, (x) => x);
|
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> {
|
getAmount(section: string, option: string): ConfigValue<AmountJson> {
|
||||||
@ -117,4 +232,28 @@ export class Configuration {
|
|||||||
Amounts.parseOrThrow(x),
|
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 {
|
export interface TimerHandle {
|
||||||
clear(): void;
|
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 {
|
class IntervalHandle {
|
||||||
@ -42,6 +48,16 @@ class IntervalHandle {
|
|||||||
clear(): void {
|
clear(): void {
|
||||||
clearInterval(this.h);
|
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 {
|
class TimeoutHandle {
|
||||||
@ -50,6 +66,16 @@ class TimeoutHandle {
|
|||||||
clear(): void {
|
clear(): void {
|
||||||
clearTimeout(this.h);
|
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
|
// do nothing
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
unref() {
|
||||||
|
// do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -141,6 +171,9 @@ export class TimerGroup {
|
|||||||
h.clear();
|
h.clear();
|
||||||
delete tm[myId];
|
delete tm[myId];
|
||||||
},
|
},
|
||||||
|
unref() {
|
||||||
|
h.unref();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +193,9 @@ export class TimerGroup {
|
|||||||
h.clear();
|
h.clear();
|
||||||
delete tm[myId];
|
delete tm[myId];
|
||||||
},
|
},
|
||||||
|
unref() {
|
||||||
|
h.unref();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,7 @@ export class BrowserHttpLib implements httpLib.HttpRequestLibrary {
|
|||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
status: myRequest.status,
|
status: myRequest.status,
|
||||||
headers: headerMap,
|
headers: headerMap,
|
||||||
|
requestMethod: method,
|
||||||
json: makeJson,
|
json: makeJson,
|
||||||
text: async () => myRequest.responseText,
|
text: async () => myRequest.responseText,
|
||||||
};
|
};
|
||||||
@ -112,7 +113,7 @@ export class BrowserHttpLib implements httpLib.HttpRequestLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(url: string, opt?: httpLib.HttpRequestOptions): Promise<httpLib.HttpResponse> {
|
get(url: string, opt?: httpLib.HttpRequestOptions): Promise<httpLib.HttpResponse> {
|
||||||
return this.req("get", url, undefined, opt);
|
return this.req("GET", url, undefined, opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
postJson(
|
postJson(
|
||||||
@ -120,7 +121,7 @@ export class BrowserHttpLib implements httpLib.HttpRequestLibrary {
|
|||||||
body: unknown,
|
body: unknown,
|
||||||
opt?: httpLib.HttpRequestOptions,
|
opt?: httpLib.HttpRequestOptions,
|
||||||
): Promise<httpLib.HttpResponse> {
|
): Promise<httpLib.HttpResponse> {
|
||||||
return this.req("post", url, JSON.stringify(body), opt);
|
return this.req("POST", url, JSON.stringify(body), opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
|
142
pnpm-lock.yaml
142
pnpm-lock.yaml
@ -29,6 +29,28 @@ importers:
|
|||||||
specifiers:
|
specifiers:
|
||||||
'@types/node': ^11.12.0
|
'@types/node': ^11.12.0
|
||||||
typescript: ^3.3.4000
|
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:
|
packages/taler-wallet-android:
|
||||||
dependencies:
|
dependencies:
|
||||||
taler-wallet-core: 'link:../taler-wallet-core'
|
taler-wallet-core: 'link:../taler-wallet-core'
|
||||||
@ -100,6 +122,7 @@ importers:
|
|||||||
'@typescript-eslint/eslint-plugin': 3.7.1_98f5354ad0bbc327ab4925c12674a6b1
|
'@typescript-eslint/eslint-plugin': 3.7.1_98f5354ad0bbc327ab4925c12674a6b1
|
||||||
'@typescript-eslint/parser': 3.7.1_eslint@7.6.0+typescript@3.9.7
|
'@typescript-eslint/parser': 3.7.1_eslint@7.6.0+typescript@3.9.7
|
||||||
ava: 3.11.0
|
ava: 3.11.0
|
||||||
|
dts-bundle-generator: 5.3.0
|
||||||
eslint: 7.6.0
|
eslint: 7.6.0
|
||||||
eslint-config-airbnb-typescript: 8.0.2_de36c6f68d63a4142de06a31bab9d790
|
eslint-config-airbnb-typescript: 8.0.2_de36c6f68d63a4142de06a31bab9d790
|
||||||
eslint-plugin-import: 2.22.0_eslint@7.6.0
|
eslint-plugin-import: 2.22.0_eslint@7.6.0
|
||||||
@ -115,6 +138,7 @@ importers:
|
|||||||
prettier: 2.0.5
|
prettier: 2.0.5
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
rollup: 2.23.0
|
rollup: 2.23.0
|
||||||
|
rollup-plugin-sourcemaps: 0.6.2_1bb4f16ce5b550396581a296af208cfa
|
||||||
source-map-resolve: 0.6.0
|
source-map-resolve: 0.6.0
|
||||||
structured-clone: 0.2.2
|
structured-clone: 0.2.2
|
||||||
typedoc: 0.17.8_typescript@3.9.7
|
typedoc: 0.17.8_typescript@3.9.7
|
||||||
@ -127,6 +151,7 @@ importers:
|
|||||||
ava: ^3.10.1
|
ava: ^3.10.1
|
||||||
axios: ^0.19.2
|
axios: ^0.19.2
|
||||||
big-integer: ^1.6.48
|
big-integer: ^1.6.48
|
||||||
|
dts-bundle-generator: ^5.3.0
|
||||||
eslint: ^7.4.0
|
eslint: ^7.4.0
|
||||||
eslint-config-airbnb-typescript: ^8.0.2
|
eslint-config-airbnb-typescript: ^8.0.2
|
||||||
eslint-plugin-import: ^2.22.0
|
eslint-plugin-import: ^2.22.0
|
||||||
@ -143,6 +168,7 @@ importers:
|
|||||||
prettier: ^2.0.5
|
prettier: ^2.0.5
|
||||||
rimraf: ^3.0.2
|
rimraf: ^3.0.2
|
||||||
rollup: ^2.23.0
|
rollup: ^2.23.0
|
||||||
|
rollup-plugin-sourcemaps: ^0.6.2
|
||||||
source-map-resolve: ^0.6.0
|
source-map-resolve: ^0.6.0
|
||||||
source-map-support: ^0.5.19
|
source-map-support: ^0.5.19
|
||||||
structured-clone: ^0.2.2
|
structured-clone: ^0.2.2
|
||||||
@ -861,6 +887,10 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
|
integrity: sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=
|
||||||
|
/arg/4.1.3:
|
||||||
|
dev: true
|
||||||
|
resolution:
|
||||||
|
integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||||
/argparse/1.0.10:
|
/argparse/1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
sprintf-js: 1.0.3
|
sprintf-js: 1.0.3
|
||||||
@ -1032,6 +1062,69 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-y5U8BGeSRjs/OypsC4CJxr+L1KtLKU5kUyHr5hcghXn7HNr2f4LE/4gvl0Q5lNkLX1obdRW1oODphNdU/glwmA==
|
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:
|
/axe-core/3.5.5:
|
||||||
dev: true
|
dev: true
|
||||||
engines:
|
engines:
|
||||||
@ -1541,6 +1634,12 @@ packages:
|
|||||||
node: '>=8'
|
node: '>=8'
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==
|
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:
|
/dir-glob/3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-type: 4.0.0
|
path-type: 4.0.0
|
||||||
@ -1617,6 +1716,16 @@ packages:
|
|||||||
node: '>=8'
|
node: '>=8'
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==
|
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:
|
/duplexer3/0.1.4:
|
||||||
dev: true
|
dev: true
|
||||||
resolution:
|
resolution:
|
||||||
@ -3070,6 +3179,10 @@ packages:
|
|||||||
node: '>=8'
|
node: '>=8'
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
|
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:
|
/map-age-cleaner/0.1.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-defer: 1.0.0
|
p-defer: 1.0.0
|
||||||
@ -3353,14 +3466,14 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||||
/onetime/5.1.0:
|
/onetime/5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
mimic-fn: 2.1.0
|
mimic-fn: 2.1.0
|
||||||
dev: true
|
dev: true
|
||||||
engines:
|
engines:
|
||||||
node: '>=6'
|
node: '>=6'
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==
|
integrity: sha512-ZpZpjcJeugQfWsfyQlshVoowIIQ1qBGSVll4rfDq6JJVO//fesjoX808hXWfBjY+ROZgpKDI5TRSRBSoJiZ8eg==
|
||||||
/optionator/0.9.1:
|
/optionator/0.9.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.3
|
deep-is: 0.1.3
|
||||||
@ -3944,7 +4057,7 @@ packages:
|
|||||||
integrity: sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
|
integrity: sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
|
||||||
/restore-cursor/3.1.0:
|
/restore-cursor/3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
onetime: 5.1.0
|
onetime: 5.1.1
|
||||||
signal-exit: 3.0.3
|
signal-exit: 3.0.3
|
||||||
dev: true
|
dev: true
|
||||||
engines:
|
engines:
|
||||||
@ -4449,6 +4562,22 @@ packages:
|
|||||||
node: '>=0.10.0'
|
node: '>=0.10.0'
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
|
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:
|
/tsconfig-paths/3.9.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/json5': 0.0.29
|
'@types/json5': 0.0.29
|
||||||
@ -4539,7 +4668,6 @@ packages:
|
|||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-/OyrHCJ8jtzu+QZ+771YaxQ9s4g5Z3XsQE3Ma7q+BL392xxBn4UMvvCdVnqKC2T/dz03/VXSLVKOP3lHmDdc/w==
|
integrity: sha512-/OyrHCJ8jtzu+QZ+771YaxQ9s4g5Z3XsQE3Ma7q+BL392xxBn4UMvvCdVnqKC2T/dz03/VXSLVKOP3lHmDdc/w==
|
||||||
/typescript/3.9.7:
|
/typescript/3.9.7:
|
||||||
dev: true
|
|
||||||
engines:
|
engines:
|
||||||
node: '>=4.2.0'
|
node: '>=4.2.0'
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -4736,3 +4864,9 @@ packages:
|
|||||||
node: '>=8'
|
node: '>=8'
|
||||||
resolution:
|
resolution:
|
||||||
integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
|
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