diff options
Diffstat (limited to 'packages/taler-integrationtests/src/harness.ts')
-rw-r--r-- | packages/taler-integrationtests/src/harness.ts | 907 |
1 files changed, 907 insertions, 0 deletions
diff --git a/packages/taler-integrationtests/src/harness.ts b/packages/taler-integrationtests/src/harness.ts new file mode 100644 index 000000000..14fa2071d --- /dev/null +++ b/packages/taler-integrationtests/src/harness.ts @@ -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`); + } +} |