diff options
Diffstat (limited to 'packages/taler-wallet-cli/src')
76 files changed, 1267 insertions, 15818 deletions
diff --git a/packages/taler-wallet-cli/src/assets.ts b/packages/taler-wallet-cli/src/assets.ts deleted file mode 100644 index 72fc9fe04..000000000 --- a/packages/taler-wallet-cli/src/assets.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import path from "path"; -import fs from "fs"; - -/** - * Resolve an asset name into an absolute filename. - * - * The asset file should be placed in the "assets" directory - * at the top level of the package (i.e. next to package.json). - */ -export function resolveAsset(name: string): string { - const n = __filename; - const d = __dirname; - let assetPath: string; - // Currently both asset paths are the same. - // This might change if the file that contains "resolveAsset" - // ever moves. Thus, we're keeping the case distinction. - if (n.endsWith("assets.js")) { - // We're not bundled. Path is relative to the current file. - assetPath = path.resolve(path.join(d, "..", "assets", name)); - } else if (n.endsWith("taler-wallet-cli.js")) { - // We're bundled. Currently, this path is the same - // FIXME: Take into account some ASSETS environment variable? - assetPath = path.resolve(path.join(d, "..", "assets", name)); - } else { - throw Error("Can't resolve asset (unknown)"); - } - if (!fs.existsSync(assetPath)) { - throw Error(`Asset '${name} not found'`); - } - return assetPath; -} diff --git a/packages/taler-wallet-cli/src/bench1.ts b/packages/taler-wallet-cli/src/bench1.ts deleted file mode 100644 index 4a2651f36..000000000 --- a/packages/taler-wallet-cli/src/bench1.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - buildCodecForObject, - codecForNumber, - codecForString, - codecOptional, -} from "@gnu-taler/taler-util"; -import { - getDefaultNodeWallet, - NodeHttpLib, - WalletApiOperation, -} from "@gnu-taler/taler-wallet-core"; - -/** - * Entry point for the benchmark. - * - * The benchmark runs against an existing Taler deployment and does not - * set up its own services. - */ -export async function runBench1(configJson: any): Promise<void> { - // Validate the configuration file for this benchmark. - const b1conf = codecForBench1Config().decode(configJson); - - const myHttpLib = new NodeHttpLib(); - const wallet = await getDefaultNodeWallet({ - // No persistent DB storage. - persistentStoragePath: undefined, - httpLib: myHttpLib, - }); - await wallet.client.call(WalletApiOperation.InitWallet, {}); - - const numIter = b1conf.iterations ?? 1; - - for (let i = 0; i < numIter; i++) { - await wallet.client.call(WalletApiOperation.WithdrawFakebank, { - amount: "TESTKUDOS:10", - bank: b1conf.bank, - exchange: b1conf.exchange, - }); - - await wallet.runTaskLoop({ - stopWhenDone: true, - }); - - await wallet.client.call(WalletApiOperation.CreateDepositGroup, { - amount: "TESTKUDOS:5", - depositPaytoUri: "payto://x-taler-bank/localhost/foo", - }); - - await wallet.runTaskLoop({ - stopWhenDone: true, - }); - } - - wallet.stop(); -} - -/** - * Format of the configuration file passed to the benchmark - */ -interface Bench1Config { - /** - * Base URL of the bank. - */ - bank: string; - - /** - * Base URL of the exchange. - */ - exchange: string; - - /** - * How many withdraw/deposit iterations should be made? - * Defaults to 1. - */ - iterations?: number; -} - -/** - * Schema validation codec for Bench1Config. - */ -const codecForBench1Config = () => - buildCodecForObject<Bench1Config>() - .property("bank", codecForString()) - .property("exchange", codecForString()) - .property("iterations", codecOptional(codecForNumber())) - .build("Bench1Config"); diff --git a/packages/taler-wallet-cli/src/clk.ts b/packages/taler-wallet-cli/src/clk.ts deleted file mode 100644 index ca6dcc1a4..000000000 --- a/packages/taler-wallet-cli/src/clk.ts +++ /dev/null @@ -1,614 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - 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 process from "process"; -import path from "path"; -import readline from "readline"; - -class Converter<T> {} - -export const INT = new Converter<number>(); -export const STRING: Converter<string> = new Converter<string>(); - -export interface OptionArgs<T> { - help?: string; - default?: T; - onPresentHandler?: (v: T) => void; -} - -export interface ArgumentArgs<T> { - metavar?: string; - help?: string; - default?: T; -} - -export interface SubcommandArgs { - help?: string; -} - -export interface FlagArgs { - help?: string; -} - -export interface ProgramArgs { - help?: string; -} - -interface ArgumentDef { - name: string; - conv: Converter<any>; - args: ArgumentArgs<any>; - required: boolean; -} - -interface SubcommandDef { - commandGroup: CommandGroup<any, any>; - name: string; - args: SubcommandArgs; -} - -type ActionFn<TG> = (x: TG) => void; - -type SubRecord<S extends keyof any, N extends keyof any, V> = { - [Y in S]: { [X in N]: V }; -}; - -interface OptionDef { - name: string; - flagspec: string[]; - /** - * Converter, only present for options, not for flags. - */ - conv?: Converter<any>; - args: OptionArgs<any>; - isFlag: boolean; - required: boolean; -} - -function splitOpt(opt: string): { key: string; value?: string } { - const idx = opt.indexOf("="); - if (idx == -1) { - return { key: opt }; - } - return { key: opt.substring(0, idx), value: opt.substring(idx + 1) }; -} - -function formatListing(key: string, value?: string): string { - const res = " " + key; - if (!value) { - return res; - } - if (res.length >= 25) { - return res + "\n" + " " + value; - } else { - return res.padEnd(24) + " " + value; - } -} - -export class CommandGroup<GN extends keyof any, TG> { - private shortOptions: { [name: string]: OptionDef } = {}; - private longOptions: { [name: string]: OptionDef } = {}; - private subcommandMap: { [name: string]: SubcommandDef } = {}; - private subcommands: SubcommandDef[] = []; - private options: OptionDef[] = []; - private arguments: ArgumentDef[] = []; - - private myAction?: ActionFn<TG>; - - constructor( - private argKey: string, - private name: string | null, - private scArgs: SubcommandArgs, - ) {} - - action(f: ActionFn<TG>): void { - if (this.myAction) { - throw Error("only one action supported per command"); - } - this.myAction = f; - } - - requiredOption<N extends keyof any, V>( - name: N, - flagspec: string[], - conv: Converter<V>, - args: OptionArgs<V> = {}, - ): CommandGroup<GN, TG & SubRecord<GN, N, V>> { - const def: OptionDef = { - args: args, - conv: conv, - flagspec: flagspec, - isFlag: false, - required: true, - name: name as string, - }; - this.options.push(def); - for (const flag of flagspec) { - if (flag.startsWith("--")) { - const flagname = flag.substring(2); - this.longOptions[flagname] = def; - } else if (flag.startsWith("-")) { - const flagname = flag.substring(1); - this.shortOptions[flagname] = def; - } else { - throw Error("option must start with '-' or '--'"); - } - } - return this as any; - } - - maybeOption<N extends keyof any, V>( - name: N, - flagspec: string[], - conv: Converter<V>, - args: OptionArgs<V> = {}, - ): CommandGroup<GN, TG & SubRecord<GN, N, V | undefined>> { - const def: OptionDef = { - args: args, - conv: conv, - flagspec: flagspec, - isFlag: false, - required: false, - name: name as string, - }; - this.options.push(def); - for (const flag of flagspec) { - if (flag.startsWith("--")) { - const flagname = flag.substring(2); - this.longOptions[flagname] = def; - } else if (flag.startsWith("-")) { - const flagname = flag.substring(1); - this.shortOptions[flagname] = def; - } else { - throw Error("option must start with '-' or '--'"); - } - } - return this as any; - } - - requiredArgument<N extends keyof any, V>( - name: N, - conv: Converter<V>, - args: ArgumentArgs<V> = {}, - ): CommandGroup<GN, TG & SubRecord<GN, N, V>> { - const argDef: ArgumentDef = { - args: args, - conv: conv, - name: name as string, - required: true, - }; - this.arguments.push(argDef); - return this as any; - } - - maybeArgument<N extends keyof any, V>( - name: N, - conv: Converter<V>, - args: ArgumentArgs<V> = {}, - ): CommandGroup<GN, TG & SubRecord<GN, N, V | undefined>> { - const argDef: ArgumentDef = { - args: args, - conv: conv, - name: name as string, - required: false, - }; - this.arguments.push(argDef); - return this as any; - } - - flag<N extends string, V>( - name: N, - flagspec: string[], - args: OptionArgs<V> = {}, - ): CommandGroup<GN, TG & SubRecord<GN, N, boolean>> { - const def: OptionDef = { - args: args, - flagspec: flagspec, - isFlag: true, - required: false, - name: name as string, - }; - this.options.push(def); - for (const flag of flagspec) { - if (flag.startsWith("--")) { - const flagname = flag.substring(2); - this.longOptions[flagname] = def; - } else if (flag.startsWith("-")) { - const flagname = flag.substring(1); - this.shortOptions[flagname] = def; - } else { - throw Error("option must start with '-' or '--'"); - } - } - return this as any; - } - - subcommand<GN extends keyof any>( - argKey: GN, - name: string, - args: SubcommandArgs = {}, - ): CommandGroup<GN, TG> { - const cg = new CommandGroup<GN, {}>(argKey as string, name, args); - const def: SubcommandDef = { - commandGroup: cg, - name: name as string, - args: args, - }; - cg.flag("help", ["-h", "--help"], { - help: "Show this message and exit.", - }); - this.subcommandMap[name as string] = def; - this.subcommands.push(def); - this.subcommands = this.subcommands.sort((x1, x2) => { - const a = x1.name; - const b = x2.name; - if (a === b) { - return 0; - } else if (a < b) { - return -1; - } else { - return 1; - } - }); - return cg as any; - } - - printHelp(progName: string, parents: CommandGroup<any, any>[]): void { - let usageSpec = ""; - for (const p of parents) { - usageSpec += (p.name ?? progName) + " "; - if (p.arguments.length >= 1) { - usageSpec += "<ARGS...> "; - } - } - usageSpec += (this.name ?? progName) + " "; - if (this.subcommands.length != 0) { - usageSpec += "COMMAND "; - } - for (const a of this.arguments) { - const argName = a.args.metavar ?? a.name; - usageSpec += `<${argName}> `; - } - usageSpec = usageSpec.trimRight(); - console.log(`Usage: ${usageSpec}`); - if (this.scArgs.help) { - console.log(); - console.log(this.scArgs.help); - } - if (this.options.length != 0) { - console.log(); - console.log("Options:"); - for (const opt of this.options) { - let optSpec = opt.flagspec.join(", "); - if (!opt.isFlag) { - optSpec = optSpec + "=VALUE"; - } - console.log(formatListing(optSpec, opt.args.help)); - } - } - - if (this.subcommands.length != 0) { - console.log(); - console.log("Commands:"); - for (const subcmd of this.subcommands) { - console.log(formatListing(subcmd.name, subcmd.args.help)); - } - } - } - - /** - * Run the (sub-)command with the given command line parameters. - */ - run( - progname: string, - parents: CommandGroup<any, any>[], - unparsedArgs: string[], - parsedArgs: any, - ): void { - let posArgIndex = 0; - let argsTerminated = false; - let i; - let foundSubcommand: CommandGroup<any, any> | undefined = undefined; - const myArgs: any = (parsedArgs[this.argKey] = {}); - const foundOptions: { [name: string]: boolean } = {}; - const currentName = this.name ?? progname; - for (i = 0; i < unparsedArgs.length; i++) { - const argVal = unparsedArgs[i]; - if (argsTerminated == false) { - if (argVal === "--") { - argsTerminated = true; - continue; - } - if (argVal.startsWith("--")) { - const opt = argVal.substring(2); - const r = splitOpt(opt); - const d = this.longOptions[r.key]; - if (!d) { - console.error( - `error: unknown option '--${r.key}' for ${currentName}`, - ); - process.exit(-1); - throw Error("not reached"); - } - if (d.isFlag) { - if (r.value !== undefined) { - console.error(`error: flag '--${r.key}' does not take a value`); - process.exit(-1); - throw Error("not reached"); - } - foundOptions[d.name] = true; - myArgs[d.name] = true; - } else { - if (r.value === undefined) { - if (i === unparsedArgs.length - 1) { - console.error(`error: option '--${r.key}' needs an argument`); - process.exit(-1); - throw Error("not reached"); - } - myArgs[d.name] = unparsedArgs[i + 1]; - i++; - } else { - myArgs[d.name] = r.value; - } - foundOptions[d.name] = true; - } - continue; - } - if (argVal.startsWith("-") && argVal != "-") { - const optShort = argVal.substring(1); - for (let si = 0; si < optShort.length; si++) { - const chr = optShort[si]; - const opt = this.shortOptions[chr]; - if (!opt) { - console.error(`error: option '-${chr}' not known`); - process.exit(-1); - } - if (opt.isFlag) { - myArgs[opt.name] = true; - foundOptions[opt.name] = true; - } else { - if (si == optShort.length - 1) { - if (i === unparsedArgs.length - 1) { - console.error(`error: option '-${chr}' needs an argument`); - process.exit(-1); - throw Error("not reached"); - } else { - myArgs[opt.name] = unparsedArgs[i + 1]; - i++; - } - } else { - myArgs[opt.name] = optShort.substring(si + 1); - } - foundOptions[opt.name] = true; - break; - } - } - continue; - } - } - if (this.subcommands.length != 0) { - const subcmd = this.subcommandMap[argVal]; - if (!subcmd) { - console.error(`error: unknown command '${argVal}'`); - process.exit(-1); - throw Error("not reached"); - } - foundSubcommand = subcmd.commandGroup; - break; - } else { - const d = this.arguments[posArgIndex]; - if (!d) { - console.error(`error: too many arguments for ${currentName}`); - process.exit(-1); - throw Error("not reached"); - } - myArgs[d.name] = unparsedArgs[i]; - posArgIndex++; - } - } - - if (parsedArgs[this.argKey].help) { - this.printHelp(progname, parents); - process.exit(0); - throw Error("not reached"); - } - - for (let i = posArgIndex; i < this.arguments.length; i++) { - const d = this.arguments[i]; - if (d.required) { - if (d.args.default !== undefined) { - myArgs[d.name] = d.args.default; - } else { - console.error( - `error: missing positional argument '${d.name}' for ${currentName}`, - ); - process.exit(-1); - throw Error("not reached"); - } - } - } - - for (const option of this.options) { - if (option.isFlag == false && option.required == true) { - if (!foundOptions[option.name]) { - if (option.args.default !== undefined) { - myArgs[option.name] = option.args.default; - } else { - const name = option.flagspec.join(","); - console.error(`error: missing option '${name}'`); - process.exit(-1); - throw Error("not reached"); - } - } - } - } - - for (const option of this.options) { - const ph = option.args.onPresentHandler; - if (ph && foundOptions[option.name]) { - ph(myArgs[option.name]); - } - } - - if (foundSubcommand) { - foundSubcommand.run( - progname, - Array.prototype.concat(parents, [this]), - unparsedArgs.slice(i + 1), - parsedArgs, - ); - } else if (this.myAction) { - let r; - try { - r = this.myAction(parsedArgs); - } catch (e) { - console.error(`An error occurred while running ${currentName}`); - console.error(e); - process.exit(1); - } - Promise.resolve(r).catch((e) => { - console.error(`An error occurred while running ${currentName}`); - console.error(e); - process.exit(1); - }); - } else { - this.printHelp(progname, parents); - process.exit(-1); - throw Error("not reached"); - } - } -} - -export class Program<PN extends keyof any, T> { - private mainCommand: CommandGroup<any, any>; - - constructor(argKey: string, args: ProgramArgs = {}) { - this.mainCommand = new CommandGroup<any, any>(argKey, null, { - help: args.help, - }); - this.mainCommand.flag("help", ["-h", "--help"], { - help: "Show this message and exit.", - }); - } - - run(): void { - const args = process.argv; - if (args.length < 2) { - console.error( - "Error while parsing command line arguments: not enough arguments", - ); - process.exit(-1); - } - const progname = path.basename(args[1]); - const rest = args.slice(2); - - this.mainCommand.run(progname, [], rest, {}); - } - - subcommand<GN extends keyof any>( - argKey: GN, - name: string, - args: SubcommandArgs = {}, - ): CommandGroup<GN, T> { - const cmd = this.mainCommand.subcommand(argKey, name as string, args); - return cmd as any; - } - - requiredOption<N extends keyof any, V>( - name: N, - flagspec: string[], - conv: Converter<V>, - args: OptionArgs<V> = {}, - ): Program<PN, T & SubRecord<PN, N, V>> { - this.mainCommand.requiredOption(name, flagspec, conv, args); - return this as any; - } - - maybeOption<N extends keyof any, V>( - name: N, - flagspec: string[], - conv: Converter<V>, - args: OptionArgs<V> = {}, - ): Program<PN, T & SubRecord<PN, N, V | undefined>> { - this.mainCommand.maybeOption(name, flagspec, conv, args); - return this as any; - } - - /** - * Add a flag (option without value) to the program. - */ - flag<N extends string>( - name: N, - flagspec: string[], - args: OptionArgs<boolean> = {}, - ): Program<PN, T & SubRecord<PN, N, boolean>> { - this.mainCommand.flag(name, flagspec, args); - return this as any; - } - - /** - * Add a required positional argument to the program. - */ - requiredArgument<N extends keyof any, V>( - name: N, - conv: Converter<V>, - args: ArgumentArgs<V> = {}, - ): Program<N, T & SubRecord<PN, N, V>> { - this.mainCommand.requiredArgument(name, conv, args); - return this as any; - } - - /** - * Add an optional argument to the program. - */ - maybeArgument<N extends keyof any, V>( - name: N, - conv: Converter<V>, - args: ArgumentArgs<V> = {}, - ): Program<N, T & SubRecord<PN, N, V | undefined>> { - this.mainCommand.maybeArgument(name, conv, args); - return this as any; - } -} - -export type GetArgType<T> = T extends Program<any, infer AT> - ? AT - : T extends CommandGroup<any, infer AT> - ? AT - : any; - -export function program<PN extends keyof any>( - argKey: PN, - args: ProgramArgs = {}, -): Program<PN, {}> { - return new Program(argKey as string, args); -} - -export function prompt(question: string): Promise<string> { - const stdinReadline = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - return new Promise<string>((resolve, reject) => { - stdinReadline.question(question, (res) => { - resolve(res); - stdinReadline.close(); - }); - }); -} diff --git a/packages/taler-wallet-cli/src/env1.ts b/packages/taler-wallet-cli/src/env1.ts deleted file mode 100644 index eb7da352c..000000000 --- a/packages/taler-wallet-cli/src/env1.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { URL } from "@gnu-taler/taler-util"; -import { CoinConfig, defaultCoinConfig } from "./harness/denomStructures.js"; -import { - GlobalTestState, - setupDb, - FakeBankService, - ExchangeService, -} from "./harness/harness.js"; - -/** - * Entry point for the benchmark. - * - * The benchmark runs against an existing Taler deployment and does not - * set up its own services. - */ -export async function runEnv1(t: GlobalTestState): Promise<void> { - const db = await setupDb(t); - - const bank = await FakeBankService.create(t, { - currency: "TESTKUDOS", - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - exchange.addBankAccount("1", { - accountName: "exchange", - accountPassword: "x", - wireGatewayApiBaseUrl: new URL("/exchange/", bank.baseUrl).href, - accountPaytoUri: "payto://x-taler-bank/localhost/exchange", - }); - - await bank.start(); - - await bank.pingUntilAvailable(); - - const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); - exchange.addCoinConfigList(coinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - console.log("setup done!"); -} diff --git a/packages/taler-wallet-cli/src/harness/denomStructures.ts b/packages/taler-wallet-cli/src/harness/denomStructures.ts deleted file mode 100644 index 5ab9aca00..000000000 --- a/packages/taler-wallet-cli/src/harness/denomStructures.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -export interface CoinConfig { - name: string; - value: string; - durationWithdraw: string; - durationSpend: string; - durationLegal: string; - feeWithdraw: string; - feeDeposit: string; - feeRefresh: string; - feeRefund: string; - rsaKeySize: number; -} - -const coinCommon = { - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, -}; - -export const coin_ct1 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_ct1`, - value: `${curr}:0.01`, - feeDeposit: `${curr}:0.00`, - feeRefresh: `${curr}:0.01`, - feeRefund: `${curr}:0.00`, - feeWithdraw: `${curr}:0.01`, -}); - -export const coin_ct10 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_ct10`, - value: `${curr}:0.10`, - feeDeposit: `${curr}:0.01`, - feeRefresh: `${curr}:0.01`, - feeRefund: `${curr}:0.00`, - feeWithdraw: `${curr}:0.01`, -}); - -export const coin_u1 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u1`, - value: `${curr}:1`, - feeDeposit: `${curr}:0.02`, - feeRefresh: `${curr}:0.02`, - feeRefund: `${curr}:0.02`, - feeWithdraw: `${curr}:0.02`, -}); - -export const coin_u2 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u2`, - value: `${curr}:2`, - feeDeposit: `${curr}:0.02`, - feeRefresh: `${curr}:0.02`, - feeRefund: `${curr}:0.02`, - feeWithdraw: `${curr}:0.02`, -}); - -export const coin_u4 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u4`, - value: `${curr}:4`, - feeDeposit: `${curr}:0.02`, - feeRefresh: `${curr}:0.02`, - feeRefund: `${curr}:0.02`, - feeWithdraw: `${curr}:0.02`, -}); - -export const coin_u8 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u8`, - value: `${curr}:8`, - feeDeposit: `${curr}:0.16`, - feeRefresh: `${curr}:0.16`, - feeRefund: `${curr}:0.16`, - feeWithdraw: `${curr}:0.16`, -}); - -const coin_u10 = (curr: string): CoinConfig => ({ - ...coinCommon, - name: `${curr}_u10`, - value: `${curr}:10`, - feeDeposit: `${curr}:0.2`, - feeRefresh: `${curr}:0.2`, - feeRefund: `${curr}:0.2`, - feeWithdraw: `${curr}:0.2`, -}); - -export const defaultCoinConfig = [ - coin_ct1, - coin_ct10, - coin_u1, - coin_u2, - coin_u4, - coin_u8, - coin_u10, -]; - -const coinCheapCommon = (curr: string) => ({ - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, - feeRefresh: `${curr}:0.2`, - feeRefund: `${curr}:0.2`, - feeWithdraw: `${curr}:0.2`, -}); - -export function makeNoFeeCoinConfig(curr: string): CoinConfig[] { - const cc: CoinConfig[] = []; - - for (let i = 0; i < 16; i++) { - const ct = 2 ** i; - - const unit = Math.floor(ct / 100); - const cent = ct % 100; - - cc.push({ - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, - name: `${curr}-u${i}`, - feeDeposit: `${curr}:0`, - feeRefresh: `${curr}:0`, - feeRefund: `${curr}:0`, - feeWithdraw: `${curr}:0`, - value: `${curr}:${unit}.${cent}`, - }); - } - - return cc; -} diff --git a/packages/taler-wallet-cli/src/harness/faultInjection.ts b/packages/taler-wallet-cli/src/harness/faultInjection.ts deleted file mode 100644 index 4c3d0c123..000000000 --- a/packages/taler-wallet-cli/src/harness/faultInjection.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <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, - ExchangeServiceInterface, - MerchantServiceInterface, - MerchantService, -} from "../harness/harness.js"; - -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) => Promise<void>; - modifyResponse?: (ctx: FaultInjectionResponseContext) => Promise<void>; -} - -export class FaultProxy { - constructor( - private globalTestState: GlobalTestState, - private faultProxyConfig: FaultProxyConfig, - ) {} - - private currentFaultSpecs: FaultSpec[] = []; - - start() { - const server = http.createServer((req, res) => { - const requestChunks: Buffer[] = []; - const requestUrl = `http://localhost:${this.faultProxyConfig.inboundPort}${req.url}`; - console.log("request for", new URL(requestUrl)); - req.on("data", (chunk) => { - requestChunks.push(chunk); - }); - req.on("end", async () => { - 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) { - await 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", async () => { - 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) { - await modResponse(faultRespContext); - } - } - if (faultRespContext.dropResponse) { - req.destroy(); - return; - } - if (faultRespContext.responseBody) { - // We must accommodate for potentially changed content length - faultRespContext.responseHeaders[ - "content-length" - ] = `${faultRespContext.responseBody.byteLength}`; - } - console.log("writing response head"); - res.writeHead( - faultRespContext.statusCode, - http.STATUS_CODES[faultRespContext.statusCode], - faultRespContext.responseHeaders, - ); - if (faultRespContext.responseBody) { - res.write(faultRespContext.responseBody); - } - res.end(); - }); - }); - }); - }); - - server.listen(this.faultProxyConfig.inboundPort); - this.globalTestState.servers.push(server); - } - - addFault(f: FaultSpec) { - this.currentFaultSpecs.push(f); - } - - clearAllFaults() { - this.currentFaultSpecs = []; - } -} - -export class FaultInjectedExchangeService implements ExchangeServiceInterface { - baseUrl: string; - port: number; - faultProxy: FaultProxy; - - get name(): string { - return this.innerExchange.name; - } - - get masterPub(): string { - return this.innerExchange.masterPub; - } - - private innerExchange: ExchangeService; - - constructor( - t: GlobalTestState, - e: ExchangeService, - proxyInboundPort: number, - ) { - this.innerExchange = e; - this.faultProxy = new FaultProxy(t, { - inboundPort: proxyInboundPort, - targetPort: e.port, - }); - this.faultProxy.start(); - - const exchangeUrl = new URL(e.baseUrl); - exchangeUrl.port = `${proxyInboundPort}`; - this.baseUrl = exchangeUrl.href; - this.port = proxyInboundPort; - } -} - -export class FaultInjectedMerchantService implements MerchantServiceInterface { - baseUrl: string; - port: number; - faultProxy: FaultProxy; - - get name(): string { - return this.innerMerchant.name; - } - - private innerMerchant: MerchantService; - private inboundPort: number; - - constructor( - t: GlobalTestState, - m: MerchantService, - proxyInboundPort: number, - ) { - this.innerMerchant = m; - this.faultProxy = new FaultProxy(t, { - inboundPort: proxyInboundPort, - targetPort: m.port, - }); - this.faultProxy.start(); - this.inboundPort = proxyInboundPort; - } - - makeInstanceBaseUrl(instanceName?: string | undefined): string { - const url = new URL(this.innerMerchant.makeInstanceBaseUrl(instanceName)); - url.port = `${this.inboundPort}`; - return url.href; - } -} diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts deleted file mode 100644 index b4ac16dbf..000000000 --- a/packages/taler-wallet-cli/src/harness/harness.ts +++ /dev/null @@ -1,1779 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <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 http from "http"; -import * as readline from "readline"; -import { deepStrictEqual } from "assert"; -import { ChildProcess, spawn } from "child_process"; -import { URL } from "url"; -import axios, { AxiosError } from "axios"; -import { - codecForMerchantOrderPrivateStatusResponse, - codecForPostOrderResponse, - PostOrderRequest, - PostOrderResponse, - MerchantOrderPrivateStatusResponse, - TippingReserveStatus, - TipCreateConfirmation, - TipCreateRequest, - MerchantInstancesResponse, -} from "./merchantApiTypes"; -import { - openPromise, - OperationFailedError, - WalletCoreApiClient, -} from "@gnu-taler/taler-wallet-core"; -import { - AmountJson, - Amounts, - Configuration, - AmountString, - Codec, - buildCodecForObject, - codecForString, - Duration, - parsePaytoUri, - CoreApiResponse, - createEddsaKeyPair, - eddsaGetPublic, - EddsaKeyPair, - encodeCrock, - getRandomBytes, -} from "@gnu-taler/taler-util"; -import { CoinConfig } from "./denomStructures.js"; - -const exec = util.promisify(require("child_process").exec); - -export async function delayMs(ms: number): Promise<void> { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(), ms); - }); -} - -export interface WithAuthorization { - Authorization?: string; -} - -interface WaitResult { - code: number | null; - signal: NodeJS.Signals | null; -} - -/** - * Run a shell command, return stdout. - */ -export async function sh( - t: GlobalTestState, - logName: string, - command: string, - env: { [index: string]: string | undefined } = process.env, -): Promise<string> { - console.log("running command", command); - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const proc = spawn(command, { - stdio: ["inherit", "pipe", "pipe"], - shell: true, - env: env, - }); - proc.stdout.on("data", (x) => { - if (x instanceof Buffer) { - stdoutChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); - const stderrLog = fs.createWriteStream(stderrLogFileName, { - flags: "a", - }); - proc.stderr.pipe(stderrLog); - proc.on("exit", (code, signal) => { - console.log(`child process exited (${code} / ${signal})`); - if (code != 0) { - reject(Error(`Unexpected exit code ${code} for '${command}'`)); - return; - } - const b = Buffer.concat(stdoutChunks).toString("utf-8"); - resolve(b); - }); - proc.on("error", () => { - reject(Error("Child process had error")); - }); - }); -} - -function shellescape(args: string[]) { - const ret = args.map((s) => { - if (/[^A-Za-z0-9_\/:=-]/.test(s)) { - s = "'" + s.replace(/'/g, "'\\''") + "'"; - s = s.replace(/^(?:'')+/g, "").replace(/\\'''/g, "\\'"); - } - return s; - }); - return ret.join(" "); -} - -/** - * Run a shell command, return stdout. - * - * Log stderr to a log file. - */ -export async function runCommand( - t: GlobalTestState, - logName: string, - command: string, - args: string[], - env: { [index: string]: string | undefined } = process.env, -): Promise<string> { - console.log("running command", shellescape([command, ...args])); - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const proc = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - shell: false, - env: env, - }); - proc.stdout.on("data", (x) => { - if (x instanceof Buffer) { - stdoutChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); - const stderrLog = fs.createWriteStream(stderrLogFileName, { - flags: "a", - }); - proc.stderr.pipe(stderrLog); - proc.on("exit", (code, signal) => { - console.log(`child process exited (${code} / ${signal})`); - if (code != 0) { - reject(Error(`Unexpected exit code ${code} for '${command}'`)); - return; - } - const b = Buffer.concat(stdoutChunks).toString("utf-8"); - resolve(b); - }); - proc.on("error", () => { - reject(Error("Child process had error")); - }); - }); -} - -export class ProcessWrapper { - private waitPromise: Promise<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 class GlobalTestParams { - testDir: string; -} - -export class GlobalTestState { - testDir: string; - procs: ProcessWrapper[]; - servers: http.Server[]; - inShutdown: boolean = false; - constructor(params: GlobalTestParams) { - this.testDir = params.testDir; - this.procs = []; - this.servers = []; - } - - async assertThrowsOperationErrorAsync( - block: () => Promise<void>, - ): Promise<OperationFailedError> { - try { - await block(); - } catch (e) { - if (e instanceof OperationFailedError) { - return e; - } - throw Error(`expected OperationFailedError to be thrown, but got ${e}`); - } - throw Error( - `expected OperationFailedError to be thrown, but block finished without throwing`, - ); - } - - async assertThrowsAsync(block: () => Promise<void>): Promise<any> { - try { - await block(); - } catch (e) { - return e; - } - throw Error( - `expected exception to be thrown, but block finished without throwing`, - ); - } - - assertAxiosError(e: any): asserts e is AxiosError { - if (!e.isAxiosError) { - throw Error("expected axios error"); - } - } - - assertTrue(b: boolean): asserts b { - if (!b) { - throw Error("test assertion failed"); - } - } - - assertDeepEqual<T>(actual: any, expected: T): asserts actual is T { - deepStrictEqual(actual, expected); - } - - assertAmountEquals( - amtActual: string | AmountJson, - amtExpected: string | AmountJson, - ): void { - if (Amounts.cmp(amtActual, amtExpected) != 0) { - throw Error( - `test assertion failed: expected ${Amounts.stringify( - amtExpected, - )} but got ${Amounts.stringify(amtActual)}`, - ); - } - } - - assertAmountLeq(a: string | AmountJson, b: string | AmountJson): void { - if (Amounts.cmp(a, b) > 0) { - throw Error( - `test assertion failed: expected ${Amounts.stringify( - a, - )} to be less or equal (leq) than ${Amounts.stringify(b)}`, - ); - } - } - - shutdownSync(): void { - for (const s of this.servers) { - s.close(); - s.removeAllListeners(); - } - for (const p of this.procs) { - if (p.proc.exitCode == null) { - p.proc.kill("SIGTERM"); - } - } - } - - spawnService( - command: string, - args: string[], - logName: string, - env: { [index: string]: string | undefined } = process.env, - ): ProcessWrapper { - console.log( - `spawning process (${logName}): ${shellescape([command, ...args])}`, - ); - const proc = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - env: env, - }); - console.log(`spawned process (${logName}) with pid ${proc.pid}`); - proc.on("error", (err) => { - console.log(`could not start process (${command})`, err); - }); - proc.on("exit", (code, signal) => { - console.log(`process ${logName} exited`); - }); - const stderrLogFileName = this.testDir + `/${logName}-stderr.log`; - const stderrLog = fs.createWriteStream(stderrLogFileName, { - flags: "a", - }); - proc.stderr.pipe(stderrLog); - const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`; - const stdoutLog = fs.createWriteStream(stdoutLogFileName, { - flags: "a", - }); - proc.stdout.pipe(stdoutLog); - const procWrap = new ProcessWrapper(proc); - this.procs.push(procWrap); - return procWrap; - } - - async shutdown(): Promise<void> { - if (this.inShutdown) { - return; - } - if (shouldLingerInTest()) { - console.log("refusing to shut down, lingering was requested"); - return; - } - this.inShutdown = true; - console.log("shutting down"); - for (const s of this.servers) { - s.close(); - s.removeAllListeners(); - } - for (const p of this.procs) { - if (p.proc.exitCode == null) { - console.log("killing process", p.proc.pid); - p.proc.kill("SIGTERM"); - await p.wait(); - } - } - } -} - -export function shouldLingerInTest(): boolean { - return !!process.env["TALER_TEST_LINGER"]; -} - -export interface TalerConfigSection { - options: Record<string, string | undefined>; -} - -export interface TalerConfig { - sections: Record<string, TalerConfigSection>; -} - -export interface DbInfo { - /** - * Postgres connection string. - */ - connStr: string; - - dbname: string; -} - -export async function setupDb(gc: GlobalTestState): Promise<DbInfo> { - const dbname = "taler-integrationtest"; - await exec(`dropdb "${dbname}" || true`); - await exec(`createdb "${dbname}"`); - return { - connStr: `postgres:///${dbname}`, - dbname, - }; -} - -export interface BankConfig { - currency: string; - httpPort: number; - database: string; - allowRegistrations: boolean; - maxDebt?: string; -} - -export interface FakeBankConfig { - currency: string; - httpPort: number; -} - -function setTalerPaths(config: Configuration, home: string) { - config.setString("paths", "taler_home", home); - // We need to make sure that the path of taler_runtime_dir isn't too long, - // as it contains unix domain sockets (108 character limit). - const runDir = fs.mkdtempSync("/tmp/taler-test-"); - config.setString("paths", "taler_runtime_dir", runDir); - 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/"); -} - -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}`); -} - -/** - * Send an HTTP request until it succeeds or the - * process dies. - */ -export async function pingProc( - proc: ProcessWrapper | undefined, - url: string, - serviceName: string, -): Promise<void> { - if (!proc || proc.proc.exitCode !== null) { - throw Error(`service process ${serviceName} not started, can't ping`); - } - while (true) { - try { - console.log(`pinging ${serviceName}`); - const resp = await axios.get(url); - console.log(`service ${serviceName} available`); - return; - } catch (e: any) { - console.log(`service ${serviceName} not ready:`, e.toString()); - await delayMs(1000); - } - if (!proc || proc.proc.exitCode !== null) { - throw Error(`service process ${serviceName} stopped unexpectedly`); - } - } -} - -export interface HarnessExchangeBankAccount { - accountName: string; - accountPassword: string; - accountPaytoUri: string; - wireGatewayApiBaseUrl: string; -} - -export interface BankServiceInterface { - readonly baseUrl: string; - readonly port: number; -} - -export enum CreditDebitIndicator { - Credit = "credit", - Debit = "debit", -} - -export interface BankAccountBalanceResponse { - balance: { - amount: AmountString; - credit_debit_indicator: CreditDebitIndicator; - }; -} - -export namespace BankAccessApi { - export async function getAccountBalance( - bank: BankServiceInterface, - bankUser: BankUser, - ): Promise<BankAccountBalanceResponse> { - const url = new URL(`accounts/${bankUser.username}`, bank.baseUrl); - const resp = await axios.get(url.href, { - auth: bankUser, - }); - return resp.data; - } - - export async function createWithdrawalOperation( - bank: BankServiceInterface, - bankUser: BankUser, - amount: string, - ): Promise<WithdrawalOperationInfo> { - const url = new URL( - `accounts/${bankUser.username}/withdrawals`, - bank.baseUrl, - ); - const resp = await axios.post( - url.href, - { - amount, - }, - { - auth: bankUser, - }, - ); - return codecForWithdrawalOperationInfo().decode(resp.data); - } -} - -export namespace BankApi { - export async function registerAccount( - bank: BankServiceInterface, - username: string, - password: string, - ): Promise<BankUser> { - const url = new URL("testing/register", bank.baseUrl); - await axios.post(url.href, { - username, - password, - }); - return { - password, - username, - accountPaytoUri: `payto://x-taler-bank/localhost/${username}`, - }; - } - - export async function createRandomBankUser( - bank: BankServiceInterface, - ): Promise<BankUser> { - const username = "user-" + encodeCrock(getRandomBytes(10)); - const password = "pw-" + encodeCrock(getRandomBytes(10)); - return await registerAccount(bank, username, password); - } - - export async function adminAddIncoming( - bank: BankServiceInterface, - params: { - exchangeBankAccount: HarnessExchangeBankAccount; - amount: string; - reservePub: string; - debitAccountPayto: string; - }, - ) { - const url = new URL( - `taler-wire-gateway/${params.exchangeBankAccount.accountName}/admin/add-incoming`, - bank.baseUrl, - ); - await axios.post( - url.href, - { - amount: params.amount, - reserve_pub: params.reservePub, - debit_account: params.debitAccountPayto, - }, - { - auth: { - username: params.exchangeBankAccount.accountName, - password: params.exchangeBankAccount.accountPassword, - }, - }, - ); - } - - export async function confirmWithdrawalOperation( - bank: BankServiceInterface, - bankUser: BankUser, - wopi: WithdrawalOperationInfo, - ): Promise<void> { - const url = new URL( - `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`, - bank.baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: bankUser, - }, - ); - } - - export async function abortWithdrawalOperation( - bank: BankServiceInterface, - bankUser: BankUser, - wopi: WithdrawalOperationInfo, - ): Promise<void> { - const url = new URL( - `accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/abort`, - bank.baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: bankUser, - }, - ); - } -} - -export class BankService implements BankServiceInterface { - proc: ProcessWrapper | undefined; - - static fromExistingConfig(gc: GlobalTestState): BankService { - const cfgFilename = gc.testDir + "/bank.conf"; - console.log("reading bank config from", cfgFilename); - const config = Configuration.load(cfgFilename); - const bc: BankConfig = { - allowRegistrations: config - .getYesNo("bank", "allow_registrations") - .required(), - currency: config.getString("taler", "currency").required(), - database: config.getString("bank", "database").required(), - httpPort: config.getNumber("bank", "http_port").required(), - }; - return new BankService(gc, bc, cfgFilename); - } - - static async create( - gc: GlobalTestState, - bc: BankConfig, - ): Promise<BankService> { - const config = new Configuration(); - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString("taler", "currency", bc.currency); - config.setString("bank", "database", bc.database); - config.setString("bank", "http_port", `${bc.httpPort}`); - config.setString("bank", "serve", "http"); - config.setString("bank", "max_debt_bank", `${bc.currency}:999999`); - config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`); - config.setString( - "bank", - "allow_registrations", - bc.allowRegistrations ? "yes" : "no", - ); - const cfgFilename = gc.testDir + "/bank.conf"; - config.write(cfgFilename); - - await sh( - gc, - "taler-bank-manage_django", - `taler-bank-manage -c '${cfgFilename}' django migrate`, - ); - await sh( - gc, - "taler-bank-manage_django", - `taler-bank-manage -c '${cfgFilename}' django provide_accounts`, - ); - - return new BankService(gc, bc, cfgFilename); - } - - setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) { - const config = Configuration.load(this.configFile); - config.setString("bank", "suggested_exchange", e.baseUrl); - config.setString("bank", "suggested_exchange_payto", exchangePayto); - } - - get baseUrl(): string { - return `http://localhost:${this.bankConfig.httpPort}/`; - } - - async createExchangeAccount( - accountName: string, - password: string, - ): Promise<HarnessExchangeBankAccount> { - await sh( - this.globalTestState, - "taler-bank-manage_django", - `taler-bank-manage -c '${this.configFile}' django add_bank_account ${accountName}`, - ); - await sh( - this.globalTestState, - "taler-bank-manage_django", - `taler-bank-manage -c '${this.configFile}' django changepassword_unsafe ${accountName} ${password}`, - ); - await sh( - this.globalTestState, - "taler-bank-manage_django", - `taler-bank-manage -c '${this.configFile}' django top_up ${accountName} ${this.bankConfig.currency}:100000`, - ); - return { - accountName: accountName, - accountPassword: password, - accountPaytoUri: `payto://x-taler-bank/${accountName}`, - wireGatewayApiBaseUrl: `http://localhost:${this.bankConfig.httpPort}/taler-wire-gateway/${accountName}/`, - }; - } - - get port() { - return this.bankConfig.httpPort; - } - - private constructor( - private globalTestState: GlobalTestState, - private bankConfig: BankConfig, - private configFile: string, - ) {} - - async start(): Promise<void> { - this.proc = this.globalTestState.spawnService( - "taler-bank-manage", - ["-c", this.configFile, "serve"], - "bank", - ); - } - - async pingUntilAvailable(): Promise<void> { - const url = `http://localhost:${this.bankConfig.httpPort}/config`; - await pingProc(this.proc, url, "bank"); - } -} - -export class FakeBankService { - proc: ProcessWrapper | undefined; - - static fromExistingConfig(gc: GlobalTestState): FakeBankService { - const cfgFilename = gc.testDir + "/bank.conf"; - console.log("reading fakebank config from", cfgFilename); - const config = Configuration.load(cfgFilename); - const bc: FakeBankConfig = { - currency: config.getString("taler", "currency").required(), - httpPort: config.getNumber("bank", "http_port").required(), - }; - return new FakeBankService(gc, bc, cfgFilename); - } - - static async create( - gc: GlobalTestState, - bc: FakeBankConfig, - ): Promise<FakeBankService> { - const config = new Configuration(); - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString("taler", "currency", bc.currency); - config.setString("bank", "http_port", `${bc.httpPort}`); - const cfgFilename = gc.testDir + "/bank.conf"; - config.write(cfgFilename); - return new FakeBankService(gc, bc, cfgFilename); - } - - get baseUrl(): string { - return `http://localhost:${this.bankConfig.httpPort}/`; - } - - get port() { - return this.bankConfig.httpPort; - } - - private constructor( - private globalTestState: GlobalTestState, - private bankConfig: FakeBankConfig, - private configFile: string, - ) {} - - async start(): Promise<void> { - this.proc = this.globalTestState.spawnService( - "taler-fakebank-run", - ["-c", this.configFile], - "fakebank", - ); - } - - async pingUntilAvailable(): Promise<void> { - // Fakebank doesn't have "/config", so we ping just "/". - const url = `http://localhost:${this.bankConfig.httpPort}/`; - await pingProc(this.proc, url, "bank"); - } -} - -export interface BankUser { - username: string; - password: string; - accountPaytoUri: string; -} - -export interface WithdrawalOperationInfo { - withdrawal_id: string; - taler_withdraw_uri: string; -} - -const codecForWithdrawalOperationInfo = (): Codec<WithdrawalOperationInfo> => - buildCodecForObject<WithdrawalOperationInfo>() - .property("withdrawal_id", codecForString()) - .property("taler_withdraw_uri", codecForString()) - .build("WithdrawalOperationInfo"); - -export interface ExchangeConfig { - name: string; - currency: string; - roundUnit?: string; - httpPort: number; - database: string; -} - -export interface ExchangeServiceInterface { - readonly baseUrl: string; - readonly port: number; - readonly name: string; - readonly masterPub: string; -} - -export class ExchangeService implements ExchangeServiceInterface { - static fromExistingConfig(gc: GlobalTestState, exchangeName: string) { - const cfgFilename = gc.testDir + `/exchange-${exchangeName}.conf`; - const config = Configuration.load(cfgFilename); - const ec: ExchangeConfig = { - currency: config.getString("taler", "currency").required(), - database: config.getString("exchangedb-postgres", "config").required(), - httpPort: config.getNumber("exchange", "port").required(), - name: exchangeName, - roundUnit: config.getString("taler", "currency_round_unit").required(), - }; - const privFile = config.getPath("exchange", "master_priv_file").required(); - const eddsaPriv = fs.readFileSync(privFile); - const keyPair: EddsaKeyPair = { - eddsaPriv, - eddsaPub: eddsaGetPublic(eddsaPriv), - }; - return new ExchangeService(gc, ec, cfgFilename, keyPair); - } - - private currentTimetravel: Duration | undefined; - - setTimetravel(t: Duration | undefined): void { - if (this.isRunning()) { - throw Error("can't set time travel while the exchange is running"); - } - this.currentTimetravel = t; - } - - private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { - // Convert to microseconds - return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; - } - return undefined; - } - - /** - * Return an empty array if no time travel is set, - * and an array with the time travel command line argument - * otherwise. - */ - private get timetravelArgArr(): string[] { - const tta = this.timetravelArg; - if (tta) { - return [tta]; - } - return []; - } - - async runWirewatchOnce() { - await runCommand( - this.globalState, - `exchange-${this.name}-wirewatch-once`, - "taler-exchange-wirewatch", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); - } - - async runAggregatorOnce() { - await runCommand( - this.globalState, - `exchange-${this.name}-aggregator-once`, - "taler-exchange-aggregator", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); - } - - async runTransferOnce() { - await runCommand( - this.globalState, - `exchange-${this.name}-transfer-once`, - "taler-exchange-transfer", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); - } - - changeConfig(f: (config: Configuration) => void) { - const config = Configuration.load(this.configFilename); - f(config); - config.write(this.configFilename); - } - - 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`, - ); - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString( - "exchange", - "revocation_dir", - "${TALER_DATA_HOME}/exchange/revocations", - ); - config.setString("exchange", "max_keys_caching", "forever"); - config.setString("exchange", "db", "postgres"); - config.setString( - "exchange-offline", - "master_priv_file", - "${TALER_DATA_HOME}/exchange/offline-keys/master.priv", - ); - config.setString("exchange", "serve", "tcp"); - config.setString("exchange", "port", `${e.httpPort}`); - - config.setString("exchangedb-postgres", "config", e.database); - - config.setString("taler-exchange-secmod-eddsa", "lookahead_sign", "20 s"); - config.setString("taler-exchange-secmod-rsa", "lookahead_sign", "20 s"); - - const exchangeMasterKey = createEddsaKeyPair(); - - config.setString( - "exchange", - "master_public_key", - encodeCrock(exchangeMasterKey.eddsaPub), - ); - - const masterPrivFile = config - .getPath("exchange-offline", "master_priv_file") - .required(); - - fs.mkdirSync(path.dirname(masterPrivFile), { recursive: true }); - - fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv)); - - const cfgFilename = gc.testDir + `/exchange-${e.name}.conf`; - config.write(cfgFilename); - return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey); - } - - addOfferedCoins(offeredCoins: ((curr: string) => CoinConfig)[]) { - const config = Configuration.load(this.configFilename); - offeredCoins.forEach((cc) => - setCoin(config, cc(this.exchangeConfig.currency)), - ); - config.write(this.configFilename); - } - - addCoinConfigList(ccs: CoinConfig[]) { - const config = Configuration.load(this.configFilename); - ccs.forEach((cc) => setCoin(config, cc)); - config.write(this.configFilename); - } - - get masterPub() { - return encodeCrock(this.keyPair.eddsaPub); - } - - get port() { - return this.exchangeConfig.httpPort; - } - - async addBankAccount( - localName: string, - exchangeBankAccount: HarnessExchangeBankAccount, - ): Promise<void> { - const config = Configuration.load(this.configFilename); - config.setString( - `exchange-account-${localName}`, - "wire_response", - `\${TALER_DATA_HOME}/exchange/account-${localName}.json`, - ); - config.setString( - `exchange-account-${localName}`, - "payto_uri", - exchangeBankAccount.accountPaytoUri, - ); - config.setString(`exchange-account-${localName}`, "enable_credit", "yes"); - config.setString(`exchange-account-${localName}`, "enable_debit", "yes"); - config.setString( - `exchange-accountcredentials-${localName}`, - "wire_gateway_url", - exchangeBankAccount.wireGatewayApiBaseUrl, - ); - config.setString( - `exchange-accountcredentials-${localName}`, - "wire_gateway_auth_method", - "basic", - ); - config.setString( - `exchange-accountcredentials-${localName}`, - "username", - exchangeBankAccount.accountName, - ); - config.setString( - `exchange-accountcredentials-${localName}`, - "password", - exchangeBankAccount.accountPassword, - ); - config.write(this.configFilename); - } - - exchangeHttpProc: ProcessWrapper | undefined; - exchangeWirewatchProc: ProcessWrapper | undefined; - - helperCryptoRsaProc: ProcessWrapper | undefined; - helperCryptoEddsaProc: ProcessWrapper | undefined; - - constructor( - private globalState: GlobalTestState, - private exchangeConfig: ExchangeConfig, - private configFilename: string, - private keyPair: EddsaKeyPair, - ) {} - - get name() { - return this.exchangeConfig.name; - } - - get baseUrl() { - return `http://localhost:${this.exchangeConfig.httpPort}/`; - } - - isRunning(): boolean { - return !!this.exchangeWirewatchProc || !!this.exchangeHttpProc; - } - - async stop(): Promise<void> { - const wirewatch = this.exchangeWirewatchProc; - if (wirewatch) { - wirewatch.proc.kill("SIGTERM"); - await wirewatch.wait(); - this.exchangeWirewatchProc = undefined; - } - const httpd = this.exchangeHttpProc; - if (httpd) { - httpd.proc.kill("SIGTERM"); - await httpd.wait(); - this.exchangeHttpProc = undefined; - } - const cryptoRsa = this.helperCryptoRsaProc; - if (cryptoRsa) { - cryptoRsa.proc.kill("SIGTERM"); - await cryptoRsa.wait(); - this.helperCryptoRsaProc = undefined; - } - const cryptoEddsa = this.helperCryptoEddsaProc; - if (cryptoEddsa) { - cryptoEddsa.proc.kill("SIGTERM"); - await cryptoEddsa.wait(); - this.helperCryptoRsaProc = undefined; - } - } - - /** - * Update keys signing the keys generated by the security module - * with the offline signing key. - */ - async keyup(): Promise<void> { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - ["-c", this.configFilename, "download", "sign", "upload"], - ); - - const accounts: string[] = []; - const accountTargetTypes: Set<string> = new Set(); - - const config = Configuration.load(this.configFilename); - for (const sectionName of config.getSectionNames()) { - if (sectionName.startsWith("EXCHANGE-ACCOUNT-")) { - const paytoUri = config.getString(sectionName, "payto_uri").required(); - const p = parsePaytoUri(paytoUri); - if (!p) { - throw Error(`invalid payto uri in exchange config: ${paytoUri}`); - } - accountTargetTypes.add(p?.targetType); - accounts.push(paytoUri); - } - } - - console.log("configuring bank accounts", accounts); - - for (const acc of accounts) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - ["-c", this.configFilename, "enable-account", acc, "upload"], - ); - } - - const year = new Date().getFullYear(); - for (const accTargetType of accountTargetTypes.values()) { - for (let i = year; i < year + 5; i++) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - "wire-fee", - `${i}`, - accTargetType, - `${this.exchangeConfig.currency}:0.01`, - `${this.exchangeConfig.currency}:0.01`, - "upload", - ], - ); - } - } - } - - async revokeDenomination(denomPubHash: string) { - if (!this.isRunning()) { - throw Error("exchange must be running when revoking denominations"); - } - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - "revoke-denomination", - denomPubHash, - "upload", - ], - ); - } - - async purgeSecmodKeys(): Promise<void> { - const cfg = Configuration.load(this.configFilename); - const rsaKeydir = cfg - .getPath("taler-exchange-secmod-rsa", "KEY_DIR") - .required(); - const eddsaKeydir = cfg - .getPath("taler-exchange-secmod-eddsa", "KEY_DIR") - .required(); - // Be *VERY* careful when changing this, or you will accidentally delete user data. - await sh(this.globalState, "rm-secmod-keys", `rm -rf ${rsaKeydir}/COIN_*`); - await sh(this.globalState, "rm-secmod-keys", `rm ${eddsaKeydir}/*`); - } - - async purgeDatabase(): Promise<void> { - await sh( - this.globalState, - "exchange-dbinit", - `taler-exchange-dbinit -r -c "${this.configFilename}"`, - ); - } - - async start(): Promise<void> { - if (this.isRunning()) { - throw Error("exchange is already running"); - } - await sh( - this.globalState, - "exchange-dbinit", - `taler-exchange-dbinit -c "${this.configFilename}"`, - ); - - this.helperCryptoEddsaProc = this.globalState.spawnService( - "taler-exchange-secmod-eddsa", - ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], - `exchange-crypto-eddsa-${this.name}`, - ); - - this.helperCryptoRsaProc = this.globalState.spawnService( - "taler-exchange-secmod-rsa", - ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], - `exchange-crypto-rsa-${this.name}`, - ); - - this.exchangeWirewatchProc = this.globalState.spawnService( - "taler-exchange-wirewatch", - ["-c", this.configFilename, ...this.timetravelArgArr], - `exchange-wirewatch-${this.name}`, - ); - - this.exchangeHttpProc = this.globalState.spawnService( - "taler-exchange-httpd", - ["-c", this.configFilename, ...this.timetravelArgArr], - `exchange-httpd-${this.name}`, - ); - - await this.pingUntilAvailable(); - await this.keyup(); - } - - async pingUntilAvailable(): Promise<void> { - // We request /management/keys, since /keys can block - // when we didn't do the key setup yet. - const url = `http://localhost:${this.exchangeConfig.httpPort}/management/keys`; - await pingProc(this.exchangeHttpProc, url, `exchange (${this.name})`); - } -} - -export interface MerchantConfig { - name: string; - currency: string; - httpPort: number; - database: string; -} - -export interface PrivateOrderStatusQuery { - instance?: string; - orderId: string; - sessionId?: string; -} - -export interface MerchantServiceInterface { - makeInstanceBaseUrl(instanceName?: string): string; - readonly port: number; - readonly name: string; -} - -export class MerchantApiClient { - constructor( - private baseUrl: string, - public readonly auth: MerchantAuthConfiguration, - ) {} - - async changeAuth(auth: MerchantAuthConfiguration): Promise<void> { - const url = new URL("private/auth", this.baseUrl); - await axios.post(url.href, auth, { - headers: this.makeAuthHeader(), - }); - } - - async deleteInstance(instanceId: string) { - const url = new URL(`management/instances/${instanceId}`, this.baseUrl); - await axios.delete(url.href, { - headers: this.makeAuthHeader(), - }); - } - - async createInstance(req: MerchantInstanceConfig): Promise<void> { - const url = new URL("management/instances", this.baseUrl); - await axios.post(url.href, req, { - headers: this.makeAuthHeader(), - }); - } - - async getInstances(): Promise<MerchantInstancesResponse> { - const url = new URL("management/instances", this.baseUrl); - const resp = await axios.get(url.href, { - headers: this.makeAuthHeader(), - }); - return resp.data; - } - - async getInstanceFullDetails(instanceId: string): Promise<any> { - const url = new URL(`management/instances/${instanceId}`, this.baseUrl); - try { - const resp = await axios.get(url.href, { - headers: this.makeAuthHeader(), - }); - return resp.data; - } catch (e) { - throw e; - } - } - - makeAuthHeader(): Record<string, string> { - switch (this.auth.method) { - case "external": - return {}; - case "token": - return { - Authorization: `Bearer ${this.auth.token}`, - }; - } - } -} - -/** - * FIXME: This should be deprecated in favor of MerchantApiClient - */ -export namespace MerchantPrivateApi { - export async function createOrder( - merchantService: MerchantServiceInterface, - instanceName: string, - req: PostOrderRequest, - withAuthorization: WithAuthorization = {}, - ): Promise<PostOrderResponse> { - const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); - let url = new URL("private/orders", baseUrl); - const resp = await axios.post(url.href, req, { - headers: withAuthorization, - }); - return codecForPostOrderResponse().decode(resp.data); - } - - export async function queryPrivateOrderStatus( - merchantService: MerchantServiceInterface, - query: PrivateOrderStatusQuery, - withAuthorization: WithAuthorization = {}, - ): Promise<MerchantOrderPrivateStatusResponse> { - const reqUrl = new URL( - `private/orders/${query.orderId}`, - merchantService.makeInstanceBaseUrl(query.instance), - ); - if (query.sessionId) { - reqUrl.searchParams.set("session_id", query.sessionId); - } - const resp = await axios.get(reqUrl.href, { headers: withAuthorization }); - return codecForMerchantOrderPrivateStatusResponse().decode(resp.data); - } - - export async function giveRefund( - merchantService: MerchantServiceInterface, - r: { - instance: string; - orderId: string; - amount: string; - justification: string; - }, - ): Promise<{ talerRefundUri: string }> { - const reqUrl = new URL( - `private/orders/${r.orderId}/refund`, - merchantService.makeInstanceBaseUrl(r.instance), - ); - const resp = await axios.post(reqUrl.href, { - refund: r.amount, - reason: r.justification, - }); - return { - talerRefundUri: resp.data.taler_refund_uri, - }; - } - - export async function createTippingReserve( - merchantService: MerchantServiceInterface, - instance: string, - req: CreateMerchantTippingReserveRequest, - ): Promise<CreateMerchantTippingReserveConfirmation> { - const reqUrl = new URL( - `private/reserves`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await axios.post(reqUrl.href, req); - // FIXME: validate - return resp.data; - } - - export async function queryTippingReserves( - merchantService: MerchantServiceInterface, - instance: string, - ): Promise<TippingReserveStatus> { - const reqUrl = new URL( - `private/reserves`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await axios.get(reqUrl.href); - // FIXME: validate - return resp.data; - } - - export async function giveTip( - merchantService: MerchantServiceInterface, - instance: string, - req: TipCreateRequest, - ): Promise<TipCreateConfirmation> { - const reqUrl = new URL( - `private/tips`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await axios.post(reqUrl.href, req); - // FIXME: validate - return resp.data; - } -} - -export interface CreateMerchantTippingReserveRequest { - // Amount that the merchant promises to put into the reserve - initial_balance: AmountString; - - // Exchange the merchant intends to use for tipping - exchange_url: string; - - // Desired wire method, for example "iban" or "x-taler-bank" - wire_method: string; -} - -export interface CreateMerchantTippingReserveConfirmation { - // Public key identifying the reserve - reserve_pub: string; - - // Wire account of the exchange where to transfer the funds - payto_uri: string; -} - -export class MerchantService implements MerchantServiceInterface { - static fromExistingConfig(gc: GlobalTestState, name: string) { - const cfgFilename = gc.testDir + `/merchant-${name}.conf`; - const config = Configuration.load(cfgFilename); - const mc: MerchantConfig = { - currency: config.getString("taler", "currency").required(), - database: config.getString("merchantdb-postgres", "config").required(), - httpPort: config.getNumber("merchant", "port").required(), - name, - }; - return new MerchantService(gc, mc, cfgFilename); - } - - proc: ProcessWrapper | undefined; - - constructor( - private globalState: GlobalTestState, - private merchantConfig: MerchantConfig, - private configFilename: string, - ) {} - - private currentTimetravel: Duration | undefined; - - private isRunning(): boolean { - return !!this.proc; - } - - setTimetravel(t: Duration | undefined): void { - if (this.isRunning()) { - throw Error("can't set time travel while the exchange is running"); - } - this.currentTimetravel = t; - } - - private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { - // Convert to microseconds - return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; - } - return undefined; - } - - /** - * Return an empty array if no time travel is set, - * and an array with the time travel command line argument - * otherwise. - */ - private get timetravelArgArr(): string[] { - const tta = this.timetravelArg; - if (tta) { - return [tta]; - } - return []; - } - - get port(): number { - return this.merchantConfig.httpPort; - } - - get name(): string { - return this.merchantConfig.name; - } - - async stop(): Promise<void> { - const httpd = this.proc; - if (httpd) { - httpd.proc.kill("SIGTERM"); - await httpd.wait(); - this.proc = undefined; - } - } - - async start(): Promise<void> { - await exec(`taler-merchant-dbinit -c "${this.configFilename}"`); - - this.proc = this.globalState.spawnService( - "taler-merchant-httpd", - ["-LDEBUG", "-c", this.configFilename, ...this.timetravelArgArr], - `merchant-${this.merchantConfig.name}`, - ); - } - - static async create( - gc: GlobalTestState, - mc: MerchantConfig, - ): Promise<MerchantService> { - const config = new Configuration(); - config.setString("taler", "currency", mc.currency); - - const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`; - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString("merchant", "serve", "tcp"); - config.setString("merchant", "port", `${mc.httpPort}`); - config.setString( - "merchant", - "keyfile", - "${TALER_DATA_HOME}/merchant/merchant.priv", - ); - config.setString("merchantdb-postgres", "config", mc.database); - config.write(cfgFilename); - - return new MerchantService(gc, mc, cfgFilename); - } - - addExchange(e: ExchangeServiceInterface): void { - const config = Configuration.load(this.configFilename); - config.setString( - `merchant-exchange-${e.name}`, - "exchange_base_url", - e.baseUrl, - ); - config.setString( - `merchant-exchange-${e.name}`, - "currency", - this.merchantConfig.currency, - ); - config.setString(`merchant-exchange-${e.name}`, "master_key", e.masterPub); - config.write(this.configFilename); - } - - async addDefaultInstance(): Promise<void> { - return await this.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - auth: { - method: "external", - }, - }); - } - - async addInstance( - instanceConfig: PartialMerchantInstanceConfig, - ): 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}/management/instances`; - const auth = instanceConfig.auth ?? { method: "external" }; - await axios.post(url, { - auth, - payto_uris: instanceConfig.paytoUris, - id: instanceConfig.id, - name: instanceConfig.name, - address: instanceConfig.address ?? {}, - jurisdiction: instanceConfig.jurisdiction ?? {}, - default_max_wire_fee: - instanceConfig.defaultMaxWireFee ?? - `${this.merchantConfig.currency}:1.0`, - default_wire_fee_amortization: - instanceConfig.defaultWireFeeAmortization ?? 3, - default_max_deposit_fee: - instanceConfig.defaultMaxDepositFee ?? - `${this.merchantConfig.currency}:1.0`, - default_wire_transfer_delay: instanceConfig.defaultWireTransferDelay ?? { - d_ms: "forever", - }, - default_pay_delay: instanceConfig.defaultPayDelay ?? { d_ms: "forever" }, - }); - } - - makeInstanceBaseUrl(instanceName?: string): string { - if (instanceName === undefined || instanceName === "default") { - return `http://localhost:${this.merchantConfig.httpPort}/`; - } else { - return `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/`; - } - } - - async pingUntilAvailable(): Promise<void> { - const url = `http://localhost:${this.merchantConfig.httpPort}/config`; - await pingProc(this.proc, url, `merchant (${this.merchantConfig.name})`); - } -} - -export interface MerchantAuthConfiguration { - method: "external" | "token"; - token?: string; -} - -export interface PartialMerchantInstanceConfig { - auth?: MerchantAuthConfiguration; - id: string; - name: string; - paytoUris: string[]; - address?: unknown; - jurisdiction?: unknown; - defaultMaxWireFee?: string; - defaultMaxDepositFee?: string; - defaultWireFeeAmortization?: number; - defaultWireTransferDelay?: Duration; - defaultPayDelay?: Duration; -} - -export interface MerchantInstanceConfig { - auth: MerchantAuthConfiguration; - id: string; - name: string; - payto_uris: string[]; - address: unknown; - jurisdiction: unknown; - default_max_wire_fee: string; - default_max_deposit_fee: string; - default_wire_fee_amortization: number; - default_wire_transfer_delay: Duration; - default_pay_delay: Duration; -} - -type TestStatus = "pass" | "fail" | "skip"; - -export interface TestRunResult { - /** - * Name of the test. - */ - name: string; - - /** - * How long did the test run? - */ - timeSec: number; - - status: TestStatus; - - reason?: string; -} - -export async function runTestWithState( - gc: GlobalTestState, - testMain: (t: GlobalTestState) => Promise<void>, - testName: string, - linger: boolean = false, -): Promise<TestRunResult> { - const startMs = new Date().getTime(); - - const p = openPromise(); - let status: TestStatus; - - const handleSignal = (s: string) => { - console.warn( - `**** received fatal process event, terminating test ${testName}`, - ); - gc.shutdownSync(); - process.exit(1); - }; - - process.on("SIGINT", handleSignal); - process.on("SIGTERM", handleSignal); - process.on("unhandledRejection", handleSignal); - process.on("uncaughtException", handleSignal); - - try { - console.log("running test in directory", gc.testDir); - await Promise.race([testMain(gc), p.promise]); - status = "pass"; - if (linger) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: true, - }); - await new Promise<void>((resolve, reject) => { - rl.question("Press enter to shut down test.", () => { - resolve(); - }); - }); - rl.close(); - } - } catch (e) { - console.error("FATAL: test failed with exception", e); - status = "fail"; - } finally { - await gc.shutdown(); - } - const afterMs = new Date().getTime(); - return { - name: testName, - timeSec: (afterMs - startMs) / 1000, - status, - }; -} - -function shellWrap(s: string) { - return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"; -} - -export class WalletCli { - private currentTimetravel: Duration | undefined; - private _client: WalletCoreApiClient; - - setTimetravel(d: Duration | undefined) { - this.currentTimetravel = d; - } - - private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { - // Convert to microseconds - return `--timetravel=${this.currentTimetravel.d_ms * 1000}`; - } - return undefined; - } - - constructor( - private globalTestState: GlobalTestState, - private name: string = "default", - ) { - const self = this; - this._client = { - async call(op: any, payload: any): Promise<any> { - console.log("calling wallet with timetravel arg", self.timetravelArg); - const resp = await sh( - self.globalTestState, - `wallet-${self.name}`, - `taler-wallet-cli ${ - self.timetravelArg ?? "" - } --no-throttle --wallet-db '${self.dbfile}' api '${op}' ${shellWrap( - JSON.stringify(payload), - )}`, - ); - console.log(resp); - const ar = JSON.parse(resp) as CoreApiResponse; - if (ar.type === "error") { - throw new OperationFailedError(ar.error); - } else { - return ar.result; - } - }, - }; - } - - get dbfile(): string { - return this.globalTestState.testDir + `/walletdb-${this.name}.json`; - } - - deleteDatabase() { - fs.unlinkSync(this.dbfile); - } - - private get timetravelArgArr(): string[] { - const tta = this.timetravelArg; - if (tta) { - return [tta]; - } - return []; - } - - get client(): WalletCoreApiClient { - return this._client; - } - - async runUntilDone(args: { maxRetries?: number } = {}): Promise<void> { - await runCommand( - this.globalTestState, - `wallet-${this.name}`, - "taler-wallet-cli", - [ - "--no-throttle", - ...this.timetravelArgArr, - "--wallet-db", - this.dbfile, - "run-until-done", - ...(args.maxRetries ? ["--max-retries", `${args.maxRetries}`] : []), - ], - ); - } - - async runPending(): Promise<void> { - await runCommand( - this.globalTestState, - `wallet-${this.name}`, - "taler-wallet-cli", - [ - "--no-throttle", - ...this.timetravelArgArr, - "--wallet-db", - this.dbfile, - "run-pending", - ], - ); - } -} diff --git a/packages/taler-wallet-cli/src/harness/helpers.ts b/packages/taler-wallet-cli/src/harness/helpers.ts deleted file mode 100644 index 3b4e1643f..000000000 --- a/packages/taler-wallet-cli/src/harness/helpers.ts +++ /dev/null @@ -1,406 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Helpers to create typical test environments. - * - * @author Florian Dold <dold@taler.net> - */ - -/** - * Imports - */ -import { - FaultInjectedExchangeService, - FaultInjectedMerchantService, -} from "./faultInjection"; -import { CoinConfig, defaultCoinConfig } from "./denomStructures"; -import { - AmountString, - Duration, - ContractTerms, - PreparePayResultType, - ConfirmPayResultType, -} from "@gnu-taler/taler-util"; -import { - DbInfo, - BankService, - ExchangeService, - MerchantService, - WalletCli, - GlobalTestState, - setupDb, - ExchangeServiceInterface, - BankApi, - BankAccessApi, - MerchantServiceInterface, - MerchantPrivateApi, - HarnessExchangeBankAccount, - WithAuthorization, -} from "./harness.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -export interface SimpleTestEnvironment { - commonDb: DbInfo; - bank: BankService; - exchange: ExchangeService; - exchangeBankAccount: HarnessExchangeBankAccount; - merchant: MerchantService; - wallet: WalletCli; -} - -export function getRandomIban(countryCode: string): string { - return `${countryCode}715001051796${(Math.random() * 100000000) - .toString() - .substring(0, 6)}`; -} - -export function getRandomString(): string { - return Math.random().toString(36).substring(2); -} - -/** - * 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, - coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), -): Promise<SimpleTestEnvironment> { - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addCoinConfigList(coinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - }; -} - -export interface FaultyMerchantTestEnvironment { - commonDb: DbInfo; - bank: BankService; - exchange: ExchangeService; - faultyExchange: FaultInjectedExchangeService; - exchangeBankAccount: HarnessExchangeBankAccount; - merchant: MerchantService; - faultyMerchant: FaultInjectedMerchantService; - wallet: WalletCli; -} - -/** - * Run a test case with a simple TESTKUDOS Taler environment, consisting - * of one exchange, one bank and one merchant. - */ -export async function createFaultInjectedMerchantTestkudosEnvironment( - t: GlobalTestState, -): Promise<FaultyMerchantTestEnvironment> { - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083); - const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange( - faultyExchange, - exchangeBankAccount.accountPaytoUri, - ); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(faultyExchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - faultyMerchant, - faultyExchange, - }; -} - -/** - * Withdraw balance. - */ -export async function startWithdrawViaBank( - t: GlobalTestState, - p: { - wallet: WalletCli; - bank: BankService; - exchange: ExchangeServiceInterface; - amount: AmountString; - }, -): Promise<void> { - const { wallet, bank, exchange, amount } = p; - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount); - - // Hand it to the wallet - - await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - - await wallet.runPending(); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bank, user, wop); - - // Withdraw - - await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }); -} - -/** - * Withdraw balance. - */ -export async function withdrawViaBank( - t: GlobalTestState, - p: { - wallet: WalletCli; - bank: BankService; - exchange: ExchangeServiceInterface; - amount: AmountString; - }, -): Promise<void> { - const { wallet } = p; - - await startWithdrawViaBank(t, p); - - await wallet.runUntilDone(); - - // Check balance - - await wallet.client.call(WalletApiOperation.GetBalances, {}); -} - -export async function applyTimeTravel( - timetravelDuration: Duration, - s: { - exchange?: ExchangeService; - merchant?: MerchantService; - wallet?: WalletCli; - }, -): Promise<void> { - if (s.exchange) { - await s.exchange.stop(); - s.exchange.setTimetravel(timetravelDuration); - await s.exchange.start(); - await s.exchange.pingUntilAvailable(); - } - - if (s.merchant) { - await s.merchant.stop(); - s.merchant.setTimetravel(timetravelDuration); - await s.merchant.start(); - await s.merchant.pingUntilAvailable(); - } - - if (s.wallet) { - s.wallet.setTimetravel(timetravelDuration); - } -} - -/** - * Make a simple payment and check that it succeeded. - */ -export async function makeTestPayment( - t: GlobalTestState, - args: { - merchant: MerchantServiceInterface; - wallet: WalletCli; - order: Partial<ContractTerms>; - instance?: string; - }, - auth: WithAuthorization = {}, -): Promise<void> { - // Set up order. - - const { wallet, merchant } = args; - const instance = args.instance ?? "default"; - - const orderResp = await MerchantPrivateApi.createOrder( - merchant, - instance, - { - order: args.order, - }, - auth, - ); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - auth, - ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - - t.assertTrue(r2.type === ConfirmPayResultType.Done); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - instance, - }, - auth, - ); - - t.assertTrue(orderStatus.order_status === "paid"); -} diff --git a/packages/taler-wallet-cli/src/harness/libeufin.ts b/packages/taler-wallet-cli/src/harness/libeufin.ts deleted file mode 100644 index 11447b389..000000000 --- a/packages/taler-wallet-cli/src/harness/libeufin.ts +++ /dev/null @@ -1,1676 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import axios from "axios"; -import { URL } from "@gnu-taler/taler-util"; -import { getRandomIban, getRandomString } from "../harness/helpers.js"; -import { - GlobalTestState, - DbInfo, - pingProc, - ProcessWrapper, - runCommand, - setupDb, - sh, -} from "../harness/harness.js"; - -export interface LibeufinSandboxServiceInterface { - baseUrl: string; -} - -export interface LibeufinNexusServiceInterface { - baseUrl: string; -} - -export interface LibeufinServices { - libeufinSandbox: LibeufinSandboxService; - libeufinNexus: LibeufinNexusService; - commonDb: DbInfo; -} - -export interface LibeufinSandboxConfig { - httpPort: number; - databaseJdbcUri: string; -} - -export interface LibeufinNexusConfig { - httpPort: number; - databaseJdbcUri: string; -} - -export interface DeleteBankConnectionRequest { - bankConnectionId: string; -} - -interface LibeufinNexusMoneyMovement { - amount: string; - creditDebitIndicator: string; - details: { - debtor: { - name: string; - }; - debtorAccount: { - iban: string; - }; - debtorAgent: { - bic: string; - }; - creditor: { - name: string; - }; - creditorAccount: { - iban: string; - }; - creditorAgent: { - bic: string; - }; - endToEndId: string; - unstructuredRemittanceInformation: string; - }; -} - -interface LibeufinNexusBatches { - batchTransactions: Array<LibeufinNexusMoneyMovement>; -} - -interface LibeufinNexusTransaction { - amount: string; - creditDebitIndicator: string; - status: string; - bankTransactionCode: string; - valueDate: string; - bookingDate: string; - accountServicerRef: string; - batches: Array<LibeufinNexusBatches>; -} - -interface LibeufinNexusTransactions { - transactions: Array<LibeufinNexusTransaction>; -} - -export interface LibeufinCliDetails { - nexusUrl: string; - sandboxUrl: string; - nexusDatabaseUri: string; - sandboxDatabaseUri: string; - user: LibeufinNexusUser; -} - -export interface LibeufinEbicsSubscriberDetails { - hostId: string; - partnerId: string; - userId: string; -} - -export interface LibeufinEbicsConnectionDetails { - subscriberDetails: LibeufinEbicsSubscriberDetails; - ebicsUrl: string; - connectionName: string; -} - -export interface LibeufinBankAccountDetails { - currency: string; - iban: string; - bic: string; - personName: string; - accountName: string; -} - -export interface LibeufinNexusUser { - username: string; - password: string; -} - -export interface LibeufinBackupFileDetails { - passphrase: string; - outputFile: string; - connectionName: string; -} - -export interface LibeufinKeyLetterDetails { - outputFile: string; - connectionName: string; -} - -export interface LibeufinBankAccountImportDetails { - offeredBankAccountName: string; - nexusBankAccountName: string; - connectionName: string; -} - -export interface BankAccountInfo { - iban: string; - bic: string; - name: string; - currency: string; - label: string; -} - -export interface LibeufinPreparedPaymentDetails { - creditorIban: string; - creditorBic: string; - creditorName: string; - subject: string; - amount: string; - currency: string; - nexusBankAccountName: string; -} - -export interface LibeufinSandboxAddIncomingRequest { - creditorIban: string; - creditorBic: string; - creditorName: string; - debtorIban: string; - debtorBic: string; - debtorName: string; - subject: string; - amount: string; - currency: string; - uid: string; - direction: string; -} - -export class LibeufinSandboxService implements LibeufinSandboxServiceInterface { - static async create( - gc: GlobalTestState, - sandboxConfig: LibeufinSandboxConfig, - ): Promise<LibeufinSandboxService> { - return new LibeufinSandboxService(gc, sandboxConfig); - } - - sandboxProc: ProcessWrapper | undefined; - globalTestState: GlobalTestState; - - constructor( - gc: GlobalTestState, - private sandboxConfig: LibeufinSandboxConfig, - ) { - this.globalTestState = gc; - } - - get baseUrl(): string { - return `http://localhost:${this.sandboxConfig.httpPort}/`; - } - - async start(): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-sandbox-config", - "libeufin-sandbox config localhost", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - }, - ); - this.sandboxProc = this.globalTestState.spawnService( - "libeufin-sandbox", - ["serve", "--port", `${this.sandboxConfig.httpPort}`], - "libeufin-sandbox", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", - }, - ); - } - - async c53tick(): Promise<string> { - const stdout = await sh( - this.globalTestState, - "libeufin-sandbox-c53tick", - "libeufin-sandbox camt053tick", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - }, - ); - return stdout; - } - - async makeTransaction( - debit: string, - credit: string, - amount: string, // $currency:x.y - subject: string,): Promise<string> { - const stdout = await sh( - this.globalTestState, - "libeufin-sandbox-maketransfer", - `libeufin-sandbox make-transaction --debit-account=${debit} --credit-account=${credit} ${amount} "${subject}"`, - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - }, - ); - return stdout; - } - - async pingUntilAvailable(): Promise<void> { - const url = this.baseUrl; - await pingProc(this.sandboxProc, url, "libeufin-sandbox"); - } -} - -export class LibeufinNexusService { - static async create( - gc: GlobalTestState, - nexusConfig: LibeufinNexusConfig, - ): Promise<LibeufinNexusService> { - return new LibeufinNexusService(gc, nexusConfig); - } - - nexusProc: ProcessWrapper | undefined; - globalTestState: GlobalTestState; - - constructor(gc: GlobalTestState, private nexusConfig: LibeufinNexusConfig) { - this.globalTestState = gc; - } - - get baseUrl(): string { - return `http://localhost:${this.nexusConfig.httpPort}/`; - } - - async start(): Promise<void> { - await runCommand( - this.globalTestState, - "libeufin-nexus-superuser", - "libeufin-nexus", - ["superuser", "admin", "--password", "test"], - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, - }, - ); - - this.nexusProc = this.globalTestState.spawnService( - "libeufin-nexus", - ["serve", "--port", `${this.nexusConfig.httpPort}`], - "libeufin-nexus", - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, - }, - ); - } - - async pingUntilAvailable(): Promise<void> { - const url = `${this.baseUrl}config`; - await pingProc(this.nexusProc, url, "libeufin-nexus"); - } - - async createNexusSuperuser(details: LibeufinNexusUser): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-nexus", - `libeufin-nexus superuser ${details.username} --password=${details.password}`, - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, - }, - ); - console.log(stdout); - } -} - -export interface CreateEbicsSubscriberRequest { - hostID: string; - userID: string; - partnerID: string; - systemID?: string; -} - -export interface TwgAddIncomingRequest { - amount: string; - reserve_pub: string; - debit_account: string; -} - -interface CreateEbicsBankAccountRequest { - subscriber: { - hostID: string; - partnerID: string; - userID: string; - systemID?: string; - }; - // IBAN - iban: string; - // BIC - bic: string; - // human name - name: string; - currency: string; - label: string; -} - -export interface SimulateIncomingTransactionRequest { - debtorIban: string; - debtorBic: string; - debtorName: string; - - /** - * Subject / unstructured remittance info. - */ - subject: string; - - /** - * Decimal amount without currency. - */ - amount: string; -} - -/** - * The bundle aims at minimizing the amount of input - * data that is required to initialize a new user + Ebics - * connection. - */ -export class NexusUserBundle { - userReq: CreateNexusUserRequest; - connReq: CreateEbicsBankConnectionRequest; - anastasisReq: CreateAnastasisFacadeRequest; - twgReq: CreateTalerWireGatewayFacadeRequest; - twgTransferPermission: PostNexusPermissionRequest; - twgHistoryPermission: PostNexusPermissionRequest; - twgAddIncomingPermission: PostNexusPermissionRequest; - localAccountName: string; - remoteAccountName: string; - - constructor(salt: string, ebicsURL: string) { - this.userReq = { - username: `username-${salt}`, - password: `password-${salt}`, - }; - - this.connReq = { - name: `connection-${salt}`, - ebicsURL: ebicsURL, - hostID: `ebicshost,${salt}`, - partnerID: `ebicspartner,${salt}`, - userID: `ebicsuser,${salt}`, - }; - - this.twgReq = { - currency: "EUR", - name: `twg-${salt}`, - reserveTransferLevel: "report", - accountName: `local-account-${salt}`, - connectionName: `connection-${salt}`, - }; - this.anastasisReq = { - currency: "EUR", - name: `anastasis-${salt}`, - reserveTransferLevel: "report", - accountName: `local-account-${salt}`, - connectionName: `connection-${salt}`, - }; - this.remoteAccountName = `remote-account-${salt}`; - this.localAccountName = `local-account-${salt}`; - this.twgTransferPermission = { - action: "grant", - permission: { - subjectId: `username-${salt}`, - subjectType: "user", - resourceType: "facade", - resourceId: `twg-${salt}`, - permissionName: "facade.talerWireGateway.transfer", - }, - }; - this.twgHistoryPermission = { - action: "grant", - permission: { - subjectId: `username-${salt}`, - subjectType: "user", - resourceType: "facade", - resourceId: `twg-${salt}`, - permissionName: "facade.talerWireGateway.history", - }, - }; - } -} - -/** - * The bundle aims at minimizing the amount of input - * data that is required to initialize a new Sandbox - * customer, associating their bank account with a Ebics - * subscriber. - */ -export class SandboxUserBundle { - ebicsBankAccount: CreateEbicsBankAccountRequest; - constructor(salt: string) { - this.ebicsBankAccount = { - currency: "EUR", - bic: "BELADEBEXXX", - iban: getRandomIban("DE"), - label: `remote-account-${salt}`, - name: `Taler Exchange: ${salt}`, - subscriber: { - hostID: `ebicshost,${salt}`, - partnerID: `ebicspartner,${salt}`, - userID: `ebicsuser,${salt}`, - }, - }; - } -} - -export class LibeufinCli { - cliDetails: LibeufinCliDetails; - globalTestState: GlobalTestState; - - constructor(gc: GlobalTestState, cd: LibeufinCliDetails) { - this.globalTestState = gc; - this.cliDetails = cd; - } - - env(): any { - return { - ...process.env, - LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl, - LIBEUFIN_SANDBOX_USERNAME: "admin", - LIBEUFIN_SANDBOX_PASSWORD: "secret", - } - } - - async checkSandbox(): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-checksandbox", - "libeufin-cli sandbox check", - this.env() - ); - } - - async createEbicsHost(hostId: string): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicshost", - `libeufin-cli sandbox ebicshost create --host-id=${hostId}`, - this.env() - ); - console.log(stdout); - } - - async createEbicsSubscriber( - details: LibeufinEbicsSubscriberDetails, - ): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicssubscriber", - "libeufin-cli sandbox ebicssubscriber create" + - ` --host-id=${details.hostId}` + - ` --partner-id=${details.partnerId}` + - ` --user-id=${details.userId}`, - this.env() - ); - console.log(stdout); - } - - async createEbicsBankAccount( - sd: LibeufinEbicsSubscriberDetails, - bankAccountDetails: LibeufinBankAccountDetails, - ): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicsbankaccount", - "libeufin-cli sandbox ebicsbankaccount create" + - ` --currency=${bankAccountDetails.currency}` + - ` --iban=${bankAccountDetails.iban}` + - ` --bic=${bankAccountDetails.bic}` + - ` --person-name='${bankAccountDetails.personName}'` + - ` --account-name=${bankAccountDetails.accountName}` + - ` --ebics-host-id=${sd.hostId}` + - ` --ebics-partner-id=${sd.partnerId}` + - ` --ebics-user-id=${sd.userId}`, - this.env() - ); - console.log(stdout); - } - - async generateTransactions(accountName: string): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-generatetransactions", - `libeufin-cli sandbox bankaccount generate-transactions ${accountName}`, - this.env() - ); - console.log(stdout); - } - - async showSandboxTransactions(accountName: string): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-showsandboxtransactions", - `libeufin-cli sandbox bankaccount transactions ${accountName}`, - this.env() - ); - console.log(stdout); - } - - async createEbicsConnection( - connectionDetails: LibeufinEbicsConnectionDetails, - ): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicsconnection", - `libeufin-cli connections new-ebics-connection` + - ` --ebics-url=${connectionDetails.ebicsUrl}` + - ` --host-id=${connectionDetails.subscriberDetails.hostId}` + - ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` + - ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` + - ` ${connectionDetails.connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async createBackupFile(details: LibeufinBackupFileDetails): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createbackupfile", - `libeufin-cli connections export-backup` + - ` --passphrase=${details.passphrase}` + - ` --output-file=${details.outputFile}` + - ` ${details.connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async createKeyLetter(details: LibeufinKeyLetterDetails): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createkeyletter", - `libeufin-cli connections get-key-letter` + - ` ${details.connectionName} ${details.outputFile}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async connect(connectionName: string): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-connect", - `libeufin-cli connections connect ${connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async downloadBankAccounts(connectionName: string): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-downloadbankaccounts", - `libeufin-cli connections download-bank-accounts ${connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async listOfferedBankAccounts(connectionName: string): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-listofferedbankaccounts", - `libeufin-cli connections list-offered-bank-accounts ${connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async importBankAccount( - importDetails: LibeufinBankAccountImportDetails, - ): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-importbankaccount", - "libeufin-cli connections import-bank-account" + - ` --offered-account-id=${importDetails.offeredBankAccountName}` + - ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` + - ` ${importDetails.connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async fetchTransactions(bankAccountName: string): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-fetchtransactions", - `libeufin-cli accounts fetch-transactions ${bankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async transactions(bankAccountName: string): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-transactions", - `libeufin-cli accounts transactions ${bankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async preparePayment(details: LibeufinPreparedPaymentDetails): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-preparepayment", - `libeufin-cli accounts prepare-payment` + - ` --creditor-iban=${details.creditorIban}` + - ` --creditor-bic=${details.creditorBic}` + - ` --creditor-name='${details.creditorName}'` + - ` --payment-subject='${details.subject}'` + - ` --payment-amount=${details.currency}:${details.amount}` + - ` ${details.nexusBankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async submitPayment( - details: LibeufinPreparedPaymentDetails, - paymentUuid: string, - ): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-submitpayment", - `libeufin-cli accounts submit-payment` + - ` --payment-uuid=${paymentUuid}` + - ` ${details.nexusBankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async newAnastasisFacade(req: NewAnastasisFacadeReq): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-new-anastasis-facade", - `libeufin-cli facades new-anastasis-facade` + - ` --currency ${req.currency}` + - ` --facade-name ${req.facadeName}` + - ` ${req.connectionName} ${req.accountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - - async newTalerWireGatewayFacade(req: NewTalerWireGatewayReq): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-new-taler-wire-gateway-facade", - `libeufin-cli facades new-taler-wire-gateway-facade` + - ` --currency ${req.currency}` + - ` --facade-name ${req.facadeName}` + - ` ${req.connectionName} ${req.accountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } - - async listFacades(): Promise<void> { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-facades-list", - `libeufin-cli facades list`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.user.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.user.password, - }, - ); - console.log(stdout); - } -} - -interface NewAnastasisFacadeReq { - facadeName: string; - connectionName: string; - accountName: string; - currency: string; -} - -interface NewTalerWireGatewayReq { - facadeName: string; - connectionName: string; - accountName: string; - currency: string; -} - -export namespace LibeufinSandboxApi { - - export async function rotateKeys( - libeufinSandboxService: LibeufinSandboxServiceInterface, - hostID: string, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL(`admin/ebics/hosts/${hostID}/rotate-keys`, baseUrl); - await axios.post(url.href, {}, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - export async function createEbicsHost( - libeufinSandboxService: LibeufinSandboxServiceInterface, - hostID: string, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/ebics/hosts", baseUrl); - await axios.post(url.href, { - hostID, - ebicsVersion: "2.5", - }, - { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function createBankAccount( - libeufinSandboxService: LibeufinSandboxServiceInterface, - req: BankAccountInfo, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL(`admin/bank-accounts/${req.label}`, baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function createEbicsSubscriber( - libeufinSandboxService: LibeufinSandboxServiceInterface, - req: CreateEbicsSubscriberRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/ebics/subscribers", baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function createEbicsBankAccount( - libeufinSandboxService: LibeufinSandboxServiceInterface, - req: CreateEbicsBankAccountRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/ebics/bank-accounts", baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function bookPayment2( - libeufinSandboxService: LibeufinSandboxService, - req: LibeufinSandboxAddIncomingRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/payments", baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function bookPayment( - libeufinSandboxService: LibeufinSandboxService, - creditorBundle: SandboxUserBundle, - debitorBundle: SandboxUserBundle, - subject: string, - amount: string, - currency: string, - ) { - let req: LibeufinSandboxAddIncomingRequest = { - creditorIban: creditorBundle.ebicsBankAccount.iban, - creditorBic: creditorBundle.ebicsBankAccount.bic, - creditorName: creditorBundle.ebicsBankAccount.name, - debtorIban: debitorBundle.ebicsBankAccount.iban, - debtorBic: debitorBundle.ebicsBankAccount.bic, - debtorName: debitorBundle.ebicsBankAccount.name, - subject: subject, - amount: amount, - currency: currency, - uid: getRandomString(), - direction: "CRDT", - }; - await bookPayment2(libeufinSandboxService, req); - } - - export async function simulateIncomingTransaction( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - req: SimulateIncomingTransactionRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL( - `admin/bank-accounts/${accountLabel}/simulate-incoming-transaction`, - baseUrl, - ); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function getAccountTransactions( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ): Promise<SandboxAccountTransactions> { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL( - `admin/bank-accounts/${accountLabel}/transactions`, - baseUrl, - ); - const res = await axios.get(url.href, { - auth: { - username: "admin", - password: "secret", - }, - }); - return res.data as SandboxAccountTransactions; - } - - export async function getCamt053( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ): Promise<any> { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/payments/camt", baseUrl); - return await axios.post(url.href, { - bankaccount: accountLabel, - type: 53, - }, - { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function getAccountInfoWithBalance( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ): Promise<any> { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL( - `admin/bank-accounts/${accountLabel}`, - baseUrl, - ); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "secret", - }, - }); - } -} - -export interface SandboxAccountTransactions { - payments: { - accountLabel: string; - creditorIban: string; - creditorBic?: string; - creditorName: string; - debtorIban: string; - debtorBic: string; - debtorName: string; - amount: string; - currency: string; - subject: string; - date: string; - creditDebitIndicator: "debit" | "credit"; - accountServicerReference: string; - }[]; -} - -export interface CreateEbicsBankConnectionRequest { - name: string; - ebicsURL: string; - hostID: string; - userID: string; - partnerID: string; - systemID?: string; -} - -export interface CreateAnastasisFacadeRequest { - name: string; - connectionName: string; - accountName: string; - currency: string; - reserveTransferLevel: "report" | "statement" | "notification"; -} - - -export interface CreateTalerWireGatewayFacadeRequest { - name: string; - connectionName: string; - accountName: string; - currency: string; - reserveTransferLevel: "report" | "statement" | "notification"; -} - -export interface UpdateNexusUserRequest { - newPassword: string; -} - -export interface NexusAuth { - auth: { - username: string; - password: string; - }; -} - -export interface CreateNexusUserRequest { - username: string; - password: string; -} - -export interface PostNexusTaskRequest { - name: string; - cronspec: string; - type: string; // fetch | submit - params: - | { - level: string; // report | statement | all - rangeType: string; // all | since-last | previous-days | latest - } - | {}; -} - -export interface PostNexusPermissionRequest { - action: "revoke" | "grant"; - permission: { - subjectType: string; - subjectId: string; - resourceType: string; - resourceId: string; - permissionName: string; - }; -} - -export namespace LibeufinNexusApi { - export async function getAllConnections( - nexus: LibeufinNexusServiceInterface, - ): Promise<any> { - let url = new URL("bank-connections", nexus.baseUrl); - const res = await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - return res; - } - - export async function deleteBankConnection( - libeufinNexusService: LibeufinNexusServiceInterface, - req: DeleteBankConnectionRequest, - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("bank-connections/delete-connection", baseUrl); - return await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function createEbicsBankConnection( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateEbicsBankConnectionRequest, - ): Promise<void> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("bank-connections", baseUrl); - await axios.post( - url.href, - { - source: "new", - type: "ebics", - name: req.name, - data: { - ebicsURL: req.ebicsURL, - hostID: req.hostID, - userID: req.userID, - partnerID: req.partnerID, - systemID: req.systemID, - }, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function getBankAccount( - libeufinNexusService: LibeufinNexusServiceInterface, - accountName: string, - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-accounts/${accountName}`, - baseUrl, - ); - return await axios.get( - url.href, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - - export async function submitInitiatedPayment( - libeufinNexusService: LibeufinNexusServiceInterface, - accountName: string, - paymentId: string, - ): Promise<void> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-accounts/${accountName}/payment-initiations/${paymentId}/submit`, - baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function fetchAccounts( - libeufinNexusService: LibeufinNexusServiceInterface, - connectionName: string, - ): Promise<void> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-connections/${connectionName}/fetch-accounts`, - baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function importConnectionAccount( - libeufinNexusService: LibeufinNexusServiceInterface, - connectionName: string, - offeredAccountId: string, - nexusBankAccountId: string, - ): Promise<void> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-connections/${connectionName}/import-account`, - baseUrl, - ); - await axios.post( - url.href, - { - offeredAccountId, - nexusBankAccountId, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function connectBankConnection( - libeufinNexusService: LibeufinNexusServiceInterface, - connectionName: string, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`bank-connections/${connectionName}/connect`, baseUrl); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function getPaymentInitiations( - libeufinNexusService: LibeufinNexusService, - accountName: string, - username: string = "admin", - password: string = "test", - ): Promise<void> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${accountName}/payment-initiations`, - baseUrl, - ); - let response = await axios.get(url.href, { - auth: { - username: username, - password: password, - }, - }); - console.log( - `Payment initiations of: ${accountName}`, - JSON.stringify(response.data, null, 2), - ); - } - - export async function getConfig( - libeufinNexusService: LibeufinNexusService, - ): Promise<void> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/config`, baseUrl); - let response = await axios.get(url.href); - } - - // Uses the Anastasis API to get a list of transactions. - export async function getAnastasisTransactions( - libeufinNexusService: LibeufinNexusService, - anastasisBaseUrl: string, - params: {}, // of the request: {delta: 5, ..} - username: string = "admin", - password: string = "test", - ): Promise<any> { - let url = new URL("history/incoming", anastasisBaseUrl); - let response = await axios.get(url.href, { params: params, - auth: { - username: username, - password: password, - }, - }); - return response; - } - - // FIXME: this function should return some structured - // object that represents a history. - export async function getAccountTransactions( - libeufinNexusService: LibeufinNexusService, - accountName: string, - username: string = "admin", - password: string = "test", - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/bank-accounts/${accountName}/transactions`, baseUrl); - let response = await axios.get(url.href, { - auth: { - username: username, - password: password, - }, - }); - return response; - } - - export async function fetchTransactions( - libeufinNexusService: LibeufinNexusService, - accountName: string, - rangeType: string = "all", - level: string = "report", - username: string = "admin", - password: string = "test", - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${accountName}/fetch-transactions`, - baseUrl, - ); - return await axios.post( - url.href, - { - rangeType: rangeType, - level: level, - }, - { - auth: { - username: username, - password: password, - }, - }, - ); - } - - export async function changePassword( - libeufinNexusService: LibeufinNexusServiceInterface, - username: string, - req: UpdateNexusUserRequest, - auth: NexusAuth, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/users/${username}/password`, baseUrl); - await axios.post(url.href, req, auth); - } - - export async function getUser( - libeufinNexusService: LibeufinNexusServiceInterface, - auth: NexusAuth, - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/user`, baseUrl); - return await axios.get(url.href, auth); - } - - export async function createUser( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateNexusUserRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/users`, baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function getAllPermissions( - libeufinNexusService: LibeufinNexusServiceInterface, - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/permissions`, baseUrl); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function postPermission( - libeufinNexusService: LibeufinNexusServiceInterface, - req: PostNexusPermissionRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/permissions`, baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function getTasks( - libeufinNexusService: LibeufinNexusServiceInterface, - bankAccountName: string, - // When void, the request returns the list of all the - // tasks under this bank account. - taskName: string | void, - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); - if (taskName) url = new URL(taskName, `${url}/`); - - // It's caller's responsibility to interpret the response. - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function deleteTask( - libeufinNexusService: LibeufinNexusServiceInterface, - bankAccountName: string, - taskName: string, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${bankAccountName}/schedule/${taskName}`, - baseUrl, - ); - await axios.delete(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function postTask( - libeufinNexusService: LibeufinNexusServiceInterface, - bankAccountName: string, - req: PostNexusTaskRequest, - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); - return await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function deleteFacade( - libeufinNexusService: LibeufinNexusServiceInterface, - facadeName: string, - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`facades/${facadeName}`, baseUrl); - return await axios.delete(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function getAllFacades( - libeufinNexusService: LibeufinNexusServiceInterface, - ): Promise<any> { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("facades", baseUrl); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function createAnastasisFacade( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateAnastasisFacadeRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("facades", baseUrl); - await axios.post( - url.href, - { - name: req.name, - type: "anastasis", - config: { - bankAccount: req.accountName, - bankConnection: req.connectionName, - currency: req.currency, - reserveTransferLevel: req.reserveTransferLevel, - }, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function createTwgFacade( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateTalerWireGatewayFacadeRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("facades", baseUrl); - await axios.post( - url.href, - { - name: req.name, - type: "taler-wire-gateway", - config: { - bankAccount: req.accountName, - bankConnection: req.connectionName, - currency: req.currency, - reserveTransferLevel: req.reserveTransferLevel, - }, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function submitAllPaymentInitiations( - libeufinNexusService: LibeufinNexusServiceInterface, - accountId: string, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${accountId}/submit-all-payment-initiations`, - baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } -} - -/** - * Launch Nexus and Sandbox AND creates users / facades / bank accounts / - * .. all that's required to start making banking traffic. - */ -export async function launchLibeufinServices( - t: GlobalTestState, - nexusUserBundle: NexusUserBundle[], - sandboxUserBundle: SandboxUserBundle[] = [], - withFacades: string[] = [], // takes only "twg" and/or "anastasis" -): Promise<LibeufinServices> { - const db = await setupDb(t); - - const libeufinSandbox = await LibeufinSandboxService.create(t, { - httpPort: 5010, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - }); - - await libeufinSandbox.start(); - await libeufinSandbox.pingUntilAvailable(); - - const libeufinNexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - - await libeufinNexus.start(); - await libeufinNexus.pingUntilAvailable(); - console.log("Libeufin services launched!"); - - for (let sb of sandboxUserBundle) { - await LibeufinSandboxApi.createEbicsHost( - libeufinSandbox, - sb.ebicsBankAccount.subscriber.hostID, - ); - await LibeufinSandboxApi.createEbicsSubscriber( - libeufinSandbox, - sb.ebicsBankAccount.subscriber, - ); - await LibeufinSandboxApi.createEbicsBankAccount( - libeufinSandbox, - sb.ebicsBankAccount, - ); - } - console.log("Sandbox user(s) / account(s) / subscriber(s): created"); - - for (let nb of nexusUserBundle) { - await LibeufinNexusApi.createEbicsBankConnection(libeufinNexus, nb.connReq); - await LibeufinNexusApi.connectBankConnection( - libeufinNexus, - nb.connReq.name, - ); - await LibeufinNexusApi.fetchAccounts(libeufinNexus, nb.connReq.name); - await LibeufinNexusApi.importConnectionAccount( - libeufinNexus, - nb.connReq.name, - nb.remoteAccountName, - nb.localAccountName, - ); - await LibeufinNexusApi.createUser(libeufinNexus, nb.userReq); - for (let facade of withFacades) { - switch (facade) { - case "twg": - await LibeufinNexusApi.createTwgFacade(libeufinNexus, nb.twgReq); - await LibeufinNexusApi.postPermission( - libeufinNexus, - nb.twgTransferPermission, - ); - await LibeufinNexusApi.postPermission( - libeufinNexus, - nb.twgHistoryPermission, - ); - break; - case "anastasis": - await LibeufinNexusApi.createAnastasisFacade(libeufinNexus, nb.anastasisReq); - } - } - } - console.log( - "Nexus user(s) / connection(s) / facade(s) / permission(s): created", - ); - - return { - commonDb: db, - libeufinNexus: libeufinNexus, - libeufinSandbox: libeufinSandbox, - }; -} - -/** - * Helper function that searches a payment among - * a list, as returned by Nexus. The key is just - * the payment subject. - */ -export function findNexusPayment( - key: string, - payments: LibeufinNexusTransactions, -): LibeufinNexusMoneyMovement | void { - let transactions = payments["transactions"]; - for (let i = 0; i < transactions.length; i++) { - let batches = transactions[i]["batches"]; - for (let y = 0; y < batches.length; y++) { - let movements = batches[y]["batchTransactions"]; - for (let z = 0; z < movements.length; z++) { - let movement = movements[z]; - if (movement["details"]["unstructuredRemittanceInformation"] == key) - return movement; - } - } - } -} diff --git a/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts b/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts deleted file mode 100644 index a93a0ed25..000000000 --- a/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts +++ /dev/null @@ -1,318 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <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 { - ContractTerms, - Duration, - Codec, - buildCodecForObject, - codecForString, - codecOptional, - codecForConstString, - codecForBoolean, - codecForNumber, - codecForContractTerms, - codecForAny, - buildCodecForUnion, - AmountString, - Timestamp, - CoinPublicKeyString, - EddsaPublicKeyString, - codecForAmountString, -} from "@gnu-taler/taler-util"; - -export interface PostOrderRequest { - // The order must at least contain the minimal - // order detail, but can override all - order: Partial<ContractTerms>; - - // if set, the backend will then set the refund deadline to the current - // time plus the specified delay. - refund_delay?: Duration; - - // specifies the payment target preferred by the client. Can be used - // to select among the various (active) wire methods supported by the instance. - payment_target?: string; - - // FIXME: some fields are missing - - // Should a token for claiming the order be generated? - // False can make sense if the ORDER_ID is sufficiently - // high entropy to prevent adversarial claims (like it is - // if the backend auto-generates one). Default is 'true'. - create_token?: boolean; -} - -export type ClaimToken = string; - -export interface PostOrderResponse { - order_id: string; - token?: ClaimToken; -} - -export const codecForPostOrderResponse = (): Codec<PostOrderResponse> => - buildCodecForObject<PostOrderResponse>() - .property("order_id", codecForString()) - .property("token", codecOptional(codecForString())) - .build("PostOrderResponse"); - -export const codecForCheckPaymentPaidResponse = (): Codec<CheckPaymentPaidResponse> => - buildCodecForObject<CheckPaymentPaidResponse>() - .property("order_status_url", codecForString()) - .property("order_status", codecForConstString("paid")) - .property("refunded", codecForBoolean()) - .property("wired", codecForBoolean()) - .property("deposit_total", codecForAmountString()) - .property("exchange_ec", codecForNumber()) - .property("exchange_hc", codecForNumber()) - .property("refund_amount", codecForAmountString()) - .property("contract_terms", codecForContractTerms()) - // FIXME: specify - .property("wire_details", codecForAny()) - .property("wire_reports", codecForAny()) - .property("refund_details", codecForAny()) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentUnpaidResponse = (): Codec<CheckPaymentUnpaidResponse> => - buildCodecForObject<CheckPaymentUnpaidResponse>() - .property("order_status", codecForConstString("unpaid")) - .property("taler_pay_uri", codecForString()) - .property("order_status_url", codecForString()) - .property("already_paid_order_id", codecOptional(codecForString())) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentClaimedResponse = (): Codec<CheckPaymentClaimedResponse> => - buildCodecForObject<CheckPaymentClaimedResponse>() - .property("order_status", codecForConstString("claimed")) - .property("contract_terms", codecForContractTerms()) - .build("CheckPaymentClaimedResponse"); - -export const codecForMerchantOrderPrivateStatusResponse = (): Codec<MerchantOrderPrivateStatusResponse> => - buildCodecForUnion<MerchantOrderPrivateStatusResponse>() - .discriminateOn("order_status") - .alternative("paid", codecForCheckPaymentPaidResponse()) - .alternative("unpaid", codecForCheckPaymentUnpaidResponse()) - .alternative("claimed", codecForCheckPaymentClaimedResponse()) - .build("MerchantOrderPrivateStatusResponse"); - -export type MerchantOrderPrivateStatusResponse = - | CheckPaymentPaidResponse - | CheckPaymentUnpaidResponse - | CheckPaymentClaimedResponse; - -export interface CheckPaymentClaimedResponse { - // Wallet claimed the order, but didn't pay yet. - order_status: "claimed"; - - contract_terms: ContractTerms; -} - -export interface CheckPaymentPaidResponse { - // did the customer pay for this contract - order_status: "paid"; - - // Was the payment refunded (even partially) - refunded: boolean; - - // Did the exchange wire us the funds - wired: boolean; - - // Total amount the exchange deposited into our bank account - // for this contract, excluding fees. - deposit_total: AmountString; - - // Numeric error code indicating errors the exchange - // encountered tracking the wire transfer for this purchase (before - // we even got to specific coin issues). - // 0 if there were no issues. - exchange_ec: number; - - // HTTP status code returned by the exchange when we asked for - // information to track the wire transfer for this purchase. - // 0 if there were no issues. - exchange_hc: number; - - // Total amount that was refunded, 0 if refunded is false. - refund_amount: AmountString; - - // Contract terms - contract_terms: ContractTerms; - - // Ihe wire transfer status from the exchange for this order if available, otherwise empty array - wire_details: TransactionWireTransfer[]; - - // Reports about trouble obtaining wire transfer details, empty array if no trouble were encountered. - wire_reports: TransactionWireReport[]; - - // The refund details for this order. One entry per - // refunded coin; empty array if there are no refunds. - refund_details: RefundDetails[]; - - order_status_url: string; -} - -export interface CheckPaymentUnpaidResponse { - order_status: "unpaid"; - - // URI that the wallet must process to complete the payment. - taler_pay_uri: string; - - order_status_url: string; - - // Alternative order ID which was paid for already in the same session. - // Only given if the same product was purchased before in the same session. - already_paid_order_id?: string; - - // We do we NOT return the contract terms here because they may not - // exist in case the wallet did not yet claim them. -} - -export interface RefundDetails { - // Reason given for the refund - reason: string; - - // when was the refund approved - timestamp: Timestamp; - - // Total amount that was refunded (minus a refund fee). - amount: AmountString; -} - -export interface TransactionWireTransfer { - // Responsible exchange - exchange_url: string; - - // 32-byte wire transfer identifier - wtid: string; - - // execution time of the wire transfer - execution_time: Timestamp; - - // Total amount that has been wire transferred - // to the merchant - amount: AmountString; - - // Was this transfer confirmed by the merchant via the - // POST /transfers API, or is it merely claimed by the exchange? - confirmed: boolean; -} - -export interface TransactionWireReport { - // Numerical error code - code: number; - - // Human-readable error description - hint: string; - - // Numerical error code from the exchange. - exchange_ec: number; - - // HTTP status code received from the exchange. - exchange_hc: number; - - // Public key of the coin for which we got the exchange error. - coin_pub: CoinPublicKeyString; -} - -export interface TippingReserveStatus { - // Array of all known reserves (possibly empty!) - reserves: ReserveStatusEntry[]; -} - -export interface ReserveStatusEntry { - // Public key of the reserve - reserve_pub: string; - - // Timestamp when it was established - creation_time: Timestamp; - - // Timestamp when it expires - expiration_time: Timestamp; - - // Initial amount as per reserve creation call - merchant_initial_amount: AmountString; - - // Initial amount as per exchange, 0 if exchange did - // not confirm reserve creation yet. - exchange_initial_amount: AmountString; - - // Amount picked up so far. - pickup_amount: AmountString; - - // Amount approved for tips that exceeds the pickup_amount. - committed_amount: AmountString; - - // Is this reserve active (false if it was deleted but not purged) - active: boolean; -} - -export interface TipCreateConfirmation { - // Unique tip identifier for the tip that was created. - tip_id: string; - - // taler://tip URI for the tip - taler_tip_uri: string; - - // URL that will directly trigger processing - // the tip when the browser is redirected to it - tip_status_url: string; - - // when does the tip expire - tip_expiration: Timestamp; -} - -export interface TipCreateRequest { - // Amount that the customer should be tipped - amount: AmountString; - - // Justification for giving the tip - justification: string; - - // URL that the user should be directed to after tipping, - // will be included in the tip_token. - next_url: string; -} - -export interface MerchantInstancesResponse { - // List of instances that are present in the backend (see Instance) - instances: MerchantInstanceDetail[]; -} - -export interface MerchantInstanceDetail { - // Merchant name corresponding to this instance. - name: string; - - // Merchant instance this response is about ($INSTANCE) - id: string; - - // Public key of the merchant/instance, in Crockford Base32 encoding. - merchant_pub: EddsaPublicKeyString; - - // List of the payment targets supported by this instance. Clients can - // specify the desired payment target in /order requests. Note that - // front-ends do not have to support wallets selecting payment targets. - payment_targets: string[]; -} diff --git a/packages/taler-wallet-cli/src/harness/sync.ts b/packages/taler-wallet-cli/src/harness/sync.ts deleted file mode 100644 index 16be89eff..000000000 --- a/packages/taler-wallet-cli/src/harness/sync.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { URL } from "@gnu-taler/taler-util"; -import * as fs from "fs"; -import * as util from "util"; -import { - GlobalTestState, - pingProc, - ProcessWrapper, -} from "../harness/harness.js"; -import { Configuration } from "@gnu-taler/taler-util"; - -const exec = util.promisify(require("child_process").exec); - -export interface SyncConfig { - /** - * Human-readable name used in the test harness logs. - */ - name: string; - - httpPort: number; - - /** - * Database connection string (only postgres is supported). - */ - database: string; - - annualFee: string; - - currency: string; - - uploadLimitMb: number; - - /** - * Fulfillment URL used for contract terms related to - * sync. - */ - fulfillmentUrl: string; - - paymentBackendUrl: string; -} - -function setSyncPaths(config: Configuration, home: string) { - config.setString("paths", "sync_home", home); - // We need to make sure that the path of taler_runtime_dir isn't too long, - // as it contains unix domain sockets (108 character limit). - const runDir = fs.mkdtempSync("/tmp/taler-test-"); - config.setString("paths", "sync_runtime_dir", runDir); - config.setString("paths", "sync_data_home", "$SYNC_HOME/.local/share/sync/"); - config.setString("paths", "sync_config_home", "$SYNC_HOME/.config/sync/"); - config.setString("paths", "sync_cache_home", "$SYNC_HOME/.config/sync/"); -} - -export class SyncService { - static async create( - gc: GlobalTestState, - sc: SyncConfig, - ): Promise<SyncService> { - const config = new Configuration(); - - const cfgFilename = gc.testDir + `/sync-${sc.name}.conf`; - setSyncPaths(config, gc.testDir + "/synchome"); - config.setString("taler", "currency", sc.currency); - config.setString("sync", "serve", "tcp"); - config.setString("sync", "port", `${sc.httpPort}`); - config.setString("sync", "db", "postgres"); - config.setString("syncdb-postgres", "config", sc.database); - config.setString("sync", "payment_backend_url", sc.paymentBackendUrl); - config.setString("sync", "upload_limit_mb", `${sc.uploadLimitMb}`); - config.write(cfgFilename); - - return new SyncService(gc, sc, cfgFilename); - } - - proc: ProcessWrapper | undefined; - - get baseUrl(): string { - return `http://localhost:${this.syncConfig.httpPort}/`; - } - - async start(): Promise<void> { - await exec(`sync-dbinit -c "${this.configFilename}"`); - - this.proc = this.globalState.spawnService( - "sync-httpd", - ["-LDEBUG", "-c", this.configFilename], - `sync-${this.syncConfig.name}`, - ); - } - - async pingUntilAvailable(): Promise<void> { - const url = new URL("config", this.baseUrl).href; - await pingProc(this.proc, url, "sync"); - } - - constructor( - private globalState: GlobalTestState, - private syncConfig: SyncConfig, - private configFilename: string, - ) {} -} diff --git a/packages/taler-wallet-cli/src/import-meta-url.js b/packages/taler-wallet-cli/src/import-meta-url.js new file mode 100644 index 000000000..c0e657160 --- /dev/null +++ b/packages/taler-wallet-cli/src/import-meta-url.js @@ -0,0 +1,2 @@ +// Helper to make 'import.meta.url' available in esbuild-bundled code as well. +export const import_meta_url = require("url").pathToFileURL(__filename); diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 142e98e7c..a1b008f5e 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -17,49 +17,59 @@ /** * Imports. */ -import os from "os"; -import fs from "fs"; -import path from "path"; -import { deepStrictEqual } from "assert"; -// Polyfill for encoding which isn't present globally in older nodejs versions -import { TextEncoder, TextDecoder } from "util"; -// @ts-ignore -global.TextEncoder = TextEncoder; -// @ts-ignore -global.TextDecoder = TextDecoder; -import * as clk from "./clk.js"; -import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; import { - PreparePayResultType, - setDangerousTimetravel, - classifyTalerUri, - TalerUriType, - RecoveryMergeStrategy, - Amounts, + AbsoluteTime, addPaytoQueryParams, + AgeRestriction, + AmountString, codecForList, codecForString, + CoreApiResponse, + Duration, + encodeCrock, + getErrorDetailFromException, + getRandomBytes, + InitRequest, + j2s, Logger, - Configuration, - decodeCrock, - rsaBlind, + NotificationType, + parsePaytoUri, + parseTalerUri, + PreparePayResultType, + sampleWalletCoreTransactions, + setDangerousTimetravel, + setGlobalLogLevelFromString, + summarizeTalerErrorDetail, + TalerUriAction, + TransactionIdStr, + WalletNotification, } from "@gnu-taler/taler-util"; +import { clk } from "@gnu-taler/taler-util/clk"; +import { + getenv, + pathHomedir, + processExit, + readFile, + readlinePrompt, + setUnhandledRejectionHandler, +} from "@gnu-taler/taler-util/compat"; +import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; +import { JsonMessage, runRpcServer } from "@gnu-taler/taler-util/twrpc"; import { - NodeHttpLib, - getDefaultNodeWallet, - OperationFailedAndReportedError, - OperationFailedError, - NodeThreadCryptoWorkerFactory, - CryptoApi, - walletCoreDebugFlags, + AccessStats, + createNativeWalletHost2, + nativeCrypto, + Wallet, WalletApiOperation, WalletCoreApiClient, - Wallet, } from "@gnu-taler/taler-wallet-core"; -import { lintExchangeDeployment } from "./lint.js"; -import { runBench1 } from "./bench1.js"; -import { runEnv1 } from "./env1.js"; -import { GlobalTestState, runTestWithState } from "./harness/harness.js"; +import { + createRemoteWallet, + getClientFromRemoteWallet, + makeNotificationWaiter, +} from "@gnu-taler/taler-wallet-core/remote"; + +import * as fs from "node:fs"; // This module also serves as the entry point for the crypto // thread worker, and thus must expose these two handlers. @@ -70,7 +80,19 @@ export { const logger = new Logger("taler-wallet-cli.ts"); -const defaultWalletDbPath = os.homedir + "/" + ".talerwalletdb.json"; +let observabilityEventFile: string | undefined = undefined; + +const EXIT_EXCEPTION = 4; +const EXIT_API_ERROR = 5; + +setUnhandledRejectionHandler((error: any) => { + logger.error("unhandledRejection", error.message); + logger.error("stack", error.stack); + processExit(1); +}); + +const defaultWalletDbPath = pathHomedir() + "/" + ".talerwalletdb.sqlite3"; +const defaultWalletCoreSocket = pathHomedir() + "/" + ".wallet-core.sock"; function assertUnreachable(x: never): never { throw new Error("Didn't expect to get here"); @@ -87,7 +109,7 @@ async function doPay( if (result.status === PreparePayResultType.InsufficientBalance) { console.log("contract", result.contractTerms); console.error("insufficient balance"); - process.exit(1); + processExit(1); return; } if (result.status === PreparePayResultType.AlreadyConfirmed) { @@ -96,8 +118,7 @@ async function doPay( } else { console.log("payment already in progress"); } - - process.exit(0); + processExit(0); return; } if (result.status === "payment-possible") { @@ -139,11 +160,11 @@ function applyVerbose(verbose: boolean): void { // TODO } +declare const __VERSION__: string; +declare const __GIT_HASH__: string; function printVersion(): void { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const info = require("../package.json"); - console.log(`${info.version}`); - process.exit(0); + console.log(`${__VERSION__} ${__GIT_HASH__}`); + processExit(0); } export const walletCli = clk @@ -151,7 +172,10 @@ export const walletCli = clk help: "Command line interface for the GNU Taler wallet.", }) .maybeOption("walletDbFile", ["--wallet-db"], clk.STRING, { - help: "location of the wallet database file", + help: "Location of the wallet database file", + }) + .maybeOption("walletConnection", ["--wallet-connection"], clk.STRING, { + help: "Connect to an RPC wallet", }) .maybeOption("timetravel", ["--timetravel"], clk.INT, { help: "modify system time by given offset in microseconds", @@ -161,61 +185,188 @@ export const walletCli = clk setDangerousTimetravel(x / 1000); }, }) + .maybeOption("cryptoWorker", ["--crypto-worker"], clk.STRING, { + help: "Override crypto worker implementation type.", + }) + .maybeOption("log", ["-L", "--log"], clk.STRING, { + help: "configure log level (NONE, ..., TRACE)", + onPresentHandler: (x) => { + setGlobalLogLevelFromString(x); + }, + }) .maybeOption("inhibit", ["--inhibit"], clk.STRING, { - help: - "Inhibit running certain operations, useful for debugging and testing.", + help: "Inhibit running certain operations, useful for debugging and testing.", }) .flag("noThrottle", ["--no-throttle"], { help: "Don't do any request throttling.", }) + .flag("noHttp", ["--no-http"], { + help: "Allow unsafe http connections.", + }) .flag("version", ["-v", "--version"], { onPresentHandler: printVersion, }) .flag("verbose", ["-V", "--verbose"], { help: "Enable verbose output.", + }) + .flag("skipDefaults", ["--skip-defaults"], { + help: "Skip configuring default exchanges.", }); type WalletCliArgsType = clk.GetArgType<typeof walletCli>; -async function withWallet<T>( +function checkEnvFlag(name: string): boolean { + const val = getenv(name); + if (val == "1") { + return true; + } + return false; +} + +export interface WalletContext { + /** + * High-level client for making API requests to wallet-core. + */ + client: WalletCoreApiClient; + + /** + * Low-level interface for making API requests to wallet-core. + */ + makeCoreApiRequest( + operation: string, + payload: unknown, + ): Promise<CoreApiResponse>; + + /** + * Return a promise that resolves after the wallet has emitted a notification + * that meets the criteria of the "cond" predicate. + */ + waitForNotificationCond<T>( + cond: (n: WalletNotification) => T | false | undefined, + ): Promise<T>; +} + +interface CreateWalletResult { + wallet: Wallet; + getStats: () => AccessStats; +} + +async function createLocalWallet( walletCliArgs: WalletCliArgsType, - f: (w: { client: WalletCoreApiClient; ws: Wallet }) => Promise<T>, -): Promise<T> { + args: WalletRunArgs, + notificationHandler?: (n: WalletNotification) => void, +): Promise<CreateWalletResult> { const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath; - const myHttpLib = new NodeHttpLib(); - if (walletCliArgs.wallet.noThrottle) { - myHttpLib.setThrottling(false); - } - const wallet = await getDefaultNodeWallet({ - persistentStoragePath: dbPath, + const myHttpLib = createPlatformHttpLib({ + enableThrottling: walletCliArgs.wallet.noThrottle ? false : true, + requireTls: walletCliArgs.wallet.noHttp, + }); + const wh = await createNativeWalletHost2({ + persistentStoragePath: dbPath !== ":memory:" ? dbPath : undefined, httpLib: myHttpLib, + notifyHandler: (n) => { + logger.info(`wallet notification: ${j2s(n)}`); + if (notificationHandler) { + notificationHandler(n); + } + }, + cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any, }); + applyVerbose(walletCliArgs.wallet.verbose); + const res = { wallet: wh.wallet, getStats: wh.getDbStats }; + + if (args.noInit) { + return res; + } try { - const w = { - ws: wallet, - client: wallet.client, - }; - await wallet.handleCoreApiRequest("initWallet", "native-init", {}); - const ret = await f(w); - return ret; + await wh.wallet.handleCoreApiRequest( + WalletApiOperation.InitWallet, + "native-init", + { + config: { + lazyTaskLoop: args.lazyTaskLoop, + testing: { + devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"), + denomselAllowLate: checkEnvFlag( + "TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE", + ), + emitObservabilityEvents: observabilityEventFile != null, + skipDefaults: walletCliArgs.wallet.skipDefaults, + }, + }, + } satisfies InitRequest, + ); + return res; } catch (e) { - if ( - e instanceof OperationFailedAndReportedError || - e instanceof OperationFailedError - ) { - console.error("Operation failed: " + e.message); - console.error( - "Error details:", - JSON.stringify(e.operationError, undefined, 2), - ); - } else { - console.error("caught unhandled exception (bug?):", e); + const ed = getErrorDetailFromException(e); + console.error("Operation failed: " + summarizeTalerErrorDetail(ed)); + console.error("Error details:", JSON.stringify(ed, undefined, 2)); + processExit(1); + } +} + +function writeObservabilityLog(notif: WalletNotification): void { + if (observabilityEventFile) { + switch (notif.type) { + case NotificationType.RequestObservabilityEvent: + case NotificationType.TaskObservabilityEvent: + fs.appendFileSync(observabilityEventFile, JSON.stringify(notif) + "\n"); + break; + } + } +} + +export interface WalletRunArgs { + lazyTaskLoop?: boolean; + noInit?: boolean; +} + +async function withWallet<T>( + walletCliArgs: WalletCliArgsType, + args: WalletRunArgs = {}, + f: (ctx: WalletContext) => Promise<T>, +): Promise<T> { + const waiter = makeNotificationWaiter(); + + const onNotif = (notif: WalletNotification) => { + waiter.notify(notif); + writeObservabilityLog(notif); + }; + + if (walletCliArgs.wallet.walletConnection) { + logger.info("creating remote wallet"); + const w = await createRemoteWallet({ + name: "wallet", + notificationHandler: onNotif, + socketFilename: walletCliArgs.wallet.walletConnection, + }); + const ctx: WalletContext = { + makeCoreApiRequest(operation, payload) { + return w.makeCoreApiRequest(operation, payload); + }, + client: getClientFromRemoteWallet(w), + waitForNotificationCond: waiter.waitForNotificationCond, + }; + const res = await f(ctx); + w.close(); + return res; + } else { + const wh = await createLocalWallet(walletCliArgs, args, onNotif); + const ctx: WalletContext = { + client: wh.wallet.client, + waitForNotificationCond: waiter.waitForNotificationCond, + makeCoreApiRequest(operation, payload) { + return wh.wallet.handleCoreApiRequest(operation, "my-req", payload); + }, + }; + const result = await f(ctx); + await wh.wallet.client.call(WalletApiOperation.Shutdown, {}); + if (process.env.TALER_WALLET_DBSTATS) { + console.log("database stats:"); + console.log(j2s(wh.getStats())); } - process.exit(1); - } finally { - logger.info("operation with wallet finished, stopping"); - wallet.stop(); + return result; } } @@ -225,92 +376,229 @@ walletCli help: "Show raw JSON.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { - const balance = await wallet.client.call( - WalletApiOperation.GetBalances, - {}, - ); - console.log(JSON.stringify(balance, undefined, 2)); - }); + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { + const balance = await wallet.client.call( + WalletApiOperation.GetBalances, + {}, + ); + console.log(JSON.stringify(balance, undefined, 2)); + }, + ); }); walletCli .subcommand("api", "api", { help: "Call the wallet-core API directly." }) .requiredArgument("operation", clk.STRING) .requiredArgument("request", clk.STRING) + .flag("expectSuccess", ["--expect-success"], { + help: "Exit with non-zero status code when request fails instead of returning error JSON.", + }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, {}, async (wallet) => { let requestJson; + logger.info(`handling 'api' request (${args.api.operation})`); + const jsonContent = args.api.request.startsWith("@") + ? readFile(args.api.request.substring(1)) + : args.api.request; try { - requestJson = JSON.parse(args.api.request); + requestJson = JSON.parse(jsonContent); } catch (e) { console.error("Invalid JSON"); - process.exit(1); + processExit(1); + } + try { + const resp = await wallet.makeCoreApiRequest( + args.api.operation, + requestJson, + ); + console.log(JSON.stringify(resp, undefined, 2)); + if (resp.type === "error") { + if (args.api.expectSuccess) { + processExit(EXIT_API_ERROR); + } + } + } catch (e) { + logger.error(`Got exception while handling API request ${e}`); + processExit(EXIT_EXCEPTION); } - const resp = await wallet.ws.handleCoreApiRequest( - args.api.operation, - "reqid-1", - requestJson, - ); - console.log(JSON.stringify(resp, undefined, 2)); }); + logger.info("finished handling API request"); }); -walletCli - .subcommand("", "pending", { help: "Show pending operations." }) - .action(async (args) => { - await withWallet(args, async (wallet) => { +const transactionsCli = walletCli + .subcommand("transactions", "transactions", { help: "Manage transactions." }) + .maybeOption("currency", ["--currency"], clk.STRING, { + help: "Filter by currency.", + }) + .maybeOption("search", ["--search"], clk.STRING, { + help: "Filter by search string", + }) + .flag("includeRefreshes", ["--include-refreshes"]); + +// Default action +transactionsCli.action(async (args) => { + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { const pending = await wallet.client.call( - WalletApiOperation.GetPendingOperations, - {}, + WalletApiOperation.GetTransactions, + { + currency: args.transactions.currency, + search: args.transactions.search, + includeRefreshes: args.transactions.includeRefreshes, + sort: "stable-ascending", + }, ); console.log(JSON.stringify(pending, undefined, 2)); + }, + ); +}); + +transactionsCli + .subcommand("deleteTransaction", "delete", { + help: "Permanently delete a transaction from the transaction list.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to delete", + }) + .action(async (args) => { + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { + await wallet.client.call(WalletApiOperation.DeleteTransaction, { + transactionId: args.deleteTransaction + .transactionId as TransactionIdStr, + }); + }, + ); + }); + +transactionsCli + .subcommand("suspendTransaction", "suspend", { + help: "Suspend a transaction.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to suspend.", + }) + .action(async (args) => { + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { + await wallet.client.call(WalletApiOperation.SuspendTransaction, { + transactionId: args.suspendTransaction + .transactionId as TransactionIdStr, + }); + }, + ); + }); + +transactionsCli + .subcommand("fail", "fail", { + help: "Fail a transaction (when it can't be aborted).", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to fail.", + }) + .action(async (args) => { + await withWallet( + args, + { + lazyTaskLoop: true, + }, + async (wallet) => { + await wallet.client.call(WalletApiOperation.FailTransaction, { + transactionId: args.fail.transactionId as TransactionIdStr, + }); + }, + ); + }); + +transactionsCli + .subcommand("resumeTransaction", "resume", { + help: "Resume a transaction.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to suspend.", + }) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + await wallet.client.call(WalletApiOperation.ResumeTransaction, { + transactionId: args.resumeTransaction.transactionId as TransactionIdStr, + }); }); }); -walletCli - .subcommand("transactions", "transactions", { help: "Show transactions." }) - .maybeOption("currency", ["--currency"], clk.STRING) - .maybeOption("search", ["--search"], clk.STRING) +transactionsCli + .subcommand("lookup", "lookup", { + help: "Look up a single transaction based on the transaction identifier.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to delete", + }) .action(async (args) => { - await withWallet(args, async (wallet) => { - const pending = await wallet.client.call( - WalletApiOperation.GetTransactions, + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const tx = await wallet.client.call( + WalletApiOperation.GetTransactionById, { - currency: args.transactions.currency, - search: args.transactions.search, + transactionId: args.lookup.transactionId, }, ); - console.log(JSON.stringify(pending, undefined, 2)); + console.log(j2s(tx)); }); }); -async function asyncSleep(milliSeconds: number): Promise<void> { - return new Promise<void>((resolve, reject) => { - setTimeout(() => resolve(), milliSeconds); +transactionsCli + .subcommand("abortTransaction", "abort", { + help: "Abort a transaction.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to delete", + }) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + await wallet.client.call(WalletApiOperation.AbortTransaction, { + transactionId: args.abortTransaction.transactionId as TransactionIdStr, + }); + }); }); -} walletCli - .subcommand("runPendingOpt", "run-pending", { - help: "Run pending operations.", + .subcommand("version", "version", { + help: "Show version details.", }) - .flag("forceNow", ["-f", "--force-now"]) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.ws.runPending(args.runPendingOpt.forceNow); + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const versionInfo = await wallet.client.call( + WalletApiOperation.GetVersion, + {}, + ); + console.log(j2s(versionInfo)); }); }); -walletCli - .subcommand("retryTransaction", "retry-transaction", { +transactionsCli + .subcommand("retryTransaction", "retry", { help: "Retry a transaction.", }) .requiredArgument("transactionId", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.RetryTransaction, { - transactionId: args.retryTransaction.transactionId, + transactionId: args.retryTransaction.transactionId as TransactionIdStr, }); }); }); @@ -319,29 +607,76 @@ walletCli .subcommand("finishPendingOpt", "run-until-done", { help: "Run until no more work is left.", }) - .maybeOption("maxRetries", ["--max-retries"], clk.INT) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.ws.runTaskLoop({ - maxRetries: args.finishPendingOpt.maxRetries, - stopWhenDone: true, - }); - wallet.ws.stop(); + await withWallet(args, { lazyTaskLoop: false }, async (ctx) => { + await ctx.client.call(WalletApiOperation.TestingWaitTasksDone, {}); }); }); -walletCli - .subcommand("deleteTransaction", "delete-transaction", { - help: "Permanently delete a transaction from the transaction list.", - }) - .requiredArgument("transactionId", clk.STRING, { - help: "Identifier of the transaction to delete", - }) +const withdrawCli = walletCli.subcommand("withdraw", "withdraw", { + help: "Withdraw with a taler://withdraw/ URI", +}); + +withdrawCli + .subcommand("withdrawCheckUri", "check-uri") + .requiredArgument("uri", clk.STRING) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.DeleteTransaction, { - transactionId: args.deleteTransaction.transactionId, - }); + const uri = args.withdrawCheckUri.uri; + const restrictAge = args.withdrawCheckUri.restrictAge; + console.log(`age restriction requested (${restrictAge})`); + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const withdrawInfo = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: uri, + restrictAge, + }, + ); + console.log("withdrawInfo", withdrawInfo); + }); + }); + +withdrawCli + .subcommand("withdrawCheckAmount", "check-amount") + .requiredArgument("exchange", clk.STRING) + .requiredArgument("amount", clk.AMOUNT) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) + .action(async (args) => { + const restrictAge = args.withdrawCheckAmount.restrictAge; + console.log(`age restriction requested (${restrictAge})`); + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const withdrawInfo = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + amount: args.withdrawCheckAmount.amount, + exchangeBaseUrl: args.withdrawCheckAmount.exchange, + restrictAge, + }, + ); + console.log("withdrawInfo", withdrawInfo); + }); + }); + +withdrawCli + .subcommand("withdrawAcceptUri", "accept-uri") + .requiredArgument("uri", clk.STRING) + .requiredOption("exchange", ["--exchange"], clk.STRING) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) + .action(async (args) => { + const uri = args.withdrawAcceptUri.uri; + const restrictAge = args.withdrawAcceptUri.restrictAge; + console.log(`age restriction requested (${restrictAge})`); + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const res = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: args.withdrawAcceptUri.exchange, + talerWithdrawUri: uri, + restrictAge, + }, + ); + console.log(j2s(res)); }); }); @@ -349,69 +684,125 @@ walletCli .subcommand("handleUri", "handle-uri", { help: "Handle a taler:// URI.", }) - .requiredArgument("uri", clk.STRING) + .maybeArgument("uri", clk.STRING) + .maybeOption("withdrawalExchange", ["--withdrawal-exchange"], clk.STRING, { + help: "Exchange to use for withdrawal operations.", + }) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) .flag("autoYes", ["-y", "--yes"]) .action(async (args) => { - await withWallet(args, async (wallet) => { - const uri: string = args.handleUri.uri; - const uriType = classifyTalerUri(uri); - switch (uriType) { - case TalerUriType.TalerPay: + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + let uri; + if (args.handleUri.uri) { + uri = args.handleUri.uri; + } else { + uri = await readlinePrompt("Taler URI: "); + } + const parsedTalerUri = parseTalerUri(uri); + if (!parsedTalerUri) { + throw Error("invalid taler URI"); + } + switch (parsedTalerUri.type) { + case TalerUriAction.Pay: await doPay(wallet.client, uri, { alwaysYes: args.handleUri.autoYes, }); break; - case TalerUriType.TalerTip: - { - const res = await wallet.client.call( - WalletApiOperation.PrepareTip, - { - talerTipUri: uri, - }, - ); - console.log("tip status", res); - await wallet.client.call(WalletApiOperation.AcceptTip, { - walletTipId: res.walletTipId, - }); - } - break; - case TalerUriType.TalerRefund: - await wallet.client.call(WalletApiOperation.ApplyRefund, { + case TalerUriAction.Refund: + await wallet.client.call(WalletApiOperation.StartRefundQueryForUri, { talerRefundUri: uri, }); break; - case TalerUriType.TalerWithdraw: - { - const withdrawInfo = await wallet.client.call( - WalletApiOperation.GetWithdrawalDetailsForUri, - { - talerWithdrawUri: uri, - }, - ); - console.log("withdrawInfo", withdrawInfo); - const selectedExchange = withdrawInfo.defaultExchangeBaseUrl; - if (!selectedExchange) { - console.error("no suggested exchange!"); - process.exit(1); - return; - } - const res = await wallet.client.call( - WalletApiOperation.AcceptBankIntegratedWithdrawal, - { - exchangeBaseUrl: selectedExchange, - talerWithdrawUri: uri, - }, + case TalerUriAction.Withdraw: { + const withdrawInfo = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: uri, + restrictAge: args.handleUri.restrictAge, + }, + ); + console.log("withdrawInfo", withdrawInfo); + const selectedExchange = + args.handleUri.withdrawalExchange ?? + withdrawInfo.defaultExchangeBaseUrl; + if (!selectedExchange) { + console.error( + "no exchange specified for withdrawal (and no exchange suggested by the bank)", ); + processExit(1); + return; } + const res = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: selectedExchange, + talerWithdrawUri: uri, + }, + ); + console.log("accept withdrawal response", res); + break; + } + case TalerUriAction.DevExperiment: { + await wallet.client.call(WalletApiOperation.ApplyDevExperiment, { + devExperimentUri: uri, + }); break; + } default: - console.log(`URI type (${uriType}) not handled`); + console.log(`URI type (${parsedTalerUri.type}) not handled`); break; } return; }); }); +withdrawCli + .subcommand("withdrawManually", "manual", { + help: "Withdraw manually from an exchange.", + }) + .requiredOption("exchange", ["--exchange"], clk.STRING, { + help: "Base URL of the exchange.", + }) + .requiredOption("amount", ["--amount"], clk.AMOUNT, { + help: "Amount to withdraw", + }) + .maybeOption("forcedReservePriv", ["--forced-reserve-priv"], clk.STRING, {}) + .maybeOption("restrictAge", ["--restrict-age"], clk.INT) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const exchangeBaseUrl = args.withdrawManually.exchange; + const amount = args.withdrawManually.amount; + const d = await wallet.client.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + amount: args.withdrawManually.amount, + exchangeBaseUrl: exchangeBaseUrl, + }, + ); + const acct = d.withdrawalAccountsList[0]; + if (!acct) { + console.log("exchange has no accounts"); + return; + } + const resp = await wallet.client.call( + WalletApiOperation.AcceptManualWithdrawal, + { + amount, + exchangeBaseUrl, + restrictAge: parseInt(String(args.withdrawManually.restrictAge), 10), + forceReservePriv: args.withdrawManually.forcedReservePriv, + }, + ); + const reservePub = resp.reservePub; + const completePaytoUri = addPaytoQueryParams(acct.paytoUri, { + amount: args.withdrawManually.amount, + message: `Taler top-up ${reservePub}`, + }); + console.log("Created reserve", reservePub); + console.log("Payto URI", completePaytoUri); + }); + }); + const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", { help: "Manage exchanges.", }); @@ -422,7 +813,7 @@ exchangesCli }) .action(async (args) => { console.log("Listing exchanges ..."); - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const exchanges = await wallet.client.call( WalletApiOperation.ListExchanges, {}, @@ -440,15 +831,34 @@ exchangesCli }) .flag("force", ["-f", "--force"]) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.AddExchange, { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + await wallet.client.call(WalletApiOperation.UpdateExchangeEntry, { exchangeBaseUrl: args.exchangesUpdateCmd.url, - forceUpdate: args.exchangesUpdateCmd.force, + force: args.exchangesUpdateCmd.force, }); }); }); exchangesCli + .subcommand("exchangesShowCmd", "show", { + help: "Show exchange details", + }) + .requiredArgument("url", clk.STRING, { + help: "Base URL of the exchange.", + }) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.GetExchangeDetailedInfo, + { + exchangeBaseUrl: args.exchangesShowCmd.url, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +exchangesCli .subcommand("exchangesAddCmd", "add", { help: "Add an exchange by base URL.", }) @@ -456,7 +866,7 @@ exchangesCli help: "Base URL of the exchange.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: args.exchangesAddCmd.url, }); @@ -464,19 +874,32 @@ exchangesCli }); exchangesCli + .subcommand("exchangesAddCmd", "delete", { + help: "Delete an exchange by base URL.", + }) + .requiredArgument("url", clk.STRING, { + help: "Base URL of the exchange.", + }) + .flag("purge", ["--purge"]) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + await wallet.client.call(WalletApiOperation.DeleteExchange, { + exchangeBaseUrl: args.exchangesAddCmd.url, + purge: args.exchangesAddCmd.purge, + }); + }); + }); + +exchangesCli .subcommand("exchangesAcceptTosCmd", "accept-tos", { help: "Accept terms of service.", }) .requiredArgument("url", clk.STRING, { help: "Base URL of the exchange.", }) - .requiredArgument("etag", clk.STRING, { - help: "ToS version tag to accept", - }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.SetExchangeTosAccepted, { - etag: args.exchangesAcceptTosCmd.etag, exchangeBaseUrl: args.exchangesAcceptTosCmd.url, }); }); @@ -484,17 +907,26 @@ exchangesCli exchangesCli .subcommand("exchangesTosCmd", "tos", { - help: "Show terms of service.", + help: "Show/request terms of service.", }) .requiredArgument("url", clk.STRING, { help: "Base URL of the exchange.", }) + .maybeOption("contentTypes", ["--content-type"], clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + let acceptedFormat: string[] | undefined = undefined; + if (args.exchangesTosCmd.contentTypes) { + const split = args.exchangesTosCmd.contentTypes + .split(",") + .map((x) => x.trim()); + acceptedFormat = split; + } + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const tosResult = await wallet.client.call( WalletApiOperation.GetExchangeTos, { exchangeBaseUrl: args.exchangesTosCmd.url, + acceptedFormat, }, ); console.log(JSON.stringify(tosResult, undefined, 2)); @@ -505,107 +937,83 @@ const backupCli = walletCli.subcommand("backupArgs", "backup", { help: "Subcommands for backups", }); -backupCli - .subcommand("setDeviceId", "set-device-id") - .requiredArgument("deviceId", clk.STRING, { - help: "new device ID", - }) - .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.SetWalletDeviceId, { - walletDeviceId: args.setDeviceId.deviceId, - }); - }); - }); - -backupCli.subcommand("exportPlain", "export-plain").action(async (args) => { - await withWallet(args, async (wallet) => { - const backup = await wallet.client.call( - WalletApiOperation.ExportBackupPlain, - {}, - ); +backupCli.subcommand("exportDb", "export-db").action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const backup = await wallet.client.call(WalletApiOperation.ExportDb, {}); console.log(JSON.stringify(backup, undefined, 2)); }); }); -backupCli.subcommand("recoverySave", "save-recovery").action(async (args) => { - await withWallet(args, async (wallet) => { - const recoveryJson = await wallet.client.call( - WalletApiOperation.ExportBackupRecovery, +backupCli.subcommand("storeBackup", "store").action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.CreateStoredBackup, {}, ); - console.log(JSON.stringify(recoveryJson, undefined, 2)); + console.log(JSON.stringify(resp, undefined, 2)); }); }); -backupCli.subcommand("run", "run").action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); - }); -}); - -backupCli.subcommand("status", "status").action(async (args) => { - await withWallet(args, async (wallet) => { - const status = await wallet.client.call( - WalletApiOperation.GetBackupInfo, +backupCli.subcommand("storeBackup", "list-stored").action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.ListStoredBackups, {}, ); - console.log(JSON.stringify(status, undefined, 2)); + console.log(JSON.stringify(resp, undefined, 2)); }); }); backupCli - .subcommand("recoveryLoad", "load-recovery") - .maybeOption("strategy", ["--strategy"], clk.STRING, { - help: - "Strategy for resolving a conflict with the existing wallet key ('theirs' or 'ours')", - }) + .subcommand("storeBackup", "delete-stored") + .requiredArgument("name", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { - const data = JSON.parse(await read(process.stdin)); - let strategy: RecoveryMergeStrategy | undefined; - const stratStr = args.recoveryLoad.strategy; - if (stratStr) { - if (stratStr === "theirs") { - strategy = RecoveryMergeStrategy.Theirs; - } else if (stratStr === "ours") { - strategy = RecoveryMergeStrategy.Theirs; - } else { - throw Error("invalid recovery strategy"); - } - } - await wallet.client.call(WalletApiOperation.ImportBackupRecovery, { - recovery: data, - strategy, - }); + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.DeleteStoredBackup, + { + name: args.storeBackup.name, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); }); }); backupCli - .subcommand("addProvider", "add-provider") - .requiredArgument("url", clk.STRING) - .maybeArgument("name", clk.STRING) - .flag("activate", ["--activate"]) + .subcommand("recoverBackup", "recover-stored") + .requiredArgument("name", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.AddBackupProvider, { - backupProviderBaseUrl: args.addProvider.url, - activate: args.addProvider.activate, - name: args.addProvider.name || args.addProvider.url, - }); + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.RecoverStoredBackup, + { + name: args.recoverBackup.name, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); }); }); +backupCli.subcommand("importDb", "import-db").action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const dumpRaw = await read(process.stdin); + const dump = JSON.parse(dumpRaw); + await wallet.client.call(WalletApiOperation.ImportDb, { + dump, + }); + }); +}); + const depositCli = walletCli.subcommand("depositArgs", "deposit", { help: "Subcommands for depositing money to payto:// accounts", }); depositCli .subcommand("createDepositArgs", "create") - .requiredArgument("amount", clk.STRING) + .requiredArgument("amount", clk.AMOUNT) .requiredArgument("targetPayto", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.CreateDepositGroup, { @@ -614,19 +1022,190 @@ depositCli }, ); console.log(`Created deposit ${resp.depositGroupId}`); - await wallet.ws.runPending(); }); }); -depositCli - .subcommand("trackDepositArgs", "track") - .requiredArgument("depositGroupId", clk.STRING) +const peerCli = walletCli.subcommand("peerArgs", "p2p", { + help: "Subcommands for peer-to-peer payments.", +}); + +peerCli + .subcommand("checkPayPush", "check-push-debit", { + help: "Check fees for starting a peer-push debit transaction.", + }) + .requiredArgument("amount", clk.AMOUNT, { + help: "Amount to pay", + }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const resp = await wallet.client.call( - WalletApiOperation.TrackDepositGroup, + WalletApiOperation.CheckPeerPushDebit, { - depositGroupId: args.trackDepositArgs.depositGroupId, + amount: args.checkPayPush.amount, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("checkPayPull", "check-pull-credit", { + help: "Check fees for a starting peer-pull credit transaction.", + }) + .requiredArgument("amount", clk.AMOUNT, { + help: "Amount to request", + }) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.CheckPeerPullCredit, + { + amount: args.checkPayPull.amount, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("prepareIncomingPayPull", "prepare-pull-debit") + .requiredArgument("talerUri", clk.STRING) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.PreparePeerPullDebit, + { + talerUri: args.prepareIncomingPayPull.talerUri, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("confirmIncomingPayPull", "confirm-pull-debit") + .requiredArgument("transactionId", clk.STRING) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.ConfirmPeerPullDebit, + { + transactionId: args.confirmIncomingPayPull + .transactionId as TransactionIdStr, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("confirmIncomingPayPush", "confirm-push-credit") + .requiredArgument("transactionId", clk.STRING) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.ConfirmPeerPushCredit, + { + transactionId: args.confirmIncomingPayPush.transactionId, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("initiatePayPull", "initiate-pull-credit", { + help: "Initiate a peer-pull payment.", + }) + .requiredArgument("amount", clk.AMOUNT, { + help: "Amount to request", + }) + .maybeOption("summary", ["--summary"], clk.STRING, { + help: "Summary to use in the contract terms.", + }) + .maybeOption("purseExpiration", ["--purse-expiration"], clk.STRING) + .maybeOption("exchangeBaseUrl", ["--exchange"], clk.STRING) + .action(async (args) => { + let purseExpiration: AbsoluteTime; + + if (args.initiatePayPull.purseExpiration) { + purseExpiration = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromPrettyString(args.initiatePayPull.purseExpiration), + ); + } else { + purseExpiration = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ hours: 1 }), + ); + } + + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.InitiatePeerPullCredit, + { + exchangeBaseUrl: args.initiatePayPull.exchangeBaseUrl, + partialContractTerms: { + amount: args.initiatePayPull.amount, + summary: args.initiatePayPull.summary ?? "Invoice", + purse_expiration: AbsoluteTime.toProtocolTimestamp(purseExpiration), + }, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("preparePushCredit", "prepare-push-credit") + .requiredArgument("talerUri", clk.STRING) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.PreparePeerPushCredit, + { + talerUri: args.preparePushCredit.talerUri, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +peerCli + .subcommand("payPush", "initiate-push-debit", { + help: "Initiate a peer-push payment.", + }) + .requiredArgument("amount", clk.AMOUNT, { + help: "Amount to pay", + }) + .maybeOption("summary", ["--summary"], clk.STRING, { + help: "Summary to use in the contract terms.", + }) + .maybeOption("purseExpiration", ["--purse-expiration"], clk.STRING) + .action(async (args) => { + let purseExpiration: AbsoluteTime; + + if (args.payPush.purseExpiration) { + purseExpiration = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromPrettyString(args.payPush.purseExpiration), + ); + } else { + purseExpiration = AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ hours: 1 }), + ); + } + + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.InitiatePeerPushDebit, + { + partialContractTerms: { + amount: args.payPush.amount, + summary: args.payPush.summary ?? "Payment", + purse_expiration: AbsoluteTime.toProtocolTimestamp(purseExpiration), + }, }, ); console.log(JSON.stringify(resp, undefined, 2)); @@ -634,154 +1213,333 @@ depositCli }); const advancedCli = walletCli.subcommand("advancedArgs", "advanced", { - help: - "Subcommands for advanced operations (only use if you know what you're doing!).", + help: "Subcommands for advanced operations (only use if you know what you're doing!).", }); advancedCli - .subcommand("bench1", "bench1", { - help: "Run the 'bench1' benchmark", + .subcommand("genReserve", "gen-reserve", { + help: "Generate a reserve key pair (not stored in the DB).", }) - .requiredOption("configJson", ["--config-json"], clk.STRING) .action(async (args) => { - let config: any; - try { - config = JSON.parse(args.bench1.configJson); - } catch (e) { - console.log("Could not parse config JSON"); - } - await runBench1(config); + const pair = await nativeCrypto.createEddsaKeypair({}); + console.log( + j2s({ + reservePub: pair.pub, + reservePriv: pair.priv, + }), + ); }); advancedCli - .subcommand("env1", "env1", { - help: "Run a test environment for bench1", + .subcommand("tasks", "tasks", { + help: "Show active wallet-core tasks.", }) .action(async (args) => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-")); - const testState = new GlobalTestState({ - testDir, + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const tasks = await wallet.client.call( + WalletApiOperation.GetActiveTasks, + {}, + ); + console.log(j2s(tasks)); }); - await runTestWithState(testState, runEnv1, "env1", true); }); advancedCli - .subcommand("withdrawFakebank", "withdraw-fakebank", { - help: "Withdraw via a fakebank.", + .subcommand("sampleTransactions", "sample-transactions", { + help: "Print sample wallet-core transactions", }) - .requiredOption("exchange", ["--exchange"], clk.STRING, { - help: "Base URL of the exchange to use", + .action(async (args) => { + console.log(JSON.stringify(sampleWalletCoreTransactions, undefined, 2)); + }); + +advancedCli + .subcommand("serve", "serve", { + help: "Serve the wallet API via a unix domain socket.", }) - .requiredOption("amount", ["--amount"], clk.STRING, { - help: "Amount to withdraw (before fees).", + .requiredOption("unixPath", ["--unix-path"], clk.STRING, { + default: defaultWalletCoreSocket, }) - .requiredOption("bank", ["--bank"], clk.STRING, { - help: "Base URL of the Taler fakebank service.", + .flag("noInit", ["--no-init"], { + help: "Do not initialize the wallet. The client must send the initWallet message.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { - await wallet.client.call(WalletApiOperation.WithdrawFakebank, { - amount: args.withdrawFakebank.amount, - bank: args.withdrawFakebank.bank, - exchange: args.withdrawFakebank.exchange, + const socketPath = args.serve.unixPath; + logger.info(`serving at ${socketPath}`); + let cleanupCalled = false; + + const cleanupSocket = (signal: string, code: number) => { + if (cleanupCalled) { + return; + } + cleanupCalled = true; + try { + logger.info("cleaning up socket"); + fs.unlinkSync(socketPath); + } catch (e) { + logger.warn(`unable to clean up socket: ${e}`); + } + process.exit(128 + code); + }; + process.on("SIGTERM", cleanupSocket); + process.on("SIGINT", cleanupSocket); + + const onNotif = (notif: WalletNotification) => { + writeObservabilityLog(notif); + }; + const wh = await createLocalWallet( + args, + { lazyTaskLoop: false, noInit: args.serve.noInit }, + onNotif, + ); + const w = wh.wallet; + let nextClientId = 1; + const notifyHandlers = new Map<number, (n: WalletNotification) => void>(); + w.addNotificationListener((n) => { + notifyHandlers.forEach((v, k) => { + v(n); }); }); + await runRpcServer({ + socketFilename: args.serve.unixPath, + onConnect(client) { + logger.info("connected"); + const clientId = nextClientId++; + notifyHandlers.set(clientId, (n: WalletNotification) => { + client.sendResponse({ + type: "notification", + payload: n as unknown as JsonMessage, + }); + }); + return { + onDisconnect() { + notifyHandlers.delete(clientId); + logger.info("disconnected"); + }, + onMessage(msg) { + logger.info(`message: ${j2s(msg)}`); + const op = (msg as any).operation; + const id = (msg as any).id; + const payload = (msg as any).args; + w.handleCoreApiRequest(op, id, payload) + .then((resp) => { + logger.info("sending response"); + client.sendResponse(resp as unknown as JsonMessage); + }) + .catch((e) => { + logger.error(`unexpected error: ${e}`); + }); + }, + }; + }, + }); }); advancedCli - .subcommand("manualWithdrawalDetails", "manual-withdrawal-details", { - help: "Query withdrawal fees.", + .subcommand("init", "init", { + help: "Initialize the wallet (with DB) and exit.", }) - .requiredArgument("exchange", clk.STRING) - .requiredArgument("amount", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { - const details = await wallet.client.call( - WalletApiOperation.GetWithdrawalDetailsForAmount, - { - amount: args.manualWithdrawalDetails.amount, - exchangeBaseUrl: args.manualWithdrawalDetails.exchange, - }, + await withWallet(args, { lazyTaskLoop: true }, async () => {}); + }); + +advancedCli + .subcommand("runPendingOpt", "run-pending", { + help: "Run pending operations.", + }) + .action(async (args) => { + logger.error( + "Subcommand run-pending not supported anymore. Please use run-until-done or the client/server wallet.", + ); + }); + +advancedCli + .subcommand("pending", "pending", { help: "Show pending operations." }) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const pending = await wallet.client.call( + WalletApiOperation.GetPendingOperations, + {}, ); - console.log(JSON.stringify(details, undefined, 2)); + console.log(JSON.stringify(pending, undefined, 2)); }); }); advancedCli - .subcommand("decode", "decode", { - help: "Decode base32-crockford.", + .subcommand("benchInternal", "bench-internal", { + help: "Run the 'bench-internal' benchmark", }) - .action((args) => { - const enc = fs.readFileSync(0, "utf8"); - fs.writeFileSync(1, decodeCrock(enc.trim())); + .action(async (args) => { + const myHttpLib = createPlatformHttpLib(); + const res = await createNativeWalletHost2({ + // No persistent DB storage. + persistentStoragePath: undefined, + httpLib: myHttpLib, + }); + const wallet = res.wallet; + await wallet.client.call(WalletApiOperation.InitWallet, {}); + await wallet.client.call(WalletApiOperation.RunIntegrationTest, { + amountToSpend: "TESTKUDOS:1" as AmountString, + amountToWithdraw: "TESTKUDOS:3" as AmountString, + corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/", + exchangeBaseUrl: "http://localhost:8081/", + merchantBaseUrl: "http://localhost:8083/", + }); + await wallet.client.call(WalletApiOperation.TestingWaitTasksDone, {}); + await wallet.client.call(WalletApiOperation.Shutdown, {}); }); advancedCli - .subcommand("withdrawManually", "withdraw-manually", { - help: "Withdraw manually from an exchange.", + .subcommand("genSegwit", "gen-segwit") + .requiredArgument("paytoUri", clk.STRING) + .requiredArgument("reservePub", clk.STRING) + .action(async (args) => { + const p = parsePaytoUri(args.genSegwit.paytoUri); + console.log(p); + }); + +const currenciesCli = walletCli.subcommand("currencies", "currencies", { + help: "Manage currencies.", +}); + +currenciesCli + .subcommand("listGlobalAuditors", "list-global-auditors", { + help: "List global-currency auditors.", }) - .requiredOption("exchange", ["--exchange"], clk.STRING, { - help: "Base URL of the exchange.", + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const currencies = await wallet.client.call( + WalletApiOperation.ListGlobalCurrencyAuditors, + {}, + ); + console.log(JSON.stringify(currencies, undefined, 2)); + }); + }); + +currenciesCli + .subcommand("listGlobalExchanges", "list-global-exchanges", { + help: "List global-currency exchanges.", }) - .requiredOption("amount", ["--amount"], clk.STRING, { - help: "Amount to withdraw", + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const currencies = await wallet.client.call( + WalletApiOperation.ListGlobalCurrencyExchanges, + {}, + ); + console.log(JSON.stringify(currencies, undefined, 2)); + }); + }); + +currenciesCli + .subcommand("addGlobalExchange", "add-global-exchange", { + help: "Add a global-currency exchange.", }) + .requiredOption("currency", ["--currency"], clk.STRING) + .requiredOption("exchangeBaseUrl", ["--url"], clk.STRING) + .requiredOption("exchangePub", ["--pub"], clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { - const exchangeBaseUrl = args.withdrawManually.exchange; - const amount = args.withdrawManually.amount; - const d = await wallet.client.call( - WalletApiOperation.GetWithdrawalDetailsForAmount, + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const currencies = await wallet.client.call( + WalletApiOperation.AddGlobalCurrencyExchange, { - amount: args.withdrawManually.amount, - exchangeBaseUrl: exchangeBaseUrl, + currency: args.addGlobalExchange.currency, + exchangeBaseUrl: args.addGlobalExchange.exchangeBaseUrl, + exchangeMasterPub: args.addGlobalExchange.exchangePub, }, ); - const acct = d.paytoUris[0]; - if (!acct) { - console.log("exchange has no accounts"); - return; - } - const resp = await wallet.client.call( - WalletApiOperation.AcceptManualWithdrawal, + console.log(JSON.stringify(currencies, undefined, 2)); + }); + }); + +currenciesCli + .subcommand("removeGlobalExchange", "remove-global-exchange", { + help: "Remove a global-currency exchange.", + }) + .requiredOption("currency", ["--currency"], clk.STRING) + .requiredOption("exchangeBaseUrl", ["--url"], clk.STRING) + .requiredOption("exchangePub", ["--pub"], clk.STRING) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const currencies = await wallet.client.call( + WalletApiOperation.RemoveGlobalCurrencyExchange, { - amount, - exchangeBaseUrl, + currency: args.removeGlobalExchange.currency, + exchangeBaseUrl: args.removeGlobalExchange.exchangeBaseUrl, + exchangeMasterPub: args.removeGlobalExchange.exchangePub, }, ); - const reservePub = resp.reservePub; - const completePaytoUri = addPaytoQueryParams(acct, { - amount: args.withdrawManually.amount, - message: `Taler top-up ${reservePub}`, - }); - console.log("Created reserve", reservePub); - console.log("Payto URI", completePaytoUri); + console.log(JSON.stringify(currencies, undefined, 2)); }); }); -const currenciesCli = walletCli.subcommand("currencies", "currencies", { - help: "Manage currencies.", -}); +currenciesCli + .subcommand("addGlobalAuditor", "add-global-auditor", { + help: "Add a global-currency auditor.", + }) + .requiredOption("currency", ["--currency"], clk.STRING) + .requiredOption("auditorBaseUrl", ["--url"], clk.STRING) + .requiredOption("auditorPub", ["--pub"], clk.STRING) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + const currencies = await wallet.client.call( + WalletApiOperation.AddGlobalCurrencyAuditor, + { + currency: args.addGlobalAuditor.currency, + auditorBaseUrl: args.addGlobalAuditor.auditorBaseUrl, + auditorPub: args.addGlobalAuditor.auditorPub, + }, + ); + console.log(JSON.stringify(currencies, undefined, 2)); + }); + }); currenciesCli - .subcommand("show", "show", { help: "Show currencies." }) + .subcommand("removeGlobalAuditor", "remove-global-auditor", { + help: "Remove a global-currency auditor.", + }) + .requiredOption("currency", ["--currency"], clk.STRING) + .requiredOption("auditorBaseUrl", ["--url"], clk.STRING) + .requiredOption("auditorPub", ["--pub"], clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const currencies = await wallet.client.call( - WalletApiOperation.ListCurrencies, - {}, + WalletApiOperation.RemoveGlobalCurrencyAuditor, + { + currency: args.removeGlobalAuditor.currency, + auditorBaseUrl: args.removeGlobalAuditor.auditorBaseUrl, + auditorPub: args.removeGlobalAuditor.auditorPub, + }, ); console.log(JSON.stringify(currencies, undefined, 2)); }); }); advancedCli + .subcommand("clearDatabase", "clear-database", { + help: "Clear the database, irrevocable deleting all data in the wallet.", + }) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + await wallet.client.call(WalletApiOperation.ClearDb, {}); + }); + }); + +advancedCli + .subcommand("recycle", "recycle", { + help: "Export, clear and re-import the database via the backup mechanism.", + }) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + await wallet.client.call(WalletApiOperation.Recycle, {}); + }); + }); + +advancedCli .subcommand("payPrepare", "pay-prepare", { help: "Claim an order but don't pay yet.", }) .requiredArgument("url", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const res = await wallet.client.call( WalletApiOperation.PreparePayForUri, { @@ -809,13 +1567,26 @@ advancedCli }); advancedCli + .subcommand("queryRefund", "query-refund", { + help: "Query refunds for a payment transaction.", + }) + .requiredArgument("transactionId", clk.STRING) + .action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + await wallet.client.call(WalletApiOperation.StartRefundQuery, { + transactionId: args.queryRefund.transactionId as TransactionIdStr, + }); + }); + }); + +advancedCli .subcommand("payConfirm", "pay-confirm", { help: "Confirm payment proposed by a merchant.", }) .requiredArgument("proposalId", clk.STRING) .maybeOption("sessionIdOverride", ["--session-id"], clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.ConfirmPay, { proposalId: args.payConfirm.proposalId, sessionId: args.payConfirm.sessionIdOverride, @@ -829,9 +1600,13 @@ advancedCli }) .requiredArgument("coinPub", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { await wallet.client.call(WalletApiOperation.ForceRefresh, { - coinPubList: [args.refresh.coinPub], + refreshCoinSpecs: [ + { + coinPub: args.refresh.coinPub, + }, + ], }); }); }); @@ -841,7 +1616,7 @@ advancedCli help: "Dump coins in an easy-to-process format.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const coinDump = await wallet.client.call( WalletApiOperation.DumpCoins, {}, @@ -858,7 +1633,7 @@ advancedCli }) .requiredArgument("coinPubSpec", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { let coinPubList: string[]; try { coinPubList = coinPubListCodec.decode( @@ -866,7 +1641,7 @@ advancedCli ); } catch (e: any) { console.log("could not parse coin list:", e.message); - process.exit(1); + processExit(1); } for (const c of coinPubList) { await wallet.client.call(WalletApiOperation.SetCoinSuspended, { @@ -883,7 +1658,7 @@ advancedCli }) .requiredArgument("coinPubSpec", clk.STRING) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { let coinPubList: string[]; try { coinPubList = coinPubListCodec.decode( @@ -891,7 +1666,7 @@ advancedCli ); } catch (e: any) { console.log("could not parse coin list:", e.message); - process.exit(1); + processExit(1); } for (const c of coinPubList) { await wallet.client.call(WalletApiOperation.SetCoinSuspended, { @@ -907,216 +1682,170 @@ advancedCli help: "List coins.", }) .action(async (args) => { - await withWallet(args, async (wallet) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { const coins = await wallet.client.call(WalletApiOperation.DumpCoins, {}); for (const coin of coins.coins) { console.log(`coin ${coin.coin_pub}`); console.log(` exchange ${coin.exchange_base_url}`); console.log(` denomPubHash ${coin.denom_pub_hash}`); - console.log( - ` remaining amount ${Amounts.stringify(coin.remaining_value)}`, - ); + console.log(` status ${coin.coin_status}`); } }); }); -const deploymentCli = walletCli.subcommand("deploymentArgs", "deployment", { - help: "Subcommands for handling GNU Taler deployments.", -}); - -deploymentCli - .subcommand("lintExchange", "lint-exchange", { - help: "Run checks on the exchange deployment.", - }) - .flag("cont", ["--continue"], { - help: "Continue after errors if possible", - }) - .flag("debug", ["--debug"], { - help: "Output extra debug info", - }) - .action(async (args) => { - await lintExchangeDeployment( - args.lintExchange.debug, - args.lintExchange.cont, - ); - }); - -deploymentCli - .subcommand("coincfg", "gen-coin-config", { - help: "Generate a coin/denomination configuration for the exchange.", - }) - .requiredOption("minAmount", ["--min-amount"], clk.STRING, { - help: "Smallest denomination", - }) - .requiredOption("maxAmount", ["--max-amount"], clk.STRING, { - help: "Largest denomination", - }) - .action(async (args) => { - let out = ""; - - const stamp = Math.floor(new Date().getTime() / 1000); - - const min = Amounts.parseOrThrow(args.coincfg.minAmount); - const max = Amounts.parseOrThrow(args.coincfg.maxAmount); - if (min.currency != max.currency) { - console.error("currency mismatch"); - process.exit(1); - } - const currency = min.currency; - let x = min; - let n = 1; - - out += "# Coin configuration for the exchange.\n"; - out += '# Should be placed in "/etc/taler/conf.d/exchange-coins.conf".\n'; - out += "\n"; - - while (Amounts.cmp(x, max) < 0) { - out += `[COIN-${currency}-n${n}-t${stamp}]\n`; - out += `VALUE = ${Amounts.stringify(x)}\n`; - out += `DURATION_WITHDRAW = 7 days\n`; - out += `DURATION_SPEND = 2 years\n`; - out += `DURATION_LEGAL = 6 years\n`; - out += `FEE_WITHDRAW = ${currency}:0\n`; - out += `FEE_DEPOSIT = ${Amounts.stringify(min)}\n`; - out += `FEE_REFRESH = ${currency}:0\n`; - out += `FEE_REFUND = ${currency}:0\n`; - out += `RSA_KEYSIZE = 2048\n`; - out += "\n"; - x = Amounts.add(x, x).amount; - n++; - } - - console.log(out); - }); - -const deploymentConfigCli = deploymentCli.subcommand("configArgs", "config", { - help: "Subcommands the Taler configuration.", -}); - -deploymentConfigCli - .subcommand("show", "show") - .flag("diagnostics", ["-d", "--diagnostics"]) - .maybeArgument("cfgfile", clk.STRING, {}) - .action(async (args) => { - const cfg = Configuration.load(args.show.cfgfile); - console.log( - cfg.stringify({ - diagnostics: args.show.diagnostics, - }), - ); - }); - const testCli = walletCli.subcommand("testingArgs", "testing", { help: "Subcommands for testing.", }); testCli - .subcommand("listIntegrationtests", "list-integrationtests") + .subcommand("withdrawTestkudos", "withdraw-testkudos") .action(async (args) => { - for (const t of getTestInfo()) { - let s = t.name; - if (t.suites.length > 0) { - s += ` (suites: ${t.suites.join(",")})`; - } - if (t.excludeByDefault) { - s += ` [excluded by default]`; - } - console.log(s); - } + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + await wallet.client.call(WalletApiOperation.WithdrawTestkudos, {}); + }); }); -testCli - .subcommand("runIntegrationtests", "run-integrationtests") - .maybeArgument("pattern", clk.STRING, { - help: "Glob pattern to select which tests to run", - }) - .maybeOption("suites", ["--suites"], clk.STRING, { - help: "Only run selected suites (comma-separated list)", - }) - .flag("dryRun", ["--dry"], { - help: "Only print tests that will be selected to run.", - }) - .flag("quiet", ["--quiet"], { - help: "Produce less output.", - }) - .action(async (args) => { - await runTests({ - includePattern: args.runIntegrationtests.pattern, - suiteSpec: args.runIntegrationtests.suites, - dryRun: args.runIntegrationtests.dryRun, - verbosity: args.runIntegrationtests.quiet ? 0 : 1, +testCli.subcommand("withdrawKudos", "withdraw-kudos").action(async (args) => { + await withWallet(args, { lazyTaskLoop: true }, async (wallet) => { + await wallet.client.call(WalletApiOperation.WithdrawTestBalance, { + amount: "KUDOS:50" as AmountString, + corebankApiBaseUrl: "https://bank.demo.taler.net/", + exchangeBaseUrl: "https://exchange.demo.taler.net/", }); }); +}); -async function read(stream: NodeJS.ReadStream) { - const chunks = []; - for await (const chunk of stream) chunks.push(chunk); - return Buffer.concat(chunks).toString("utf8"); -} +class PerfTimer { + tStarted: bigint | undefined; + tSum = BigInt(0); + tSumSq = BigInt(0); -testCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => { - const data = await read(process.stdin); + start() { + this.tStarted = process.hrtime.bigint(); + } - const lines = data.match(/[^\r\n]+/g); + stop() { + const now = process.hrtime.bigint(); + const s = this.tStarted; + if (s == null) { + throw Error(); + } + this.tSum = this.tSum + (now - s); + this.tSumSq = this.tSumSq + (now - s) * (now - s); + } - if (!lines) { - throw Error("can't split lines"); + mean(nRuns: number): number { + return Number(this.tSum / BigInt(nRuns)); } - const vals: Record<string, string> = {}; + stdev(nRuns: number) { + const m = this.tSum / BigInt(nRuns); + const x = this.tSumSq / BigInt(nRuns) - m * m; + return Math.floor(Math.sqrt(Number(x))); + } +} - let inBlindSigningSection = false; +testCli + .subcommand("benchmarkAgeRestrictions", "benchmark-age-restrictions") + .requiredOption("reps", ["--reps"], clk.INT, { + default: 100, + help: "repetitions (default: 100)", + }) + .action(async (args) => { + const numReps = args.benchmarkAgeRestrictions.reps ?? 100; + let tCommit = new PerfTimer(); + let tAttest = new PerfTimer(); + let tVerify = new PerfTimer(); + let tDerive = new PerfTimer(); + let tCompare = new PerfTimer(); + + console.log("starting benchmark"); + + for (let i = 0; i < numReps; i++) { + console.log(`doing iteration ${i}`); + tCommit.start(); + const commitProof = await AgeRestriction.restrictionCommit( + 0b1000001010101010101001, + 21, + ); + tCommit.stop(); - for (const line of lines) { - if (line === "blind signing:") { - inBlindSigningSection = true; - continue; - } - if (line[0] !== " ") { - inBlindSigningSection = false; - continue; - } - if (inBlindSigningSection) { - const m = line.match(/ (\w+) (\w+)/); - if (!m) { - console.log("bad format"); - process.exit(2); + tAttest.start(); + const attest = AgeRestriction.commitmentAttest(commitProof, 18); + tAttest.stop(); + + tVerify.start(); + const attestRes = AgeRestriction.commitmentVerify( + commitProof.commitment, + encodeCrock(attest), + 18, + ); + tVerify.stop(); + if (!attestRes) { + throw Error(); } - vals[m[1]] = m[2]; - } - } - console.log(vals); + const salt = getRandomBytes(32); + tDerive.start(); + const deriv = await AgeRestriction.commitmentDerive(commitProof, salt); + tDerive.stop(); - const req = (k: string) => { - if (!vals[k]) { - throw Error(`no value for ${k}`); + tCompare.start(); + const res2 = await AgeRestriction.commitCompare( + deriv.commitment, + commitProof.commitment, + salt, + ); + tCompare.stop(); + if (!res2) { + throw Error(); + } } - return decodeCrock(vals[k]); - }; - const myBm = rsaBlind( - req("message_hash"), - req("blinding_key_secret"), - req("rsa_public_key"), - ); - - deepStrictEqual(req("blinded_message"), myBm); + console.log( + `edx25519-commit (ns): ${tCommit.mean(numReps)} (stdev ${tCommit.stdev( + numReps, + )})`, + ); + console.log( + `edx25519-attest (ns): ${tAttest.mean(numReps)} (stdev ${tAttest.stdev( + numReps, + )})`, + ); + console.log( + `edx25519-verify (ns): ${tVerify.mean(numReps)} (stdev ${tVerify.stdev( + numReps, + )})`, + ); + console.log( + `edx25519-derive (ns): ${tDerive.mean(numReps)} (stdev ${tDerive.stdev( + numReps, + )})`, + ); + console.log( + `edx25519-compare (ns): ${tCompare.mean(numReps)} (stdev ${tCompare.stdev( + numReps, + )})`, + ); + }); - console.log("check passed!"); +testCli.subcommand("logtest", "logtest").action(async (args) => { + logger.trace("This is a trace message."); + logger.info("This is an info message."); + logger.warn("This is an warning message."); + logger.error("This is an error message."); }); -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); -}); +async function read(stream: NodeJS.ReadStream) { + const chunks = []; + for await (const chunk of stream) chunks.push(chunk); + return Buffer.concat(chunks).toString("utf8"); +} export function main() { - if (process.env["TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE"]) { - logger.warn("Allowing withdrawal of late denominations for debugging"); - walletCoreDebugFlags.denomselAllowLate = true; + const maybeFilename = getenv("TALER_WALLET_DEBUG_OBSERVE"); + if (!!maybeFilename) { + observabilityEventFile = maybeFilename; } walletCli.run(); } diff --git a/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts b/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts deleted file mode 100644 index ea05de8e9..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runPromptPaymentScenario(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - console.log(orderStatus); - - // Wait "forever" - await new Promise(() => {}); -} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts deleted file mode 100644 index 0f8af05e5..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - GlobalTestState, - WalletCli, - ExchangeService, - setupDb, - BankService, - MerchantService, - BankApi, - BankAccessApi, - CreditDebitIndicator, -} from "../harness/harness.js"; -import { createEddsaKeyPair, encodeCrock } from "@gnu-taler/taler-util"; -import { defaultCoinConfig } from "../harness/denomStructures"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runBankApiTest(t: GlobalTestState) { - // Set up test environment - - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - await merchant.addDefaultInstance(); - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - const bankUser = await BankApi.registerAccount(bank, "user1", "pw1"); - - // Make sure that registering twice results in a 409 Conflict - { - const e = await t.assertThrowsAsync(async () => { - await BankApi.registerAccount(bank, "user1", "pw1"); - }); - t.assertAxiosError(e); - t.assertTrue(e.response?.status === 409); - } - - let balResp = await BankAccessApi.getAccountBalance(bank, bankUser); - - console.log(balResp); - - // Check that we got the sign-up bonus. - t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:100"); - t.assertTrue( - balResp.balance.credit_debit_indicator === CreditDebitIndicator.Credit, - ); - - const res = createEddsaKeyPair(); - - await BankApi.adminAddIncoming(bank, { - amount: "TESTKUDOS:115", - debitAccountPayto: bankUser.accountPaytoUri, - exchangeBankAccount: exchangeBankAccount, - reservePub: encodeCrock(res.eddsaPub), - }); - - balResp = await BankAccessApi.getAccountBalance(bank, bankUser); - t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:15"); - t.assertTrue( - balResp.balance.credit_debit_indicator === CreditDebitIndicator.Debit, - ); -} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts b/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts deleted file mode 100644 index a509e3b19..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; -import { URL } from "url"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for the merchant's order lifecycle. - * - * FIXME: Is this test still necessary? We initially wrote if to confirm/document - * assumptions about how the merchant should work. - */ -export async function runClaimLoopTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - // Query private order status before claiming it. - let orderStatusBefore = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - ); - t.assertTrue(orderStatusBefore.order_status === "unpaid"); - let statusUrlBefore = new URL(orderStatusBefore.order_status_url); - - // Make wallet claim the unpaid order. - t.assertTrue(orderStatusBefore.order_status === "unpaid"); - const talerPayUri = orderStatusBefore.taler_pay_uri; - await wallet.client.call(WalletApiOperation.PreparePayForUri, { - talerPayUri, - }); - - // Query private order status after claiming it. - let orderStatusAfter = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - ); - t.assertTrue(orderStatusAfter.order_status === "claimed"); - - await t.shutdown(); -} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts deleted file mode 100644 index 28cca0758..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - PreparePayResultType, - TalerErrorCode, - TalerErrorDetails, - TransactionType, -} from "@gnu-taler/taler-util"; -import { - WalletApiOperation, -} from "@gnu-taler/taler-wallet-core"; -import { makeEventId } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; - -export async function runDenomUnofferedTest(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" }); - - // Make the exchange forget the denomination. - // Effectively we completely reset the exchange, - // but keep the exchange master public key. - - await exchange.stop(); - await exchange.purgeDatabase(); - await exchange.purgeSecmodKeys(); - await exchange.start(); - await exchange.pingUntilAvailable(); - - await merchant.stop(); - await merchant.start(); - await merchant.pingUntilAvailable(); - - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }; - - { - const orderResp = await MerchantPrivateApi.createOrder( - merchant, - "default", - { - order: order, - }, - ); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - const exc = await t.assertThrowsAsync(async () => { - await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - }); - - const errorDetails: TalerErrorDetails = exc.operationError; - // FIXME: We might want a more specific error code here! - t.assertDeepEqual( - errorDetails.code, - TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, - ); - const merchantErrorCode = (errorDetails.details as any).errorResponse.code; - t.assertDeepEqual( - merchantErrorCode, - TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND, - ); - } - - await wallet.client.call(WalletApiOperation.AddExchange, { - exchangeBaseUrl: exchange.baseUrl, - forceUpdate: true, - }); - - // Now withdrawal should work again. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - await wallet.runUntilDone(); - - const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {}); - console.log(JSON.stringify(txs, undefined, 2)); -} - -runDenomUnofferedTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts b/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts deleted file mode 100644 index f33c8338b..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; - -/** - * Run test for basic, bank-integrated withdrawal and payment. - */ -export async function runDepositTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - await wallet.runUntilDone(); - - const { depositGroupId } = await wallet.client.call( - WalletApiOperation.CreateDepositGroup, - { - amount: "TESTKUDOS:10", - depositPaytoUri: "payto://x-taler-bank/localhost/foo", - }, - ); - - await wallet.runUntilDone(); - - const transactions = await wallet.client.call( - WalletApiOperation.GetTransactions, - {}, - ); - console.log("transactions", JSON.stringify(transactions, undefined, 2)); - t.assertDeepEqual(transactions.transactions[0].type, "withdrawal"); - t.assertTrue(!transactions.transactions[0].pending); - t.assertDeepEqual(transactions.transactions[1].type, "deposit"); - t.assertTrue(!transactions.transactions[1].pending); - // The raw amount is what ends up on the bank account, which includes - // deposit and wire fees. - t.assertDeepEqual(transactions.transactions[1].amountRaw, "TESTKUDOS:9.79"); - - const trackResult = wallet.client.call(WalletApiOperation.TrackDepositGroup, { - depositGroupId, - }); - - console.log(JSON.stringify(trackResult, undefined, 2)); -} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts deleted file mode 100644 index 8a5d563ce..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts +++ /dev/null @@ -1,272 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - GlobalTestState, - WalletCli, - setupDb, - BankService, - ExchangeService, - MerchantService, - BankApi, - BankAccessApi, -} from "../harness/harness.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - ExchangesListRespose, - URL, - TalerErrorCode, -} from "@gnu-taler/taler-util"; -import { - FaultInjectedExchangeService, - FaultInjectionResponseContext, -} from "../harness/faultInjection"; -import { defaultCoinConfig } from "../harness/denomStructures"; - -/** - * Test if the wallet handles outdated exchange versions correct.y - */ -export async function runExchangeManagementTest(t: GlobalTestState) { - // Set up test environment - - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); - - bank.setSuggestedExchange( - faultyExchange, - exchangeBankAccount.accountPaytoUri, - ); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - /* - * ========================================================================= - * Check that the exchange can be added to the wallet - * (without any faults active). - * ========================================================================= - */ - - const wallet = new WalletCli(t); - - let exchangesList: ExchangesListRespose; - - exchangesList = await wallet.client.call( - WalletApiOperation.ListExchanges, - {}, - ); - t.assertTrue(exchangesList.exchanges.length === 0); - - // Try before fault is injected - await wallet.client.call(WalletApiOperation.AddExchange, { - exchangeBaseUrl: faultyExchange.baseUrl, - }); - - exchangesList = await wallet.client.call( - WalletApiOperation.ListExchanges, - {}, - ); - t.assertTrue(exchangesList.exchanges.length === 1); - - await wallet.client.call(WalletApiOperation.ListExchanges, { - exchangeBaseUrl: faultyExchange.baseUrl, - }); - - console.log("listing exchanges"); - - exchangesList = await wallet.client.call( - WalletApiOperation.ListExchanges, - {}, - ); - t.assertTrue(exchangesList.exchanges.length === 1); - - console.log("got list", exchangesList); - - /* - * ========================================================================= - * Check what happens if the exchange returns something totally - * bogus for /keys. - * ========================================================================= - */ - - wallet.deleteDatabase(); - - exchangesList = await wallet.client.call( - WalletApiOperation.ListExchanges, - {}, - ); - t.assertTrue(exchangesList.exchanges.length === 0); - - faultyExchange.faultProxy.addFault({ - async modifyResponse(ctx: FaultInjectionResponseContext) { - const url = new URL(ctx.request.requestUrl); - if (url.pathname === "/keys") { - const body = { - version: "whaaat", - }; - ctx.responseBody = Buffer.from(JSON.stringify(body), "utf-8"); - } - }, - }); - - const err1 = await t.assertThrowsOperationErrorAsync(async () => { - await wallet.client.call(WalletApiOperation.AddExchange, { - exchangeBaseUrl: faultyExchange.baseUrl, - }); - }); - - // Response is malformed, since it didn't even contain a version code - // in a format the wallet can understand. - t.assertTrue( - err1.operationError.code === - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - ); - - exchangesList = await wallet.client.call( - WalletApiOperation.ListExchanges, - {}, - ); - t.assertTrue(exchangesList.exchanges.length === 0); - - /* - * ========================================================================= - * Check what happens if the exchange returns an old, unsupported - * version for /keys - * ========================================================================= - */ - - wallet.deleteDatabase(); - faultyExchange.faultProxy.clearAllFaults(); - - faultyExchange.faultProxy.addFault({ - async modifyResponse(ctx: FaultInjectionResponseContext) { - const url = new URL(ctx.request.requestUrl); - if (url.pathname === "/keys") { - const keys = ctx.responseBody?.toString("utf-8"); - t.assertTrue(keys != null); - const keysJson = JSON.parse(keys); - keysJson["version"] = "2:0:0"; - ctx.responseBody = Buffer.from(JSON.stringify(keysJson), "utf-8"); - } - }, - }); - - const err2 = await t.assertThrowsOperationErrorAsync(async () => { - await wallet.client.call(WalletApiOperation.AddExchange, { - exchangeBaseUrl: faultyExchange.baseUrl, - }); - }); - - t.assertTrue( - err2.operationError.code === - TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, - ); - - exchangesList = await wallet.client.call( - WalletApiOperation.ListExchanges, - {}, - ); - t.assertTrue(exchangesList.exchanges.length === 0); - - /* - * ========================================================================= - * Check that the exchange version is also checked when - * the exchange is implicitly added via the suggested - * exchange of a bank-integrated withdrawal. - * ========================================================================= - */ - - // Fault from above is still active! - - // Create withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, - "TESTKUDOS:10", - ); - - // Hand it to the wallet - - const wd = await wallet.client.call( - WalletApiOperation.GetWithdrawalDetailsForUri, - { - talerWithdrawUri: wop.taler_withdraw_uri, - }, - ); - - // Make sure the faulty exchange isn't used for the suggestion. - t.assertTrue(wd.possibleExchanges.length === 0); -} - -runExchangeManagementTest.suites = ["exchange"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts deleted file mode 100644 index 56684f70a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts +++ /dev/null @@ -1,221 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - codecForExchangeKeysJson, - ConfirmPayResultType, - Duration, - durationFromSpec, - PreparePayResultType, - stringifyTimestamp, -} from "@gnu-taler/taler-util"; -import { - NodeHttpLib, - PendingOperationsResponse, - readSuccessResponseJsonOrThrow, - WalletApiOperation, -} from "@gnu-taler/taler-wallet-core"; -import { makeNoFeeCoinConfig } from "../harness/denomStructures"; -import { - BankService, - ExchangeService, - GlobalTestState, - MerchantPrivateApi, - MerchantService, - setupDb, - WalletCli, -} from "../harness/harness.js"; -import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js"; - -async function applyTimeTravel( - timetravelDuration: Duration, - s: { - exchange?: ExchangeService; - merchant?: MerchantService; - wallet?: WalletCli; - }, -): Promise<void> { - if (s.exchange) { - await s.exchange.stop(); - s.exchange.setTimetravel(timetravelDuration); - await s.exchange.start(); - await s.exchange.pingUntilAvailable(); - } - - if (s.merchant) { - await s.merchant.stop(); - s.merchant.setTimetravel(timetravelDuration); - await s.merchant.start(); - await s.merchant.pingUntilAvailable(); - } - - if (s.wallet) { - console.log("setting wallet time travel to", timetravelDuration); - s.wallet.setTimetravel(timetravelDuration); - } -} - -const http = new NodeHttpLib(); - -/** - * Basic time travel test. - */ -export async function runExchangeTimetravelTest(t: GlobalTestState) { - // Set up test environment - - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addCoinConfigList(makeNoFeeCoinConfig("TESTKUDOS")); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - - const keysResp1 = await http.get(exchange.baseUrl + "keys"); - const keys1 = await readSuccessResponseJsonOrThrow( - keysResp1, - codecForExchangeKeysJson(), - ); - console.log( - "keys 1 (before time travel):", - JSON.stringify(keys1, undefined, 2), - ); - - // Travel into the future, the deposit expiration is two years - // into the future. - console.log("applying first time travel"); - await applyTimeTravel(durationFromSpec({ days: 400 }), { - wallet, - exchange, - merchant, - }); - - const keysResp2 = await http.get(exchange.baseUrl + "keys"); - const keys2 = await readSuccessResponseJsonOrThrow( - keysResp2, - codecForExchangeKeysJson(), - ); - console.log( - "keys 2 (after time travel):", - JSON.stringify(keys2, undefined, 2), - ); - - const denomPubs1 = keys1.denoms.map((x) => { - return { - denomPub: x.denom_pub, - expireDeposit: stringifyTimestamp(x.stamp_expire_deposit), - }; - }); - - const denomPubs2 = keys2.denoms.map((x) => { - return { - denomPub: x.denom_pub, - expireDeposit: stringifyTimestamp(x.stamp_expire_deposit), - }; - }); - const dps2 = new Set(denomPubs2.map((x) => x.denomPub)); - - console.log("=== KEYS RESPONSE 1 ==="); - - console.log("list issue date", stringifyTimestamp(keys1.list_issue_date)); - console.log("num denoms", keys1.denoms.length) - console.log("denoms", JSON.stringify(denomPubs1, undefined, 2)); - - console.log("=== KEYS RESPONSE 2 ==="); - - console.log("list issue date", stringifyTimestamp(keys2.list_issue_date)); - console.log("num denoms", keys2.denoms.length) - console.log("denoms", JSON.stringify(denomPubs2, undefined, 2)); - - for (const da of denomPubs1) { - if (!dps2.has(da.denomPub)) { - console.log("=== ERROR ==="); - console.log(`denomination with public key ${da.denomPub} is not present in new /keys response`); - console.log( - `the new /keys response was issued ${stringifyTimestamp( - keys2.list_issue_date, - )}`, - ); - console.log( - `however, the missing denomination has stamp_expire_deposit ${da.expireDeposit}`, - ); - console.log("see above for the verbatim /keys responses"); - t.assertTrue(false); - } - } -} - -runExchangeTimetravelTest.suites = ["exchange"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts b/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts deleted file mode 100644 index 025e12226..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - GlobalTestState, - BankService, - ExchangeService, - MerchantService, - setupDb, - WalletCli, -} from "../harness/harness.js"; -import { - withdrawViaBank, - makeTestPayment, - SimpleTestEnvironment, -} from "../harness/helpers.js"; - -/** - * Run a test case with a simple TESTKUDOS Taler environment, consisting - * of one exchange, one bank and one merchant. - */ -export async function createMyTestkudosEnvironment( - t: GlobalTestState, -): Promise<SimpleTestEnvironment> { - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - const coinCommon = { - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, - feeDeposit: "TESTKUDOS:0.0025", - feeWithdraw: "TESTKUDOS:0", - feeRefresh: "TESTKUDOS:0", - feeRefund: "TESTKUDOS:0", - }; - - exchange.addCoinConfigList([ - { - ...coinCommon, - name: "c1", - value: "TESTKUDOS:1.28", - }, - { - ...coinCommon, - name: "c2", - value: "TESTKUDOS:0.64", - }, - { - ...coinCommon, - name: "c3", - value: "TESTKUDOS:0.32", - }, - { - ...coinCommon, - name: "c4", - value: "TESTKUDOS:0.16", - }, - { - ...coinCommon, - name: "c5", - value: "TESTKUDOS:0.08", - }, - { - ...coinCommon, - name: "c5", - value: "TESTKUDOS:0.04", - }, - { - ...coinCommon, - name: "c6", - value: "TESTKUDOS:0.02", - }, - { - ...coinCommon, - name: "c7", - value: "TESTKUDOS:0.01", - }, - ]); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addDefaultInstance(); - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - }; -} - -/** - * Run test for basic, bank-integrated withdrawal and payment. - */ -export async function runFeeRegressionTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createMyTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { - wallet, - bank, - exchange, - amount: "TESTKUDOS:1.92", - }); - - const coins = await wallet.client.call(WalletApiOperation.DumpCoins, {}); - - // Make sure we really withdraw one 0.64 and one 1.28 coin. - t.assertTrue(coins.coins.length === 2); - - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:1.30", - fulfillment_url: "taler://fulfillment-success/thx", - }; - - await makeTestPayment(t, { wallet, merchant, order }); - - await wallet.runUntilDone(); - - const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {}); - t.assertAmountEquals(txs.transactions[1].amountEffective, "TESTKUDOS:1.30"); - console.log(txs); -} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts deleted file mode 100644 index 839ad5fa7..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - NexusUserBundle, - LibeufinNexusApi, - LibeufinNexusService, - LibeufinSandboxService, - LibeufinSandboxApi, - findNexusPayment, -} from "../harness/libeufin"; - -/** - * Run basic test with LibEuFin. - */ -export async function runLibeufinApiBankaccountTest(t: GlobalTestState) { - const nexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - await nexus.start(); - await nexus.pingUntilAvailable(); - - await LibeufinNexusApi.createUser(nexus, { - username: "one", - password: "testing-the-bankaccount-api", - }); - const sandbox = await LibeufinSandboxService.create(t, { - httpPort: 5012, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - }); - await sandbox.start(); - await sandbox.pingUntilAvailable(); - await LibeufinSandboxApi.createEbicsHost(sandbox, "mock"); - await LibeufinSandboxApi.createEbicsSubscriber(sandbox, { - hostID: "mock", - userID: "mock", - partnerID: "mock", - }); - await LibeufinSandboxApi.createEbicsBankAccount(sandbox, { - subscriber: { - hostID: "mock", - partnerID: "mock", - userID: "mock", - }, - iban: "DE71500105179674997361", - bic: "BELADEBEXXX", - name: "mock", - currency: "EUR", - label: "mock", - }); - await LibeufinNexusApi.createEbicsBankConnection(nexus, { - name: "bankaccount-api-test-connection", - ebicsURL: "http://localhost:5012/ebicsweb", - hostID: "mock", - userID: "mock", - partnerID: "mock", - }); - await LibeufinNexusApi.connectBankConnection( - nexus, - "bankaccount-api-test-connection", - ); - await LibeufinNexusApi.fetchAccounts( - nexus, - "bankaccount-api-test-connection", - ); - - await LibeufinNexusApi.importConnectionAccount( - nexus, - "bankaccount-api-test-connection", - "mock", - "local-mock", - ); - await LibeufinSandboxApi.simulateIncomingTransaction( - sandbox, - "mock", // creditor bankaccount label - { - debtorIban: "DE84500105176881385584", - debtorBic: "BELADEBEXXX", - debtorName: "mock2", - amount: "1", - subject: "mock subject", - } - ); - await LibeufinNexusApi.fetchTransactions(nexus, "local-mock"); - let transactions = await LibeufinNexusApi.getAccountTransactions( - nexus, - "local-mock", - ); - let el = findNexusPayment("mock subject", transactions.data); - t.assertTrue(el instanceof Object); -} -runLibeufinApiBankaccountTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts deleted file mode 100644 index f1d507c03..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - NexusUserBundle, - LibeufinNexusApi, - LibeufinNexusService, - LibeufinSandboxService, - LibeufinSandboxApi, - findNexusPayment, -} from "../harness/libeufin"; - -/** - * Run basic test with LibEuFin. - */ -export async function runLibeufinApiBankconnectionTest(t: GlobalTestState) { - const nexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - await nexus.start(); - await nexus.pingUntilAvailable(); - - await LibeufinNexusApi.createUser(nexus, { - username: "one", - password: "testing-the-bankconnection-api", - }); - - await LibeufinNexusApi.createEbicsBankConnection(nexus, { - name: "bankconnection-api-test-connection", - ebicsURL: "http://localhost:5012/ebicsweb", - hostID: "mock", - userID: "mock", - partnerID: "mock", - }); - - let connections = await LibeufinNexusApi.getAllConnections(nexus); - t.assertTrue(connections.data["bankConnections"].length == 1); - - await LibeufinNexusApi.deleteBankConnection(nexus, { - bankConnectionId: "bankconnection-api-test-connection", - }); - connections = await LibeufinNexusApi.getAllConnections(nexus); - t.assertTrue(connections.data["bankConnections"].length == 0); -} -runLibeufinApiBankconnectionTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts deleted file mode 100644 index b106cf304..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import axios from "axios"; -import { URL } from "@gnu-taler/taler-util"; -import { GlobalTestState } from "../harness/harness.js"; -import { - SandboxUserBundle, - NexusUserBundle, - launchLibeufinServices, - LibeufinNexusApi, -} from "../harness/libeufin"; - -export async function runLibeufinApiFacadeBadRequestTest(t: GlobalTestState) { - - /** - * User saltetd "01" - */ - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - const user01sandbox = new SandboxUserBundle("01"); - - /** - * Launch Sandbox and Nexus. - */ - const libeufinServices = await launchLibeufinServices( - t, - [user01nexus], - [user01sandbox], - ["twg"], - ); - console.log("malformed facade"); - const baseUrl = libeufinServices.libeufinNexus.baseUrl; - let url = new URL("facades", baseUrl); - let resp = await axios.post( - url.href, - { - name: "malformed-facade", - type: "taler-wire-gateway", - config: {}, // malformation here. - }, - { - auth: { - username: "admin", - password: "test", - }, - validateStatus: () => true, - }, - ); - t.assertTrue(resp.status == 400); -} - -runLibeufinApiFacadeBadRequestTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts deleted file mode 100644 index c49d49712..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - SandboxUserBundle, - NexusUserBundle, - launchLibeufinServices, - LibeufinNexusApi, -} from "../harness/libeufin"; - -/** - * Run basic test with LibEuFin. - */ -export async function runLibeufinApiFacadeTest(t: GlobalTestState) { - /** - * User saltetd "01" - */ - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - const user01sandbox = new SandboxUserBundle("01"); - - /** - * Launch Sandbox and Nexus. - */ - const libeufinServices = await launchLibeufinServices( - t, - [user01nexus], - [user01sandbox], - ["twg"], - ); - let resp = await LibeufinNexusApi.getAllFacades( - libeufinServices.libeufinNexus, - ); - // check that original facade shows up. - t.assertTrue(resp.data["facades"][0]["name"] == user01nexus.twgReq["name"]); - - const twgBaseUrl: string = resp.data["facades"][0]["baseUrl"]; - t.assertTrue(typeof twgBaseUrl === "string"); - t.assertTrue(twgBaseUrl.startsWith("http://")); - t.assertTrue(twgBaseUrl.endsWith("/")); - - // delete it. - resp = await LibeufinNexusApi.deleteFacade( - libeufinServices.libeufinNexus, - user01nexus.twgReq["name"], - ); - // check that no facades show up. - t.assertTrue(!resp.data.hasOwnProperty("facades")); -} - -runLibeufinApiFacadeTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts deleted file mode 100644 index e64f459a0..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - NexusUserBundle, - LibeufinNexusApi, - LibeufinNexusService, -} from "../harness/libeufin"; - -/** - * Run basic test with LibEuFin. - */ -export async function runLibeufinApiPermissionsTest(t: GlobalTestState) { - const nexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - await nexus.start(); - await nexus.pingUntilAvailable(); - - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - - await LibeufinNexusApi.createUser(nexus, user01nexus.userReq); - await LibeufinNexusApi.postPermission( - nexus, - user01nexus.twgTransferPermission, - ); - let transferPermission = await LibeufinNexusApi.getAllPermissions(nexus); - let element = transferPermission.data["permissions"].pop(); - t.assertTrue( - element["permissionName"] == "facade.talerwiregateway.transfer" && - element["subjectId"] == "username-01", - ); - let denyTransfer = user01nexus.twgTransferPermission; - - // Now revoke permission. - denyTransfer["action"] = "revoke"; - await LibeufinNexusApi.postPermission(nexus, denyTransfer); - - transferPermission = await LibeufinNexusApi.getAllPermissions(nexus); - t.assertTrue(transferPermission.data["permissions"].length == 0); -} - -runLibeufinApiPermissionsTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts deleted file mode 100644 index f5df4cfa3..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - NexusUserBundle, - LibeufinNexusApi, - LibeufinNexusService, - LibeufinSandboxService, - LibeufinSandboxApi, - findNexusPayment, -} from "../harness/libeufin"; - -// This test only checks that LibEuFin doesn't fail when -// it generates Camt statements - no assertions take place. -// Furthermore, it prints the Camt.053 being generated. -export async function runLibeufinApiSandboxCamtTest(t: GlobalTestState) { - - const sandbox = await LibeufinSandboxService.create(t, { - httpPort: 5012, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - }); - await sandbox.start(); - await sandbox.pingUntilAvailable(); - await LibeufinSandboxApi.createBankAccount(sandbox, { - iban: "DE71500105179674997361", - bic: "BELADEBEXXX", - name: "Mock Name", - label: "mock-account-0", - currency: "EUR" - }); - await LibeufinSandboxApi.createBankAccount(sandbox, { - iban: "DE71500105179674997361", - bic: "BELADEBEXXX", - name: "Mock Name", - label: "mock-account-1", - currency: "EUR" - }); - await sandbox.makeTransaction("mock-account-0", "mock-account-1", "EUR:1", "+1"); - await sandbox.makeTransaction("mock-account-0", "mock-account-1", "EUR:1", "+1"); - await sandbox.makeTransaction("mock-account-0", "mock-account-1", "EUR:1", "+1"); - await sandbox.makeTransaction("mock-account-1", "mock-account-0", "EUR:5", "minus 5"); - await sandbox.c53tick(); - let ret = await LibeufinSandboxApi.getCamt053(sandbox, "mock-account-1"); - console.log(ret); -} -runLibeufinApiSandboxCamtTest.excludeByDefault = true; -runLibeufinApiSandboxCamtTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts deleted file mode 100644 index a90644926..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - NexusUserBundle, - LibeufinNexusApi, - LibeufinNexusService, - LibeufinSandboxService, - LibeufinSandboxApi, - findNexusPayment, -} from "../harness/libeufin"; - -export async function runLibeufinApiSandboxTransactionsTest(t: GlobalTestState) { - - const sandbox = await LibeufinSandboxService.create(t, { - httpPort: 5012, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - }); - await sandbox.start(); - await sandbox.pingUntilAvailable(); - await LibeufinSandboxApi.createBankAccount(sandbox, { - iban: "DE71500105179674997361", - bic: "BELADEBEXXX", - name: "Mock Name", - label: "mock-account", - currency: "EUR" - }); - await LibeufinSandboxApi.simulateIncomingTransaction( - sandbox, - "mock-account", - { - - debtorIban: "DE84500105176881385584", - debtorBic: "BELADEBEXXX", - debtorName: "mock2", - subject: "mock subject", - amount: "1" // EUR is default. - } - ) - await LibeufinSandboxApi.simulateIncomingTransaction( - sandbox, - "mock-account", - { - - debtorIban: "DE84500105176881385584", - debtorBic: "BELADEBEXXX", - debtorName: "mock2", - subject: "mock subject 2", - amount: "1.1" // EUR is default. - } - ) - let ret = await LibeufinSandboxApi.getAccountInfoWithBalance(sandbox, "mock-account"); - t.assertAmountEquals(ret.data.balance, "EUR:2.1"); -} -runLibeufinApiSandboxTransactionsTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts deleted file mode 100644 index 3863c5711..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, setupDb } from "../harness/harness.js"; -import { - SandboxUserBundle, - NexusUserBundle, - launchLibeufinServices, - LibeufinSandboxApi, - LibeufinNexusApi, - LibeufinNexusService, -} from "../harness/libeufin"; - -/** - * Test Nexus scheduling API. It creates a task, check whether it shows - * up, then deletes it, and check if it's gone. Ideally, a check over the - * _liveliness_ of a scheduled task should happen. - */ -export async function runLibeufinApiSchedulingTest(t: GlobalTestState) { - const nexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - await nexus.start(); - await nexus.pingUntilAvailable(); - - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - const user01sandbox = new SandboxUserBundle("01"); - await launchLibeufinServices(t, [user01nexus], [user01sandbox]); - await LibeufinNexusApi.postTask(nexus, user01nexus.localAccountName, { - name: "test-task", - cronspec: "* * *", - type: "fetch", - params: { - level: "all", - rangeType: "all", - }, - }); - let resp = await LibeufinNexusApi.getTasks( - nexus, - user01nexus.localAccountName, - "test-task", - ); - t.assertTrue(resp.data["taskName"] == "test-task"); - await LibeufinNexusApi.deleteTask( - nexus, - user01nexus.localAccountName, - "test-task", - ); - try { - await LibeufinNexusApi.getTasks( - nexus, - user01nexus.localAccountName, - "test-task", - ); - } catch (err: any) { - t.assertTrue(err.response.status == 404); - } - - // Same with submit task. - await LibeufinNexusApi.postTask(nexus, user01nexus.localAccountName, { - name: "test-task", - cronspec: "* * *", - type: "submit", - params: {}, - }); - resp = await LibeufinNexusApi.getTasks( - nexus, - user01nexus.localAccountName, - "test-task", - ); - t.assertTrue(resp.data["taskName"] == "test-task"); - await LibeufinNexusApi.deleteTask( - nexus, - user01nexus.localAccountName, - "test-task", - ); - try { - await LibeufinNexusApi.getTasks( - nexus, - user01nexus.localAccountName, - "test-task", - ); - } catch (err: any) { - t.assertTrue(err.response.status == 404); - } -} -runLibeufinApiSchedulingTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts deleted file mode 100644 index edf66690b..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { LibeufinNexusApi, LibeufinNexusService } from "../harness/libeufin"; - -/** - * Run basic test with LibEuFin. - */ -export async function runLibeufinApiUsersTest(t: GlobalTestState) { - const nexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - await nexus.start(); - await nexus.pingUntilAvailable(); - - await LibeufinNexusApi.createUser(nexus, { - username: "one", - password: "will-be-changed", - }); - - await LibeufinNexusApi.changePassword( - nexus, - "one", - { - newPassword: "got-changed", - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - - let resp = await LibeufinNexusApi.getUser(nexus, { - auth: { - username: "one", - password: "got-changed", - }, - }); - console.log(resp.data); - t.assertTrue(resp.data["username"] == "one" && !resp.data["superuser"]); -} - -runLibeufinApiUsersTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts deleted file mode 100644 index 786e61832..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, delayMs } from "../harness/harness.js"; -import { - NexusUserBundle, - LibeufinNexusApi, - LibeufinNexusService, - LibeufinSandboxService, -} from "../harness/libeufin"; - -/** - * Testing how Nexus reacts when the Sandbox is unreachable. - * Typically, because the user specified a wrong EBICS endpoint. - */ -export async function runLibeufinBadGatewayTest(t: GlobalTestState) { - /** - * User saltetd "01" - */ - const user01nexus = new NexusUserBundle( - "01", "http://localhost:5010/not-found", // the EBICS endpoint at Sandbox - ); - - // Start Nexus - const libeufinNexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - await libeufinNexus.start(); - await libeufinNexus.pingUntilAvailable(); - - // Start Sandbox - const libeufinSandbox = await LibeufinSandboxService.create(t, { - httpPort: 5010, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - }); - await libeufinSandbox.start(); - await libeufinSandbox.pingUntilAvailable(); - - // Connecting to a non-existent Sandbox endpoint. - await LibeufinNexusApi.createEbicsBankConnection( - libeufinNexus, - user01nexus.connReq - ); - - // 502 Bad Gateway expected. - try { - await LibeufinNexusApi.connectBankConnection( - libeufinNexus, - user01nexus.connReq.name, - ); - } catch(e: any) { - t.assertTrue(e.response.status == 502); - return; - } - t.assertTrue(false); -} -runLibeufinBadGatewayTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts deleted file mode 100644 index 9e1842d03..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts +++ /dev/null @@ -1,314 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { CoreApiResponse } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures"; -import { - DbInfo, - HarnessExchangeBankAccount, - ExchangeService, - GlobalTestState, - MerchantService, - setupDb, - WalletCli, -} from "../harness/harness.js"; -import { makeTestPayment } from "../harness/helpers.js"; -import { - LibeufinNexusApi, - LibeufinNexusService, - LibeufinSandboxApi, - LibeufinSandboxService, -} from "../harness/libeufin"; - -const exchangeIban = "DE71500105179674997361"; -const customerIban = "DE84500105176881385584"; -const customerBic = "BELADEBEXXX"; -const merchantIban = "DE42500105171245624648"; - -export interface LibeufinTestEnvironment { - commonDb: DbInfo; - exchange: ExchangeService; - exchangeBankAccount: HarnessExchangeBankAccount; - merchant: MerchantService; - wallet: WalletCli; - libeufinSandbox: LibeufinSandboxService; - libeufinNexus: LibeufinNexusService; -} - -/** - * Create a Taler environment with LibEuFin and an EBICS account. - */ -export async function createLibeufinTestEnvironment( - t: GlobalTestState, - coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("EUR")), -): Promise<LibeufinTestEnvironment> { - const db = await setupDb(t); - - const libeufinSandbox = await LibeufinSandboxService.create(t, { - httpPort: 5010, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - }); - - await libeufinSandbox.start(); - await libeufinSandbox.pingUntilAvailable(); - - const libeufinNexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - - await libeufinNexus.start(); - await libeufinNexus.pingUntilAvailable(); - - await LibeufinSandboxApi.createEbicsHost(libeufinSandbox, "host01"); - // Subscriber and bank Account for the exchange - await LibeufinSandboxApi.createEbicsSubscriber(libeufinSandbox, { - hostID: "host01", - partnerID: "partner01", - userID: "user01", - }); - await LibeufinSandboxApi.createEbicsBankAccount(libeufinSandbox, { - bic: "DEUTDEBB101", - iban: exchangeIban, - label: "exchangeacct", - name: "Taler Exchange", - subscriber: { - hostID: "host01", - partnerID: "partner01", - userID: "user01", - }, - currency: "EUR", - }); - // Subscriber and bank Account for the merchant - // (Merchant doesn't need EBICS access, but sandbox right now only supports EBICS - // accounts.) - await LibeufinSandboxApi.createEbicsSubscriber(libeufinSandbox, { - hostID: "host01", - partnerID: "partner02", - userID: "user02", - }); - await LibeufinSandboxApi.createEbicsBankAccount(libeufinSandbox, { - bic: "AUTOATW1XXX", - iban: merchantIban, - label: "merchantacct", - name: "Merchant", - subscriber: { - hostID: "host01", - partnerID: "partner02", - userID: "user02", - }, - currency: "EUR", - }); - - await LibeufinNexusApi.createEbicsBankConnection(libeufinNexus, { - name: "myconn", - ebicsURL: "http://localhost:5010/ebicsweb", - hostID: "host01", - partnerID: "partner01", - userID: "user01", - }); - await LibeufinNexusApi.connectBankConnection(libeufinNexus, "myconn"); - await LibeufinNexusApi.fetchAccounts(libeufinNexus, "myconn"); - await LibeufinNexusApi.importConnectionAccount( - libeufinNexus, - "myconn", - "exchangeacct", - "myacct", - ); - - await LibeufinNexusApi.createTwgFacade(libeufinNexus, { - name: "twg1", - accountName: "myacct", - connectionName: "myconn", - currency: "EUR", - reserveTransferLevel: "report", - }); - - await LibeufinNexusApi.createUser(libeufinNexus, { - username: "twguser", - password: "twgpw", - }); - - await LibeufinNexusApi.postPermission(libeufinNexus, { - action: "grant", - permission: { - subjectType: "user", - subjectId: "twguser", - resourceType: "facade", - resourceId: "twg1", - permissionName: "facade.talerWireGateway.history", - }, - }); - - await LibeufinNexusApi.postPermission(libeufinNexus, { - action: "grant", - permission: { - subjectType: "user", - subjectId: "twguser", - resourceType: "facade", - resourceId: "twg1", - permissionName: "facade.talerWireGateway.transfer", - }, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "EUR", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "EUR", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount: HarnessExchangeBankAccount = { - accountName: "twguser", - accountPassword: "twgpw", - accountPaytoUri: `payto://iban/${exchangeIban}?receiver-name=Exchange`, - wireGatewayApiBaseUrl: - "http://localhost:5011/facades/twg1/taler-wire-gateway/", - }; - - exchange.addBankAccount("1", exchangeBankAccount); - - exchange.addCoinConfigList(coinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://iban/${merchantIban}?receiver-name=Merchant`], - defaultWireTransferDelay: { d_ms: 0 }, - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - exchangeBankAccount, - libeufinNexus, - libeufinSandbox, - }; -} - -/** - * Run basic test with LibEuFin. - */ -export async function runLibeufinBasicTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - exchange, - merchant, - libeufinSandbox, - libeufinNexus, - } = await createLibeufinTestEnvironment(t); - - await wallet.client.call(WalletApiOperation.AddExchange, { - exchangeBaseUrl: exchange.baseUrl, - }); - - const wr = await wallet.client.call( - WalletApiOperation.AcceptManualWithdrawal, - { - exchangeBaseUrl: exchange.baseUrl, - amount: "EUR:10", - }, - ); - - const reservePub: string = wr.reservePub; - - await LibeufinSandboxApi.simulateIncomingTransaction( - libeufinSandbox, - "exchangeacct", - { - amount: "15.00", - debtorBic: customerBic, - debtorIban: customerIban, - debtorName: "Jane Customer", - subject: `Taler Top-up ${reservePub}`, - }, - ); - - await LibeufinNexusApi.fetchTransactions(libeufinNexus, "myacct"); - - await exchange.runWirewatchOnce(); - - await wallet.runUntilDone(); - - const bal = await wallet.client.call(WalletApiOperation.GetBalances, {}); - console.log("balances", JSON.stringify(bal, undefined, 2)); - t.assertAmountEquals(bal.balances[0].available, "EUR:14.7"); - - const order = { - summary: "Buy me!", - amount: "EUR:5", - fulfillment_url: "taler://fulfillment-success/thx", - }; - - await makeTestPayment(t, { wallet, merchant, order }); - - await exchange.runAggregatorOnce(); - await exchange.runTransferOnce(); - - await LibeufinNexusApi.submitAllPaymentInitiations(libeufinNexus, "myacct"); - - const exchangeTransactions = await LibeufinSandboxApi.getAccountTransactions( - libeufinSandbox, - "exchangeacct", - ); - - console.log( - "exchange transactions:", - JSON.stringify(exchangeTransactions, undefined, 2), - ); - - t.assertDeepEqual( - exchangeTransactions.payments[0].creditDebitIndicator, - "credit", - ); - t.assertDeepEqual( - exchangeTransactions.payments[1].creditDebitIndicator, - "debit", - ); - t.assertDeepEqual(exchangeTransactions.payments[1].debtorIban, exchangeIban); - t.assertDeepEqual( - exchangeTransactions.payments[1].creditorIban, - merchantIban, - ); -} -runLibeufinBasicTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts deleted file mode 100644 index 5a995fb69..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, delayMs } from "../harness/harness.js"; -import { - SandboxUserBundle, - NexusUserBundle, - launchLibeufinServices, - LibeufinSandboxApi, - LibeufinNexusApi, -} from "../harness/libeufin"; - -/** - * This test checks how the C52 and C53 coordinate. It'll test - * whether fresh transactions stop showing as C52 after they get - * included in a bank statement. - */ -export async function runLibeufinC5xTest(t: GlobalTestState) { - /** - * User saltetd "01" - */ - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - const user01sandbox = new SandboxUserBundle("01"); - - /** - * User saltetd "02". - */ - const user02nexus = new NexusUserBundle( - "02", - "http://localhost:5010/ebicsweb", - ); - const user02sandbox = new SandboxUserBundle("02"); - - /** - * Launch Sandbox and Nexus. - */ - const libeufinServices = await launchLibeufinServices( - t, - [user01nexus, user02nexus], - [user01sandbox, user02sandbox], - ["twg"], - ); - - // Check that C52 and C53 have zero entries. - - // C52 - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - "all", // range - "report", // level - ); - // C53 - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - "all", // range - "statement", // level - ); - const nexusTxs = await LibeufinNexusApi.getAccountTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - ); - t.assertTrue(nexusTxs.data["transactions"].length == 0); - - // Addressing one payment to user 01 - await libeufinServices.libeufinSandbox.makeTransaction( - user02sandbox.ebicsBankAccount.label, // debit - user01sandbox.ebicsBankAccount.label, // credit - "EUR:10", - "first payment", - ); - - // Checking that C52 has one and C53 has zero. - - let expectOne = await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - "all", // range - "report", // C52 - ); - t.assertTrue(expectOne.data.newTransactions == 1); - t.assertTrue(expectOne.data.downloadedTransactions == 1); - - let expectZero = await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - "all", // range - "statement", // C53 - ); - t.assertTrue(expectZero.data.newTransactions == 0); - t.assertTrue(expectZero.data.downloadedTransactions == 0); - - // Ticking now: the one payment should be downloaded - // in a C53 but not in a C52. In any case, the payment - // is not new anymore, because it was already ingested - // when it was downloaded for the first time along the - // c52 above. - await libeufinServices.libeufinSandbox.c53tick(); - - expectOne = await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - "all", // range - "statement", // C53 - ); - t.assertTrue(expectOne.data.downloadedTransactions == 1); - t.assertTrue(expectOne.data.newTransactions == 0); - - expectZero = await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - "all", // range - "report", // C52 - ); - t.assertTrue(expectZero.data.downloadedTransactions == 0); - t.assertTrue(expectZero.data.newTransactions == 0); -} -runLibeufinC5xTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts deleted file mode 100644 index 0bbd4fd28..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts +++ /dev/null @@ -1,169 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - SandboxUserBundle, - NexusUserBundle, - launchLibeufinServices, - LibeufinNexusApi, - LibeufinSandboxApi, -} from "../harness/libeufin"; - -/** - * Testing the Anastasis API, offered by the Anastasis facade. - */ -export async function runLibeufinAnastasisFacadeTest(t: GlobalTestState) { - /** - * User saltetd "01" - */ - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - const user01sandbox = new SandboxUserBundle("01"); - - /** - * Launch Sandbox and Nexus. - */ - const libeufinServices = await launchLibeufinServices( - t, - [user01nexus], - [user01sandbox], - ["anastasis"], // create only one Anastasis facade. - ); - let resp = await LibeufinNexusApi.getAllFacades( - libeufinServices.libeufinNexus, - ); - // check that original facade shows up. - t.assertTrue(resp.data["facades"][0]["name"] == user01nexus.anastasisReq["name"]); - const anastasisBaseUrl: string = resp.data["facades"][0]["baseUrl"]; - t.assertTrue(typeof anastasisBaseUrl === "string"); - t.assertTrue(anastasisBaseUrl.startsWith("http://")); - t.assertTrue(anastasisBaseUrl.endsWith("/")); - - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - ); - - await LibeufinNexusApi.postPermission( - libeufinServices.libeufinNexus, { - action: "grant", - permission: { - subjectId: user01nexus.userReq.username, - subjectType: "user", - resourceType: "facade", - resourceId: user01nexus.anastasisReq.name, - permissionName: "facade.anastasis.history", - }, - } - ); - - // check if empty. - let txsEmpty = await LibeufinNexusApi.getAnastasisTransactions( - libeufinServices.libeufinNexus, - anastasisBaseUrl, {delta: 5}) - - t.assertTrue(txsEmpty.data.incoming_transactions.length == 0); - - LibeufinSandboxApi.simulateIncomingTransaction( - libeufinServices.libeufinSandbox, - user01sandbox.ebicsBankAccount.label, - { - debtorIban: "ES3314655813489414469157", - debtorBic: "BCMAESM1XXX", - debtorName: "Mock Donor", - subject: "Anastasis donation", - amount: "3", // Sandbox takes currency from its 'config' - }, - ) - - LibeufinSandboxApi.simulateIncomingTransaction( - libeufinServices.libeufinSandbox, - user01sandbox.ebicsBankAccount.label, - { - debtorIban: "ES3314655813489414469157", - debtorBic: "BCMAESM1XXX", - debtorName: "Mock Donor", - subject: "another Anastasis donation", - amount: "1", // Sandbox takes currency from its "config" - }, - ) - - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - ); - - let txs = await LibeufinNexusApi.getAnastasisTransactions( - libeufinServices.libeufinNexus, - anastasisBaseUrl, - {delta: 5}, - user01nexus.userReq.username, - user01nexus.userReq.password, - ); - - // check the two payments show up - let txsList = txs.data.incoming_transactions - t.assertTrue(txsList.length == 2); - t.assertTrue([txsList[0].subject, txsList[1].subject].includes("Anastasis donation")); - t.assertTrue([txsList[0].subject, txsList[1].subject].includes("another Anastasis donation")); - t.assertTrue(txsList[0].row_id == 1) - t.assertTrue(txsList[1].row_id == 2) - - LibeufinSandboxApi.simulateIncomingTransaction( - libeufinServices.libeufinSandbox, - user01sandbox.ebicsBankAccount.label, - { - debtorIban: "ES3314655813489414469157", - debtorBic: "BCMAESM1XXX", - debtorName: "Mock Donor", - subject: "last Anastasis donation", - amount: "10.10", // Sandbox takes currency from its "config" - }, - ) - - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - ); - - let txsLast = await LibeufinNexusApi.getAnastasisTransactions( - libeufinServices.libeufinNexus, - anastasisBaseUrl, - {delta: 5, start: 2}, - user01nexus.userReq.username, - user01nexus.userReq.password, - ); - console.log(txsLast.data.incoming_transactions[0].subject == "last Anastasis donation"); - - let txsReverse = await LibeufinNexusApi.getAnastasisTransactions( - libeufinServices.libeufinNexus, - anastasisBaseUrl, - {delta: -5, start: 4}, - user01nexus.userReq.username, - user01nexus.userReq.password, - ); - t.assertTrue(txsReverse.data.incoming_transactions[0].row_id == 3); - t.assertTrue(txsReverse.data.incoming_transactions[1].row_id == 2); - t.assertTrue(txsReverse.data.incoming_transactions[2].row_id == 1); -} - -runLibeufinAnastasisFacadeTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts deleted file mode 100644 index 5dc31f0bf..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - SandboxUserBundle, - NexusUserBundle, - launchLibeufinServices, - LibeufinSandboxApi, - LibeufinNexusApi, -} from "../harness/libeufin"; - -/** - * Run basic test with LibEuFin. - */ -export async function runLibeufinKeyrotationTest(t: GlobalTestState) { - /** - * User saltetd "01" - */ - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - const user01sandbox = new SandboxUserBundle("01"); - - /** - * Launch Sandbox and Nexus. - */ - const libeufinServices = await launchLibeufinServices( - t, [user01nexus], [user01sandbox], - ); - - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - ); - - /* Rotate the Sandbox keys, and fetch the transactions again */ - await LibeufinSandboxApi.rotateKeys( - libeufinServices.libeufinSandbox, - user01sandbox.ebicsBankAccount.subscriber.hostID, - ); - - try { - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - ); - } catch (e: any) { - /** - * Asserting that Nexus responded with a 500 Internal server - * error, because the bank signed the last response with a new - * key pair that was never downloaded by Nexus. - * - * NOTE: the bank accepted the request addressed to the old - * public key. Should it in this case reject the request even - * before trying to verify it? - */ - t.assertTrue(e.response.status == 500); - t.assertTrue(e.response.data.code == 9000); - } -} -runLibeufinKeyrotationTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts deleted file mode 100644 index 23d76081f..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, delayMs } from "../harness/harness.js"; -import { - SandboxUserBundle, - NexusUserBundle, - launchLibeufinServices, - LibeufinSandboxApi, - LibeufinNexusApi, -} from "../harness/libeufin"; - -/** - * This test checks how the C52 and C53 coordinate. It'll test - * whether fresh transactions stop showing as C52 after they get - * included in a bank statement. - */ -export async function runLibeufinNexusBalanceTest(t: GlobalTestState) { - /** - * User saltetd "01" - */ - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - const user01sandbox = new SandboxUserBundle("01"); - - /** - * User saltetd "02". - */ - const user02nexus = new NexusUserBundle( - "02", - "http://localhost:5010/ebicsweb", - ); - const user02sandbox = new SandboxUserBundle("02"); - - /** - * Launch Sandbox and Nexus. - */ - const libeufinServices = await launchLibeufinServices( - t, - [user01nexus, user02nexus], - [user01sandbox, user02sandbox], - ["twg"], - ); - - // user 01 gets 10 - await libeufinServices.libeufinSandbox.makeTransaction( - user02sandbox.ebicsBankAccount.label, // debit - user01sandbox.ebicsBankAccount.label, // credit - "EUR:10", - "first payment", - ); - - // user 01 gets another 10 - await libeufinServices.libeufinSandbox.makeTransaction( - user02sandbox.ebicsBankAccount.label, // debit - user01sandbox.ebicsBankAccount.label, // credit - "EUR:10", - "first payment", - ); - - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - "all", // range - "report", // level - ); - - // Check that user 01 has 20, via Nexus. - let accountInfo = await LibeufinNexusApi.getBankAccount( - libeufinServices.libeufinNexus, - user01nexus.localAccountName - ); - t.assertTrue(accountInfo.data.lastSeenBalance == "EUR:20"); - - // user 01 gives 30 - await libeufinServices.libeufinSandbox.makeTransaction( - user01sandbox.ebicsBankAccount.label, // credit - user02sandbox.ebicsBankAccount.label, // debit - "EUR:30", - "third payment", - ); - - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - "all", // range - "report", // level - ); - - let accountInfoDebit = await LibeufinNexusApi.getBankAccount( - libeufinServices.libeufinNexus, - user01nexus.localAccountName - ); - t.assertTrue(accountInfoDebit.data.lastSeenBalance == "-EUR:10"); -} -runLibeufinNexusBalanceTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts deleted file mode 100644 index 39517f247..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, delayMs } from "../harness/harness.js"; -import { - SandboxUserBundle, - NexusUserBundle, - launchLibeufinServices, - LibeufinSandboxApi, - LibeufinNexusApi, -} from "../harness/libeufin"; - -/** - * User 01 expects a refund from user 02, and expectedly user 03 - * should not be involved in the process. - */ -export async function runLibeufinRefundMultipleUsersTest(t: GlobalTestState) { - /** - * User saltetd "01" - */ - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - const user01sandbox = new SandboxUserBundle("01"); - - /** - * User saltetd "02" - */ - const user02nexus = new NexusUserBundle( - "02", - "http://localhost:5010/ebicsweb", - ); - const user02sandbox = new SandboxUserBundle("02"); - - /** - * User saltetd "03" - */ - const user03nexus = new NexusUserBundle( - "03", - "http://localhost:5010/ebicsweb", - ); - const user03sandbox = new SandboxUserBundle("03"); - - /** - * Launch Sandbox and Nexus. - */ - const libeufinServices = await launchLibeufinServices( - t, - [user01nexus, user02nexus], - [user01sandbox, user02sandbox], - ["twg"], - ); - - // user 01 gets the payment - await libeufinServices.libeufinSandbox.makeTransaction( - user02sandbox.ebicsBankAccount.label, // debit - user01sandbox.ebicsBankAccount.label, // credit - "EUR:1", - "not a public key", - ); - - // user 01 fetches the payments - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - ); - - // user 01 tries to submit the reimbursement, as - // the payment didn't have a valid public key in - // the subject. - await LibeufinNexusApi.submitInitiatedPayment( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - "1", // so far the only one that can exist. - ); - - // user 02 checks whether a reimbursement arrived. - let history = await LibeufinSandboxApi.getAccountTransactions( - libeufinServices.libeufinSandbox, - user02sandbox.ebicsBankAccount["label"], - ); - // reimbursement arrived IFF the total payments are 2: - // 1 the original (faulty) transaction + 1 the reimbursement. - t.assertTrue(history["payments"].length == 2); -} - -runLibeufinRefundMultipleUsersTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts deleted file mode 100644 index d91ae88bb..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, delayMs } from "../harness/harness.js"; -import { - SandboxUserBundle, - NexusUserBundle, - launchLibeufinServices, - LibeufinSandboxApi, - LibeufinNexusApi, -} from "../harness/libeufin"; - -/** - * Run basic test with LibEuFin. - */ -export async function runLibeufinRefundTest(t: GlobalTestState) { - /** - * User saltetd "01" - */ - const user01nexus = new NexusUserBundle( - "01", - "http://localhost:5010/ebicsweb", - ); - const user01sandbox = new SandboxUserBundle("01"); - - /** - * User saltetd "02" - */ - const user02nexus = new NexusUserBundle( - "02", - "http://localhost:5010/ebicsweb", - ); - const user02sandbox = new SandboxUserBundle("02"); - - /** - * Launch Sandbox and Nexus. - */ - const libeufinServices = await launchLibeufinServices( - t, - [user01nexus, user02nexus], - [user01sandbox, user02sandbox], - ["twg"], - ); - - // user 02 pays user 01 with a faulty (non Taler) subject. - await libeufinServices.libeufinSandbox.makeTransaction( - user02sandbox.ebicsBankAccount.label, // debit - user01sandbox.ebicsBankAccount.label, // credit - "EUR:1", - "not a public key", - ); - - // The bad payment should be now ingested and prepared as - // a reimbursement. - await LibeufinNexusApi.fetchTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - ); - // Check that the payment arrived at the Nexus. - const nexusTxs = await LibeufinNexusApi.getAccountTransactions( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - ); - t.assertTrue(nexusTxs.data["transactions"].length == 1); - - // Submit the reimbursement - await LibeufinNexusApi.submitInitiatedPayment( - libeufinServices.libeufinNexus, - user01nexus.localAccountName, - // The initiated payment (= the reimbursement) ID below - // got set by the Taler facade; at this point only one must - // exist. If "1" is not found, a 404 will make this test fail. - "1", - ); - - // user 02 checks whether the reimbursement arrived. - let history = await LibeufinSandboxApi.getAccountTransactions( - libeufinServices.libeufinSandbox, - user02sandbox.ebicsBankAccount["label"], - ); - // 2 payments must exist: 1 the original (faulty) payment + - // 1 the reimbursement. - t.assertTrue(history["payments"].length == 2); -} -runLibeufinRefundTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts deleted file mode 100644 index 5560f091a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - NexusUserBundle, - LibeufinNexusApi, - LibeufinNexusService, - LibeufinSandboxService, - LibeufinSandboxApi, - findNexusPayment, -} from "../harness/libeufin"; - -export async function runLibeufinSandboxWireTransferCliTest(t: GlobalTestState) { - - const sandbox = await LibeufinSandboxService.create(t, { - httpPort: 5012, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - }); - await sandbox.start(); - await sandbox.pingUntilAvailable(); - await LibeufinSandboxApi.createBankAccount(sandbox, { - iban: "DE71500105179674997361", - bic: "BELADEBEXXX", - name: "Mock Name", - label: "mock-account", - currency: "EUR" - }); - - await LibeufinSandboxApi.createBankAccount(sandbox, { - iban: "DE71500105179674997364", - bic: "BELADEBEXXX", - name: "Mock Name 2", - label: "mock-account-2", - currency: "EUR" - }); - await sandbox.makeTransaction( - "mock-account", "mock-account-2", "EUR:1", "one!" - ); - await sandbox.makeTransaction( - "mock-account", "mock-account-2", "EUR:1", "two!" - ); - await sandbox.makeTransaction( - "mock-account", "mock-account-2", "EUR:1", "three!" - ); - await sandbox.makeTransaction( - "mock-account-2", "mock-account", "EUR:1", "Give one back." - ); - await sandbox.makeTransaction( - "mock-account-2", "mock-account", "EUR:0.11", "Give fraction back." - ); - let ret = await LibeufinSandboxApi.getAccountInfoWithBalance(sandbox, "mock-account-2"); - console.log(ret.data.balance) - t.assertTrue(ret.data.balance == "EUR:1.89") -} -runLibeufinSandboxWireTransferCliTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts deleted file mode 100644 index 71a1e8c4b..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - LibeufinNexusService, - LibeufinSandboxService, - LibeufinCli, -} from "../harness/libeufin"; - -/** - * Run basic test with LibEuFin. - */ -export async function runLibeufinTutorialTest(t: GlobalTestState) { - // Set up test environment - - const libeufinSandbox = await LibeufinSandboxService.create(t, { - httpPort: 5010, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - }); - - await libeufinSandbox.start(); - await libeufinSandbox.pingUntilAvailable(); - - const libeufinNexus = await LibeufinNexusService.create(t, { - httpPort: 5011, - databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - }); - - const nexusUser = { username: "foo", password: "secret" }; - const libeufinCli = new LibeufinCli(t, { - sandboxUrl: libeufinSandbox.baseUrl, - nexusUrl: libeufinNexus.baseUrl, - sandboxDatabaseUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`, - nexusDatabaseUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`, - user: nexusUser, - }); - - const ebicsDetails = { - hostId: "testhost", - partnerId: "partner01", - userId: "user01", - }; - const bankAccountDetails = { - currency: "EUR", - iban: "DE18500105172929531888", - bic: "INGDDEFFXXX", - personName: "Jane Normal", - accountName: "testacct01", - }; - - await libeufinCli.checkSandbox(); - await libeufinCli.createEbicsHost("testhost"); - await libeufinCli.createEbicsSubscriber(ebicsDetails); - await libeufinCli.createEbicsBankAccount(ebicsDetails, bankAccountDetails); - await libeufinCli.generateTransactions(bankAccountDetails.accountName); - - await libeufinNexus.start(); - await libeufinNexus.pingUntilAvailable(); - - await libeufinNexus.createNexusSuperuser(nexusUser); - const connectionDetails = { - subscriberDetails: ebicsDetails, - ebicsUrl: `${libeufinSandbox.baseUrl}ebicsweb`, // FIXME: need appropriate URL concatenation - connectionName: "my-ebics-conn", - }; - await libeufinCli.createEbicsConnection(connectionDetails); - await libeufinCli.createBackupFile({ - passphrase: "secret", - outputFile: `${t.testDir}/connection-backup.json`, - connectionName: connectionDetails.connectionName, - }); - await libeufinCli.createKeyLetter({ - outputFile: `${t.testDir}/letter.pdf`, - connectionName: connectionDetails.connectionName, - }); - await libeufinCli.connect(connectionDetails.connectionName); - await libeufinCli.downloadBankAccounts(connectionDetails.connectionName); - await libeufinCli.listOfferedBankAccounts(connectionDetails.connectionName); - - const bankAccountImportDetails = { - offeredBankAccountName: bankAccountDetails.accountName, - nexusBankAccountName: "at-nexus-testacct01", - connectionName: connectionDetails.connectionName, - }; - - await libeufinCli.importBankAccount(bankAccountImportDetails); - await libeufinSandbox.c53tick() - await libeufinCli.fetchTransactions(bankAccountImportDetails.nexusBankAccountName); - await libeufinCli.transactions(bankAccountImportDetails.nexusBankAccountName); - - const paymentDetails = { - creditorIban: "DE42500105171245624648", - creditorBic: "BELADEBEXXX", - creditorName: "Mina Musterfrau", - subject: "Purchase 01234", - amount: "1.0", - currency: "EUR", - nexusBankAccountName: bankAccountImportDetails.nexusBankAccountName, - }; - await libeufinCli.preparePayment(paymentDetails); - await libeufinCli.submitPayment(paymentDetails, "1"); - - await libeufinCli.newTalerWireGatewayFacade({ - accountName: bankAccountImportDetails.nexusBankAccountName, - connectionName: "my-ebics-conn", - currency: "EUR", - facadeName: "my-twg", - }); - await libeufinCli.listFacades(); -} -runLibeufinTutorialTest.suites = ["libeufin"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts deleted file mode 100644 index 8e8f966b9..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts +++ /dev/null @@ -1,244 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - BankService, - ExchangeService, - GlobalTestState, - MerchantPrivateApi, - MerchantService, - setupDb, - WalletCli, -} from "../harness/harness.js"; -import { - withdrawViaBank, - createFaultInjectedMerchantTestkudosEnvironment, - FaultyMerchantTestEnvironment, -} from "../harness/helpers.js"; -import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, - ConfirmPayResultType, -} from "@gnu-taler/taler-util"; -import axios from "axios"; -import { - FaultInjectedExchangeService, - FaultInjectedMerchantService, - FaultInjectionRequestContext, -} from "../harness/faultInjection"; -import { defaultCoinConfig } from "../harness/denomStructures"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { URL } from "url"; - -/** - * Run a test case with a simple TESTKUDOS Taler environment, consisting - * of one exchange, one bank and one merchant. - */ -export async function createConfusedMerchantTestkudosEnvironment( - t: GlobalTestState, -): Promise<FaultyMerchantTestEnvironment> { - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083); - const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange( - faultyExchange, - exchangeBankAccount.accountPaytoUri, - ); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - // Confuse the merchant by adding the non-proxied exchange. - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - faultyMerchant, - faultyExchange, - }; -} - -/** - * Confuse the merchant by having one URL for the same exchange in the config, - * but sending coins from the same exchange with a different URL. - */ -export async function runMerchantExchangeConfusionTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - faultyExchange, - faultyMerchant, - } = await createConfusedMerchantTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { - wallet, - bank, - exchange: faultyExchange, - amount: "TESTKUDOS:20", - }); - - /** - * ========================================================================= - * Create an order and let the wallet pay under a session ID - * - * We check along the way that the JSON response to /orders/{order_id} - * returns the right thing. - * ========================================================================= - */ - - const merchant = faultyMerchant; - - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-one", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - let publicOrderStatusUrl = orderStatus.order_status_url; - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(pubUnpaidStatus); - - let preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: pubUnpaidStatus.taler_pay_uri, - }, - ); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - const proposalId = preparePayResp.proposalId; - - const orderUrlWithHash = new URL(publicOrderStatusUrl); - orderUrlWithHash.searchParams.set("h_contract", preparePayResp.contractTermsHash); - - console.log("requesting", orderUrlWithHash.href); - - publicOrderStatusResp = await axios.get(orderUrlWithHash.href, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); -} - -runMerchantExchangeConfusionTest.suites = ["merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts deleted file mode 100644 index 589c79120..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { URL } from "@gnu-taler/taler-util"; -import axios from "axios"; -import { - ExchangeService, - GlobalTestState, - MerchantApiClient, - MerchantService, - setupDb, -} from "../harness/harness.js"; - -/** - * Test instance deletion and authentication for it - */ -export async function runMerchantInstancesDeleteTest(t: GlobalTestState) { - // Set up test environment - - const db = await setupDb(t); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - // We add the exchange to the config, but note that the exchange won't be started. - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - // Base URL for the default instance. - const baseUrl = merchant.makeInstanceBaseUrl(); - - { - const r = await axios.get(new URL("config", baseUrl).href); - console.log(r.data); - t.assertDeepEqual(r.data.currency, "TESTKUDOS"); - } - - // Instances should initially be empty - { - const r = await axios.get(new URL("management/instances", baseUrl).href); - t.assertDeepEqual(r.data.instances, []); - } - - // Add an instance, no auth! - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - auth: { - method: "external", - }, - }); - - // Add an instance, no auth! - await merchant.addInstance({ - id: "myinst", - name: "Second Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - auth: { - method: "external", - }, - }); - - let merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), { - method: "external", - }); - - await merchantClient.changeAuth({ - method: "token", - token: "secret-token:foobar", - }); - - merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), { - method: "token", - token: "secret-token:foobar", - }); - - // Check that deleting an instance checks the auth - // of the default instance. - { - const unauthMerchantClient = new MerchantApiClient( - merchant.makeInstanceBaseUrl(), - { - method: "token", - token: "secret-token:invalid", - }, - ); - - const exc = await t.assertThrowsAsync(async () => { - await unauthMerchantClient.deleteInstance("myinst"); - }); - console.log("Got expected exception", exc); - t.assertAxiosError(exc); - t.assertDeepEqual(exc.response?.status, 401); - } -} - -runMerchantInstancesDeleteTest.suites = ["merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts deleted file mode 100644 index fc5e7305a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts +++ /dev/null @@ -1,178 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import axios from "axios"; -import { - ExchangeService, - GlobalTestState, - MerchantApiClient, - MerchantService, - setupDb, -} from "../harness/harness.js"; - -/** - * Do basic checks on instance management and authentication. - */ -export async function runMerchantInstancesUrlsTest(t: GlobalTestState) { - // Set up test environment - - const db = await setupDb(t); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - const clientForDefault = new MerchantApiClient( - merchant.makeInstanceBaseUrl(), - { - method: "token", - token: "secret-token:i-am-default", - }, - ); - - await clientForDefault.createInstance({ - id: "default", - address: {}, - default_max_deposit_fee: "TESTKUDOS:1", - default_max_wire_fee: "TESTKUDOS:1", - default_pay_delay: { d_ms: 60000 }, - default_wire_fee_amortization: 1, - default_wire_transfer_delay: { d_ms: 60000 }, - jurisdiction: {}, - name: "My Default Instance", - payto_uris: ["payto://x-taler-bank/foo/bar"], - auth: { - method: "token", - token: "secret-token:i-am-default", - }, - }); - - await clientForDefault.createInstance({ - id: "myinst", - address: {}, - default_max_deposit_fee: "TESTKUDOS:1", - default_max_wire_fee: "TESTKUDOS:1", - default_pay_delay: { d_ms: 60000 }, - default_wire_fee_amortization: 1, - default_wire_transfer_delay: { d_ms: 60000 }, - jurisdiction: {}, - name: "My Second Instance", - payto_uris: ["payto://x-taler-bank/foo/bar"], - auth: { - method: "token", - token: "secret-token:i-am-myinst", - }, - }); - - async function check(url: string, token: string, expectedStatus: number) { - const resp = await axios.get(url, { - headers: { - Authorization: `Bearer ${token}`, - }, - validateStatus: () => true, - }); - console.log( - `checking ${url}, expected ${expectedStatus}, got ${resp.status}`, - ); - t.assertDeepEqual(resp.status, expectedStatus); - } - - const tokDefault = "secret-token:i-am-default"; - - const defaultBaseUrl = merchant.makeInstanceBaseUrl(); - - await check( - `${defaultBaseUrl}private/instances/default/instances/default/config`, - tokDefault, - 404, - ); - - // Instance management is only available when accessing the default instance - // directly. - await check( - `${defaultBaseUrl}instances/default/private/instances`, - "foo", - 404, - ); - - // Non-default instances don't allow instance management. - await check(`${defaultBaseUrl}instances/foo/private/instances`, "foo", 404); - await check( - `${defaultBaseUrl}instances/myinst/private/instances`, - "foo", - 404, - ); - - await check(`${defaultBaseUrl}config`, "foo", 200); - await check(`${defaultBaseUrl}instances/default/config`, "foo", 200); - await check(`${defaultBaseUrl}instances/myinst/config`, "foo", 200); - await check(`${defaultBaseUrl}instances/foo/config`, "foo", 404); - await check( - `${defaultBaseUrl}instances/default/instances/config`, - "foo", - 404, - ); - - await check( - `${defaultBaseUrl}private/instances/myinst/config`, - tokDefault, - 404, - ); - - await check( - `${defaultBaseUrl}instances/myinst/private/orders`, - tokDefault, - 401, - ); - - await check( - `${defaultBaseUrl}instances/myinst/private/orders`, - tokDefault, - 401, - ); - - await check( - `${defaultBaseUrl}instances/myinst/private/orders`, - "secret-token:i-am-myinst", - 200, - ); - - await check( - `${defaultBaseUrl}private/instances/myinst/orders`, - tokDefault, - 404, - ); -} - -runMerchantInstancesUrlsTest.suites = ["merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts deleted file mode 100644 index 46af87922..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts +++ /dev/null @@ -1,182 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { URL } from "@gnu-taler/taler-util"; -import axios from "axios"; -import { - ExchangeService, - GlobalTestState, - MerchantApiClient, - MerchantService, - setupDb, -} from "../harness/harness.js"; - -/** - * Do basic checks on instance management and authentication. - */ -export async function runMerchantInstancesTest(t: GlobalTestState) { - // Set up test environment - - const db = await setupDb(t); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - // We add the exchange to the config, but note that the exchange won't be started. - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - // Base URL for the default instance. - const baseUrl = merchant.makeInstanceBaseUrl(); - - { - const r = await axios.get(new URL("config", baseUrl).href); - console.log(r.data); - t.assertDeepEqual(r.data.currency, "TESTKUDOS"); - } - - // Instances should initially be empty - { - const r = await axios.get(new URL("management/instances", baseUrl).href); - t.assertDeepEqual(r.data.instances, []); - } - - // Add an instance, no auth! - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - auth: { - method: "external", - }, - }); - - // Add an instance, no auth! - await merchant.addInstance({ - id: "myinst", - name: "Second Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - auth: { - method: "external", - }, - }); - - let merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), { - method: "external", - }); - - { - const r = await merchantClient.getInstances(); - t.assertDeepEqual(r.instances.length, 2); - } - - // Check that a "malformed" bearer Authorization header gets ignored - { - const url = merchant.makeInstanceBaseUrl(); - const resp = await axios.get(new URL("management/instances", url).href, { - headers: { - Authorization: "foo bar-baz", - }, - }); - t.assertDeepEqual(resp.status, 200); - } - - { - const fullDetails = await merchantClient.getInstanceFullDetails("default"); - t.assertDeepEqual(fullDetails.auth.method, "external"); - } - - await merchantClient.changeAuth({ - method: "token", - token: "secret-token:foobar", - }); - - // Now this should fail, as we didn't change the auth of the client yet. - const exc = await t.assertThrowsAsync(async () => { - console.log("requesting instances with auth", merchantClient.auth); - const resp = await merchantClient.getInstances(); - console.log("instances result:", resp); - }); - - console.log(exc); - - t.assertAxiosError(exc); - t.assertTrue(exc.response?.status === 401); - - merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), { - method: "token", - token: "secret-token:foobar", - }); - - // With the new client auth settings, request should work again. - await merchantClient.getInstances(); - - // Now, try some variations. - { - const url = merchant.makeInstanceBaseUrl(); - const resp = await axios.get(new URL("management/instances", url).href, { - headers: { - // Note the spaces - Authorization: "Bearer secret-token:foobar", - }, - }); - t.assertDeepEqual(resp.status, 200); - } - - // Check that auth is reported properly - { - const fullDetails = await merchantClient.getInstanceFullDetails("default"); - t.assertDeepEqual(fullDetails.auth.method, "token"); - // Token should *not* be reported back. - t.assertDeepEqual(fullDetails.auth.token, undefined); - } - - // Check that deleting an instance checks the auth - // of the default instance. - { - const unauthMerchantClient = new MerchantApiClient( - merchant.makeInstanceBaseUrl(), - { - method: "external", - }, - ); - - const exc = await t.assertThrowsAsync(async () => { - await unauthMerchantClient.deleteInstance("myinst"); - }); - console.log(exc); - t.assertAxiosError(exc); - t.assertDeepEqual(exc.response?.status, 401); - } -} - -runMerchantInstancesTest.suites = ["merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts deleted file mode 100644 index 556d9074e..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts +++ /dev/null @@ -1,161 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; -import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, - ConfirmPayResultType, - URL, -} from "@gnu-taler/taler-util"; -import axios from "axios"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runMerchantLongpollingTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - /** - * ========================================================================= - * Create an order and let the wallet pay under a session ID - * - * We check along the way that the JSON response to /orders/{order_id} - * returns the right thing. - * ========================================================================= - */ - - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - }, - create_token: false, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-one", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - let publicOrderStatusUrl = new URL(orderStatus.order_status_url); - - // First, request order status without longpolling - { - console.log("requesting", publicOrderStatusUrl.href); - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (before claiming, no long polling), but got ${publicOrderStatusResp.status}`, - ); - } - } - - // Now do long-polling for half a second! - publicOrderStatusUrl.searchParams.set("timeout_ms", "500"); - - console.log("requesting", publicOrderStatusUrl.href); - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (before claiming, with long-polling), but got ${publicOrderStatusResp.status}`, - ); - } - - let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(pubUnpaidStatus); - - /** - * ========================================================================= - * Now actually pay, but WHILE a long poll is active! - * ========================================================================= - */ - - let preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: pubUnpaidStatus.taler_pay_uri, - }, - ); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - publicOrderStatusUrl.searchParams.set("timeout_ms", "5000"); - publicOrderStatusUrl.searchParams.set( - "h_contract", - preparePayResp.contractTermsHash, - ); - - let publicOrderStatusPromise = axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - const proposalId = preparePayResp.proposalId; - - publicOrderStatusResp = await publicOrderStatusPromise; - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); -} - -runMerchantLongpollingTest.suites = ["merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts deleted file mode 100644 index 466b1efbd..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts +++ /dev/null @@ -1,296 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - GlobalTestState, - MerchantPrivateApi, - BankServiceInterface, - MerchantServiceInterface, - WalletCli, - ExchangeServiceInterface, -} from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; -import { - URL, - durationFromSpec, - PreparePayResultType, -} from "@gnu-taler/taler-util"; -import axios from "axios"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -async function testRefundApiWithFulfillmentUrl( - t: GlobalTestState, - env: { - merchant: MerchantServiceInterface; - bank: BankServiceInterface; - wallet: WalletCli; - exchange: ExchangeServiceInterface; - }, -): Promise<void> { - const { wallet, bank, exchange, merchant } = env; - - // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/fulfillment", - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUri = orderStatus.taler_pay_uri; - const orderId = orderResp.order_id; - - // Make wallet pay for the order - - let preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.AlreadyConfirmed, - ); - - await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5"); - - // Now test what the merchant gives as a response for various requests to the - // public order status URL! - - let publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - publicOrderStatusUrl.searchParams.set( - "h_contract", - preparePayResult.contractTermsHash, - ); - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - console.log(publicOrderStatusResp.data); - t.assertTrue(publicOrderStatusResp.status === 200); - t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5"); - - publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - console.log(`requesting order status via '${publicOrderStatusUrl.href}'`); - publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - console.log(publicOrderStatusResp.status); - console.log(publicOrderStatusResp.data); - // We didn't give any authentication, so we should get a fulfillment URL back - t.assertTrue(publicOrderStatusResp.status === 403); -} - -async function testRefundApiWithFulfillmentMessage( - t: GlobalTestState, - env: { - merchant: MerchantServiceInterface; - bank: BankServiceInterface; - wallet: WalletCli; - exchange: ExchangeServiceInterface; - }, -): Promise<void> { - const { wallet, bank, exchange, merchant } = env; - - // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_message: "Thank you for buying foobar", - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUri = orderStatus.taler_pay_uri; - const orderId = orderResp.order_id; - - // Make wallet pay for the order - - let preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.AlreadyConfirmed, - ); - - await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5"); - - // Now test what the merchant gives as a response for various requests to the - // public order status URL! - - let publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - publicOrderStatusUrl.searchParams.set( - "h_contract", - preparePayResult.contractTermsHash, - ); - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - console.log(publicOrderStatusResp.data); - t.assertTrue(publicOrderStatusResp.status === 200); - t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5"); - - publicOrderStatusUrl = new URL( - `orders/${orderId}`, - merchant.makeInstanceBaseUrl(), - ); - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - console.log(publicOrderStatusResp.data); - // We didn't give any authentication, so we should get a fulfillment URL back - t.assertTrue(publicOrderStatusResp.status === 403); -} - -/** - * Test case for the refund API of the merchant backend. - */ -export async function runMerchantRefundApiTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - await testRefundApiWithFulfillmentUrl(t, { - wallet, - bank, - exchange, - merchant, - }); - - await testRefundApiWithFulfillmentMessage(t, { - wallet, - bank, - exchange, - merchant, - }); -} - -runMerchantRefundApiTest.suites = ["merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts deleted file mode 100644 index 70edaaf0c..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts +++ /dev/null @@ -1,620 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - ConfirmPayResultType, - PreparePayResultType, - URL, - encodeCrock, - getRandomBytes, -} from "@gnu-taler/taler-util"; -import { NodeHttpLib, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - BankService, - ExchangeService, - GlobalTestState, - MerchantPrivateApi, - MerchantService, - WalletCli, -} from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, -} from "../harness/helpers.js"; - -const httpLib = new NodeHttpLib(); - -interface Context { - merchant: MerchantService; - merchantBaseUrl: string; - bank: BankService; - exchange: ExchangeService; -} - -async function testWithClaimToken( - t: GlobalTestState, - c: Context, -): Promise<void> { - const wallet = new WalletCli(t, "withclaimtoken"); - const { bank, exchange } = c; - const { merchant, merchantBaseUrl } = c; - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - const sessionId = "mysession"; - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }); - - const claimToken = orderResp.token; - const orderId = orderResp.order_id; - t.assertTrue(!!claimToken); - let talerPayUri: string; - - { - const httpResp = await httpLib.get( - new URL(`orders/${orderId}`, merchantBaseUrl).href, - ); - const r = await httpResp.json(); - t.assertDeepEqual(httpResp.status, 202); - console.log(r); - } - - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - talerPayUri = r.taler_pay_uri; - t.assertTrue(!!talerPayUri); - } - - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href, { - headers: { - Accept: "text/html", - }, - }); - const r = await httpResp.text(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - } - - const preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri, - }, - ); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - const contractTermsHash = preparePayResp.contractTermsHash; - const proposalId = preparePayResp.proposalId; - - // claimed, unpaid, access with wrong h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const hcWrong = encodeCrock(getRandomBytes(64)); - url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // claimed, unpaid, access with wrong claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const ctWrong = encodeCrock(getRandomBytes(16)); - url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // claimed, unpaid, access with correct claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // claimed, unpaid, access with correct contract terms hash - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // claimed, unpaid, access without credentials - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 202); - } - - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); - - // paid, access without credentials - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 202); - } - - // paid, access with wrong h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const hcWrong = encodeCrock(getRandomBytes(64)); - url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // paid, access with wrong claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const ctWrong = encodeCrock(getRandomBytes(16)); - url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // paid, access with correct h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - } - - // paid, access with correct claim token, JSON - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - const respFulfillmentUrl = r.fulfillment_url; - t.assertDeepEqual(respFulfillmentUrl, "https://example.com/article42"); - } - - // paid, access with correct claim token, HTML - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("token", claimToken); - const httpResp = await httpLib.get(url.href, { - headers: { Accept: "text/html" }, - }); - t.assertDeepEqual(httpResp.status, 200); - } - - const confirmPayRes2 = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - sessionId: sessionId, - }, - ); - - t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done); - - // Create another order with identical fulfillment URL to test the "already paid" flow - const alreadyPaidOrderResp = await MerchantPrivateApi.createOrder( - merchant, - "default", - { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }, - ); - - const apOrderId = alreadyPaidOrderResp.order_id; - const apToken = alreadyPaidOrderResp.token; - t.assertTrue(!!apToken); - - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // Check for already paid session ID, JSON - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - const alreadyPaidOrderId = r.already_paid_order_id; - t.assertDeepEqual(alreadyPaidOrderId, orderId); - } - - // Check for already paid session ID, HTML - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.get(url.href, { - headers: { Accept: "text/html" }, - }); - t.assertDeepEqual(httpResp.status, 302); - const location = httpResp.headers.get("Location"); - console.log("location header:", location); - t.assertDeepEqual(location, "https://example.com/article42"); - } -} - -async function testWithoutClaimToken( - t: GlobalTestState, - c: Context, -): Promise<void> { - const wallet = new WalletCli(t, "withoutct"); - const sessionId = "mysession2"; - const { bank, exchange } = c; - const { merchant, merchantBaseUrl } = c; - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - create_token: false, - }); - - const orderId = orderResp.order_id; - let talerPayUri: string; - - { - const httpResp = await httpLib.get( - new URL(`orders/${orderId}`, merchantBaseUrl).href, - ); - const r = await httpResp.json(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - } - - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - talerPayUri = r.taler_pay_uri; - t.assertTrue(!!talerPayUri); - } - - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href, { - headers: { - Accept: "text/html", - }, - }); - const r = await httpResp.text(); - t.assertDeepEqual(httpResp.status, 402); - console.log(r); - } - - const preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri, - }, - ); - - console.log(preparePayResp); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - const contractTermsHash = preparePayResp.contractTermsHash; - const proposalId = preparePayResp.proposalId; - - // claimed, unpaid, access with wrong h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const hcWrong = encodeCrock(getRandomBytes(64)); - url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // claimed, unpaid, access with wrong claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const ctWrong = encodeCrock(getRandomBytes(16)); - url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // claimed, unpaid, no claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // claimed, unpaid, access with correct contract terms hash - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // claimed, unpaid, access without credentials - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - // No credentials, but the order doesn't require a claim token. - // This effectively means that the order ID is already considered - // enough authentication, at least to check for the basic order status - t.assertDeepEqual(httpResp.status, 402); - } - - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); - - // paid, access without credentials - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - } - - // paid, access with wrong h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const hcWrong = encodeCrock(getRandomBytes(64)); - url.searchParams.set("h_contract", hcWrong); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // paid, access with wrong claim token - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const ctWrong = encodeCrock(getRandomBytes(16)); - url.searchParams.set("token", ctWrong); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 403); - } - - // paid, access with correct h_contract - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - url.searchParams.set("h_contract", contractTermsHash); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - } - - // paid, JSON - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 200); - const respFulfillmentUrl = r.fulfillment_url; - t.assertDeepEqual(respFulfillmentUrl, "https://example.com/article42"); - } - - // paid, HTML - { - const url = new URL(`orders/${orderId}`, merchantBaseUrl); - const httpResp = await httpLib.get(url.href, { - headers: { Accept: "text/html" }, - }); - t.assertDeepEqual(httpResp.status, 200); - } - - const confirmPayRes2 = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - sessionId: sessionId, - }, - ); - - t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done); - - // Create another order with identical fulfillment URL to test the "already paid" flow - const alreadyPaidOrderResp = await MerchantPrivateApi.createOrder( - merchant, - "default", - { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }, - ); - - const apOrderId = alreadyPaidOrderResp.order_id; - const apToken = alreadyPaidOrderResp.token; - t.assertTrue(!!apToken); - - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - } - - // Check for already paid session ID, JSON - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.get(url.href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 402); - const alreadyPaidOrderId = r.already_paid_order_id; - t.assertDeepEqual(alreadyPaidOrderId, orderId); - } - - // Check for already paid session ID, HTML - { - const url = new URL(`orders/${apOrderId}`, merchantBaseUrl); - url.searchParams.set("token", apToken); - url.searchParams.set("session_id", sessionId); - const httpResp = await httpLib.get(url.href, { - headers: { Accept: "text/html" }, - }); - t.assertDeepEqual(httpResp.status, 302); - const location = httpResp.headers.get("Location"); - console.log("location header:", location); - t.assertDeepEqual(location, "https://example.com/article42"); - } -} - -/** - * Checks for the /orders/{id} endpoint of the merchant. - * - * The tests here should exercise all code paths in the executable - * specification of the endpoint. - */ -export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) { - const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment( - t, - ); - - // Base URL for the default instance. - const merchantBaseUrl = merchant.makeInstanceBaseUrl(); - - { - const httpResp = await httpLib.get(new URL("config", merchantBaseUrl).href); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(r.currency, "TESTKUDOS"); - } - - { - const httpResp = await httpLib.get( - new URL("orders/foo", merchantBaseUrl).href, - ); - const r = await httpResp.json(); - console.log(r); - t.assertDeepEqual(httpResp.status, 404); - // FIXME: also check Taler error code - } - - { - const httpResp = await httpLib.get( - new URL("orders/foo", merchantBaseUrl).href, - { - headers: { - Accept: "text/html", - }, - }, - ); - const r = await httpResp.text(); - console.log(r); - t.assertDeepEqual(httpResp.status, 404); - // FIXME: also check Taler error code - } - - await testWithClaimToken(t, { - merchant, - merchantBaseUrl, - exchange, - bank, - }); - - await testWithoutClaimToken(t, { - merchant, - merchantBaseUrl, - exchange, - bank, - }); -} - -runMerchantSpecPublicOrdersTest.suites = ["merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts b/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts deleted file mode 100644 index 0fa9ec81d..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-pay-abort.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Fault injection test to check aborting partial payment - * via refunds. - */ - -/** - * Imports. - */ -import { URL, PreparePayResultType, TalerErrorCode } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { - FaultInjectionRequestContext, - FaultInjectionResponseContext, -} from "../harness/faultInjection"; -import { GlobalTestState, MerchantPrivateApi, setupDb } from "../harness/harness.js"; -import { - createFaultInjectedMerchantTestkudosEnvironment, - withdrawViaBank, -} from "../harness/helpers.js"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runPayAbortTest(t: GlobalTestState) { - const { - bank, - faultyExchange, - wallet, - faultyMerchant, - } = await createFaultInjectedMerchantTestkudosEnvironment(t); - // Set up test environment - - await withdrawViaBank(t, { - wallet, - exchange: faultyExchange, - amount: "TESTKUDOS:20", - bank, - }); - - const orderResp = await MerchantPrivateApi.createOrder( - faultyMerchant, - "default", - { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:15", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }, - ); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - faultyMerchant, - { - orderId: orderResp.order_id, - }, - ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - // We let only the first deposit through! - let firstDepositUrl: string | undefined; - - faultyExchange.faultProxy.addFault({ - async modifyRequest(ctx: FaultInjectionRequestContext) { - const url = new URL(ctx.requestUrl); - if (url.pathname.endsWith("/deposit")) { - if (!firstDepositUrl) { - firstDepositUrl = url.href; - return; - } - if (url.href != firstDepositUrl) { - url.pathname = "/doesntexist"; - ctx.requestUrl = url.href; - } - } - }, - async modifyResponse(ctx: FaultInjectionResponseContext) { - const url = new URL(ctx.request.requestUrl); - if (url.pathname.endsWith("/deposit") && url.href != firstDepositUrl) { - ctx.responseBody = Buffer.from("{}"); - ctx.statusCode = 500; - } - }, - }); - - faultyMerchant.faultProxy.addFault({ - async modifyResponse(ctx: FaultInjectionResponseContext) { - const url = new URL(ctx.request.requestUrl); - if (url.pathname.endsWith("/pay") && url.href != firstDepositUrl) { - ctx.responseBody = Buffer.from("{}"); - ctx.statusCode = 400; - } - }, - }); - - await t.assertThrowsOperationErrorAsync(async () => { - await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - }); - - let txr = await wallet.client.call(WalletApiOperation.GetTransactions, {}); - console.log(JSON.stringify(txr, undefined, 2)); - - t.assertDeepEqual(txr.transactions[1].type, "payment"); - t.assertDeepEqual(txr.transactions[1].pending, true); - t.assertDeepEqual( - txr.transactions[1].error?.code, - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - ); - - await wallet.client.call(WalletApiOperation.AbortFailedPayWithRefund, { - proposalId: preparePayResult.proposalId, - }); - - await wallet.runUntilDone(); - - txr = await wallet.client.call(WalletApiOperation.GetTransactions, {}); - console.log(JSON.stringify(txr, undefined, 2)); - - const txTypes = txr.transactions.map((x) => x.type); - - t.assertDeepEqual(txTypes, ["withdrawal", "payment", "refund"]); -} - -runPayAbortTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts b/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts deleted file mode 100644 index 2d291ddd3..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts +++ /dev/null @@ -1,225 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { - withdrawViaBank, - createFaultInjectedMerchantTestkudosEnvironment, -} from "../harness/helpers.js"; -import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, - ConfirmPayResultType, - URL, -} from "@gnu-taler/taler-util"; -import axios from "axios"; -import { FaultInjectionRequestContext } from "../harness/faultInjection"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for the wallets repurchase detection mechanism - * based on the fulfillment URL. - * - * FIXME: This test is now almost the same as test-paywall-flow, - * since we can't initiate payment via a "claimed" private order status - * response. - */ -export async function runPayPaidTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - faultyExchange, - faultyMerchant, - } = await createFaultInjectedMerchantTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { - wallet, - bank, - exchange: faultyExchange, - amount: "TESTKUDOS:20", - }); - - /** - * ========================================================================= - * Create an order and let the wallet pay under a session ID - * - * We check along the way that the JSON response to /orders/{order_id} - * returns the right thing. - * ========================================================================= - */ - - const merchant = faultyMerchant; - - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-one", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - let publicOrderStatusUrl = orderStatus.order_status_url; - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(pubUnpaidStatus); - - let preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: pubUnpaidStatus.taler_pay_uri, - }, - ); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - const proposalId = preparePayResp.proposalId; - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - console.log(publicOrderStatusResp.data); - - if (publicOrderStatusResp.status != 200) { - console.log(publicOrderStatusResp.data); - throw Error( - `expected status 200 (after paying), but got ${publicOrderStatusResp.status}`, - ); - } - - /** - * ========================================================================= - * Now change up the session ID and do payment re-play! - * ========================================================================= - */ - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-two", - }); - - console.log( - "order status under mysession-two:", - JSON.stringify(orderStatus, undefined, 2), - ); - - // Should be claimed (not paid!) because of a new session ID - t.assertTrue(orderStatus.order_status === "claimed"); - - let numPayRequested = 0; - let numPaidRequested = 0; - - faultyMerchant.faultProxy.addFault({ - async modifyRequest(ctx: FaultInjectionRequestContext) { - const url = new URL(ctx.requestUrl); - if (url.pathname.endsWith("/pay")) { - numPayRequested++; - } else if (url.pathname.endsWith("/paid")) { - numPaidRequested++; - } - }, - }); - - let orderRespTwo = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }); - - let orderStatusTwo = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderRespTwo.order_id, - sessionId: "mysession-two", - }, - ); - - t.assertTrue(orderStatusTwo.order_status === "unpaid"); - - // Pay with new taler://pay URI, which should - // have the new session ID! - // Wallet should now automatically re-play payment. - preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatusTwo.taler_pay_uri, - }, - ); - - t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); - t.assertTrue(preparePayResp.paid); - - // Make sure the wallet is actually doing the replay properly. - t.assertTrue(numPaidRequested == 1); - t.assertTrue(numPayRequested == 0); -} - -runPayPaidTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts deleted file mode 100644 index ba3bd8e0a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi, WalletCli } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; -import { PreparePayResultType } from "@gnu-taler/taler-util"; -import { TalerErrorCode } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runPaymentClaimTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - const walletTwo = new WalletCli(t, "two"); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUri = orderStatus.taler_pay_uri; - - // Make wallet pay for the order - - const preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - t.assertThrowsOperationErrorAsync(async () => { - await walletTwo.client.call(WalletApiOperation.PreparePayForUri, { - talerPayUri, - }); - }); - - await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - walletTwo.deleteDatabase(); - - const err = await t.assertThrowsOperationErrorAsync(async () => { - await walletTwo.client.call(WalletApiOperation.PreparePayForUri, { - talerPayUri, - }); - }); - - t.assertTrue( - err.operationError.code === TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED, - ); - - await t.shutdown(); -} - -runPaymentClaimTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts deleted file mode 100644 index 2be01d919..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts +++ /dev/null @@ -1,217 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Sample fault injection test. - */ - -/** - * Imports. - */ -import { - GlobalTestState, - MerchantService, - ExchangeService, - setupDb, - BankService, - WalletCli, - MerchantPrivateApi, - BankApi, - BankAccessApi, -} from "../harness/harness.js"; -import { - FaultInjectedExchangeService, - FaultInjectionRequestContext, - FaultInjectionResponseContext, -} from "../harness/faultInjection"; -import { CoreApiResponse } from "@gnu-taler/taler-util"; -import { defaultCoinConfig } from "../harness/denomStructures"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runPaymentFaultTest(t: GlobalTestState) { - // Set up test environment - - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - await exchange.addBankAccount("1", exchangeBankAccount); - exchange.addOfferedCoins(defaultCoinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091); - - // Print all requests to the exchange - faultyExchange.faultProxy.addFault({ - async modifyRequest(ctx: FaultInjectionRequestContext) { - console.log("got request", ctx); - }, - async modifyResponse(ctx: FaultInjectionResponseContext) { - console.log("got response", ctx); - }, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - merchant.addExchange(faultyExchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - // Create withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, - "TESTKUDOS:20", - ); - - // Hand it to the wallet - - await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - - await wallet.runPending(); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bank, user, wop); - - // Withdraw - - await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { - exchangeBaseUrl: faultyExchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }); - await wallet.runUntilDone(); - - // Check balance - - await wallet.client.call(WalletApiOperation.GetBalances, {}); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - let apiResp: CoreApiResponse; - - const prepResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - const proposalId = prepResp.proposalId; - - await wallet.runPending(); - - // Drop 3 responses from the exchange. - let faultCount = 0; - faultyExchange.faultProxy.addFault({ - async modifyResponse(ctx: FaultInjectionResponseContext) { - if (!ctx.request.requestUrl.endsWith("/deposit")) { - return; - } - if (faultCount < 3) { - console.log(`blocking /deposit request #${faultCount}`); - faultCount++; - ctx.dropResponse = true; - } else { - console.log(`letting through /deposit request #${faultCount}`); - } - }, - }); - - // confirmPay won't work, as the exchange is unreachable - - await wallet.client.call(WalletApiOperation.ConfirmPay, { - // FIXME: should be validated, don't cast! - proposalId: proposalId, - }); - - await wallet.runUntilDone(); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); -} - -runPaymentFaultTest.suites = ["wallet"]; -runPaymentFaultTest.timeoutMs = 120000; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts deleted file mode 100644 index 3bdd6bef3..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - makeTestPayment, -} from "../harness/helpers.js"; - -/** - * Run test for payment with a contract that has forgettable fields. - */ -export async function runPaymentForgettableTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - { - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - extra: { - foo: { bar: "baz" }, - $forgettable: { - foo: "gnu", - }, - }, - }; - - await makeTestPayment(t, { wallet, merchant, order }); - } - - console.log("testing with forgettable field without hash"); - - { - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - extra: { - foo: { bar: "baz" }, - $forgettable: { - foo: true, - }, - }, - }; - - await makeTestPayment(t, { wallet, merchant, order }); - } - - await wallet.runUntilDone(); -} - -runPaymentForgettableTest.suites = ["wallet", "merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts deleted file mode 100644 index 9378465a0..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; -import { PreparePayResultType } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Test the wallet-core payment API, especially that repeated operations - * return the expected result. - */ -export async function runPaymentIdempotencyTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUri = orderStatus.taler_pay_uri; - - // Make wallet pay for the order - - const preparePayResult = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - const preparePayResultRep = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - t.assertTrue( - preparePayResultRep.status === PreparePayResultType.PaymentPossible, - ); - - const proposalId = preparePayResult.proposalId; - - await wallet.client.call(WalletApiOperation.ConfirmPay, { - // FIXME: should be validated, don't cast! - proposalId: proposalId, - }); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - const preparePayResultAfter = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri, - }, - ); - - t.assertTrue( - preparePayResultAfter.status === PreparePayResultType.AlreadyConfirmed, - ); - t.assertTrue(preparePayResultAfter.paid === true); - - await t.shutdown(); -} - -runPaymentIdempotencyTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts deleted file mode 100644 index 754c3a0e8..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - GlobalTestState, - setupDb, - BankService, - ExchangeService, - MerchantService, - WalletCli, - MerchantPrivateApi, -} from "../harness/harness.js"; -import { withdrawViaBank } from "../harness/helpers.js"; -import { coin_ct10, coin_u1 } from "../harness/denomStructures"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -async function setupTest( - t: GlobalTestState, -): Promise<{ - merchant: MerchantService; - exchange: ExchangeService; - bank: BankService; -}> { - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - - exchange.addOfferedCoins([coin_ct10, coin_u1]); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - await exchange.addBankAccount("1", exchangeBankAccount); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - return { - merchant, - bank, - exchange, - }; -} - -/** - * Run test. - * - * This test uses a very sub-optimal denomination structure. - */ -export async function runPaymentMultipleTest(t: GlobalTestState) { - // Set up test environment - - const { merchant, bank, exchange } = await setupTest(t); - - const wallet = new WalletCli(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:100" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:80", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, { - talerPayUri: orderStatus.taler_pay_uri, - }); - - await wallet.client.call(WalletApiOperation.ConfirmPay, { - // FIXME: should be validated, don't cast! - proposalId: r1.proposalId, - }); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - await t.shutdown(); -} - -runPaymentMultipleTest.suites = ["wallet"]; -runPaymentMultipleTest.timeoutMs = 120000; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts deleted file mode 100644 index 1d419fd9a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - GlobalTestState, - BankApi, - WalletCli, - BankAccessApi -} from "../harness/harness.js"; -import { - makeTestPayment, -} from "../harness/helpers.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal and payment. - */ -export async function runPaymentDemoTest(t: GlobalTestState) { - - // Withdraw digital cash into the wallet. - let bankInterface = { - baseUrl: "https://bank.demo.taler.net/", - port: 0 // unused. - }; - let user = await BankApi.createRandomBankUser(bankInterface); - let wop = await BankAccessApi.createWithdrawalOperation(bankInterface, user, "KUDOS:20"); - - let wallet = new WalletCli(t); - await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - - await wallet.runPending(); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bankInterface, user, wop); - - // Withdraw - - await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { - exchangeBaseUrl: "https://exchange.demo.taler.net/", - talerWithdrawUri: wop.taler_withdraw_uri, - }); - await wallet.runUntilDone(); - - let balanceBefore = await wallet.client.call(WalletApiOperation.GetBalances, {}); - t.assertTrue(balanceBefore["balances"].length == 1); - - const order = { - summary: "Buy me!", - amount: "KUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }; - - let merchant = { - makeInstanceBaseUrl: function(instanceName?: string) { - return "https://backend.demo.taler.net/instances/donations/"; - }, - port: 0, - name: "donations", - }; - - t.assertTrue("TALER_ENV_FRONTENDS_APITOKEN" in process.env); - - await makeTestPayment( - t, - { - merchant, wallet, order - }, - { - "Authorization": `Bearer ${process.env["TALER_ENV_FRONTENDS_APITOKEN"]}`, - }); - - await wallet.runUntilDone(); - - let balanceAfter = await wallet.client.call(WalletApiOperation.GetBalances, {}); - t.assertTrue(balanceAfter["balances"].length == 1); - t.assertTrue(balanceBefore["balances"][0]["available"] > balanceAfter["balances"][0]["available"]); -} - -runPaymentDemoTest.excludeByDefault = true; -runPaymentDemoTest.suites = ["buildbot"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts deleted file mode 100644 index 75d44d495..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts +++ /dev/null @@ -1,187 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { - withdrawViaBank, - createFaultInjectedMerchantTestkudosEnvironment, -} from "../harness/helpers.js"; -import axios from "axios"; -import { - FaultInjectionRequestContext, - FaultInjectionResponseContext, -} from "../harness/faultInjection"; -import { - codecForMerchantOrderStatusUnpaid, - ConfirmPayResultType, - PreparePayResultType, - TalerErrorCode, - TalerErrorDetails, - URL, -} from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for a payment where the merchant has a transient - * failure in /pay - */ -export async function runPaymentTransientTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - faultyMerchant, - } = await createFaultInjectedMerchantTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - const merchant = faultyMerchant; - - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-one", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - let publicOrderStatusUrl = orderStatus.order_status_url; - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - - - throw Error( - `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(pubUnpaidStatus); - - let preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: pubUnpaidStatus.taler_pay_uri, - }, - ); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - const proposalId = preparePayResp.proposalId; - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - let faultInjected = false; - - faultyMerchant.faultProxy.addFault({ - async modifyResponse(ctx: FaultInjectionResponseContext) { - console.log("in modifyResponse"); - const url = new URL(ctx.request.requestUrl); - console.log("pathname is", url.pathname); - if (!url.pathname.endsWith("/pay")) { - return; - } - if (faultInjected) { - console.log("not injecting pay fault"); - return; - } - faultInjected = true; - console.log("injecting pay fault"); - const err: TalerErrorDetails = { - code: TalerErrorCode.GENERIC_DB_COMMIT_FAILED, - details: {}, - hint: "huh", - message: "something went wrong", - }; - ctx.responseBody = Buffer.from(JSON.stringify(err)); - ctx.statusCode = 500; - }, - }); - - const confirmPayResp = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId, - }, - ); - - console.log(confirmPayResp); - - t.assertTrue(confirmPayResp.type === ConfirmPayResultType.Pending); - t.assertTrue(faultInjected); - - const confirmPayRespTwo = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId, - }, - ); - - t.assertTrue(confirmPayRespTwo.type === ConfirmPayResultType.Done); - - // Now ask the merchant if paid - - console.log("requesting", publicOrderStatusUrl); - publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { - validateStatus: () => true, - }); - - console.log(publicOrderStatusResp.data); - - if (publicOrderStatusResp.status != 200) { - console.log(publicOrderStatusResp.data); - throw Error( - `expected status 200 (after paying), but got ${publicOrderStatusResp.status}`, - ); - } -} - -runPaymentTransientTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts deleted file mode 100644 index c38b8b382..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState } from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - makeTestPayment, -} from "../harness/helpers.js"; - -/** - * Run test for a payment for a "free" order with - * an amount of zero. - */ -export async function runPaymentZeroTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // First, make a "free" payment when we don't even have - // any money in the - - // Withdraw digital cash into the wallet. - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - await wallet.runUntilDone(); - - await makeTestPayment(t, { - wallet, - merchant, - order: { - summary: "I am free!", - amount: "TESTKUDOS:0", - fulfillment_url: "taler://fulfillment-success/thx", - }, - }); - - await wallet.runUntilDone(); - - const transactions = await wallet.client.call( - WalletApiOperation.GetTransactions, - {}, - ); - - for (const tr of transactions.transactions) { - t.assertDeepEqual(tr.pending, false); - } -} - -runPaymentZeroTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-payment.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment.ts deleted file mode 100644 index 967d491be..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - makeTestPayment, -} from "../harness/helpers.js"; - -/** - * Run test for basic, bank-integrated withdrawal and payment. - */ -export async function runPaymentTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }; - - await makeTestPayment(t, { wallet, merchant, order }); - - await wallet.runUntilDone(); -} - -runPaymentTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts b/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts deleted file mode 100644 index a8e3b3e95..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts +++ /dev/null @@ -1,252 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; -import { - PreparePayResultType, - codecForMerchantOrderStatusUnpaid, - ConfirmPayResultType, - URL, -} from "@gnu-taler/taler-util"; -import axios from "axios"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runPaywallFlowTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - /** - * ========================================================================= - * Create an order and let the wallet pay under a session ID - * - * We check along the way that the JSON response to /orders/{order_id} - * returns the right thing. - * ========================================================================= - */ - - let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }); - - const firstOrderId = orderResp.order_id; - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-one", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const talerPayUriOne = orderStatus.taler_pay_uri; - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - let publicOrderStatusUrl = new URL(orderStatus.order_status_url); - - let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(pubUnpaidStatus); - - let preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: pubUnpaidStatus.taler_pay_uri, - }, - ); - - t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible); - - const proposalId = preparePayResp.proposalId; - - console.log("requesting", publicOrderStatusUrl.href); - publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - console.log("response body", publicOrderStatusResp.data); - if (publicOrderStatusResp.status != 402) { - throw Error( - `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`, - ); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - const confirmPayRes = await wallet.client.call( - WalletApiOperation.ConfirmPay, - { - proposalId: proposalId, - }, - ); - - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); - - publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - - console.log(publicOrderStatusResp.data); - - if (publicOrderStatusResp.status != 200) { - console.log(publicOrderStatusResp.data); - throw Error( - `expected status 200 (after paying), but got ${publicOrderStatusResp.status}`, - ); - } - - /** - * ========================================================================= - * Now change up the session ID! - * ========================================================================= - */ - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - sessionId: "mysession-two", - }); - - // Should be claimed (not paid!) because of a new session ID - t.assertTrue(orderStatus.order_status === "claimed"); - - // Pay with new taler://pay URI, which should - // have the new session ID! - // Wallet should now automatically re-play payment. - preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: talerPayUriOne, - }, - ); - - t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); - t.assertTrue(preparePayResp.paid); - - /** - * ========================================================================= - * Now we test re-purchase detection. - * ========================================================================= - */ - - orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - // Same fulfillment URL as previously! - fulfillment_url: "https://example.com/article42", - public_reorder_url: "https://example.com/article42-share", - }, - }); - - const secondOrderId = orderResp.order_id; - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: secondOrderId, - sessionId: "mysession-three", - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - t.assertTrue(orderStatus.already_paid_order_id === undefined); - publicOrderStatusUrl = new URL(orderStatus.order_status_url); - - // Here the re-purchase detection should kick in, - // and the wallet should re-pay for the old order - // under the new session ID (mysession-three). - preparePayResp = await wallet.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); - t.assertTrue(preparePayResp.paid); - - // The first order should now be paid under "mysession-three", - // as the wallet did re-purchase detection - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: firstOrderId, - sessionId: "mysession-three", - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - // Check that with a completely new session ID, the status would NOT - // be paid. - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: firstOrderId, - sessionId: "mysession-four", - }); - - t.assertTrue(orderStatus.order_status === "claimed"); - - // Now check if the public status of the new order is correct. - - console.log("requesting public status", publicOrderStatusUrl); - - // Ask the order status of the claimed-but-unpaid order - publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, { - validateStatus: () => true, - }); - - if (publicOrderStatusResp.status != 402) { - throw Error(`expected status 402, but got ${publicOrderStatusResp.status}`); - } - - pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( - publicOrderStatusResp.data, - ); - - console.log(publicOrderStatusResp.data); - - t.assertTrue(pubUnpaidStatus.already_paid_order_id === firstOrderId); -} - -runPaywallFlowTest.suites = ["merchant", "wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts deleted file mode 100644 index 230fc942d..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; -import { durationFromSpec } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runRefundAutoTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - auto_refund: { - d_ms: 3000, - }, - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, { - talerPayUri: orderStatus.taler_pay_uri, - }); - - await wallet.client.call(WalletApiOperation.ConfirmPay, { - // FIXME: should be validated, don't cast! - proposalId: r1.proposalId, - }); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - const ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - console.log(ref); - - // The wallet should now automatically pick up the refund. - await wallet.runUntilDone(); - - const transactions = await wallet.client.call( - WalletApiOperation.GetTransactions, - {}, - ); - console.log(JSON.stringify(transactions, undefined, 2)); - - const transactionTypes = transactions.transactions.map((x) => x.type); - t.assertDeepEqual(transactionTypes, ["withdrawal", "payment", "refund"]); - - await t.shutdown(); -} - -runRefundAutoTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts deleted file mode 100644 index acb74b3d3..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - applyTimeTravel, -} from "../harness/helpers.js"; -import { - durationFromSpec, - timestampAddDuration, - getTimestampNow, - timestampTruncateToSecond, -} from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runRefundGoneTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - pay_deadline: timestampTruncateToSecond( - timestampAddDuration( - getTimestampNow(), - durationFromSpec({ - minutes: 10, - }), - ), - ), - }, - refund_delay: durationFromSpec({ minutes: 1 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, { - talerPayUri: orderStatus.taler_pay_uri, - }); - - const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: r1.proposalId, - }); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - console.log(orderStatus); - - await applyTimeTravel(durationFromSpec({ hours: 1 }), { exchange, wallet }); - - await exchange.runAggregatorOnce(); - - const ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - console.log(ref); - - let rr = await wallet.client.call(WalletApiOperation.ApplyRefund, { - talerRefundUri: ref.talerRefundUri, - }); - - t.assertAmountEquals(rr.amountRefundGone, "TESTKUDOS:5"); - console.log(rr); - - await wallet.runUntilDone(); - - let r = await wallet.client.call(WalletApiOperation.GetBalances, {}); - console.log(JSON.stringify(r, undefined, 2)); - - const r3 = await wallet.client.call(WalletApiOperation.GetTransactions, {}); - console.log(JSON.stringify(r3, undefined, 2)); - - await t.shutdown(); -} - -runRefundGoneTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts deleted file mode 100644 index 47c2293e2..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts +++ /dev/null @@ -1,196 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, delayMs, MerchantPrivateApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; -import { - TransactionType, - Amounts, - durationFromSpec, -} from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runRefundIncrementalTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:10", - fulfillment_url: "taler://fulfillment-success/thx", - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, { - talerPayUri: orderStatus.taler_pay_uri, - }); - - await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: r1.proposalId, - }); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - let ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:2.5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - console.log("first refund increase response", ref); - - { - let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, { - talerRefundUri: ref.talerRefundUri, - }); - console.log(wr); - const txs = await wallet.client.call( - WalletApiOperation.GetTransactions, - {}, - ); - console.log( - "transactions after applying first refund:", - JSON.stringify(txs, undefined, 2), - ); - } - - // Wait at least a second, because otherwise the increased - // refund will be grouped with the previous one. - await delayMs(1200); - - ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "bar", - orderId: orderResp.order_id, - }); - - console.log("second refund increase response", ref); - - // Wait at least a second, because otherwise the increased - // refund will be grouped with the previous one. - await delayMs(1200); - - ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:10", - instance: "default", - justification: "bar", - orderId: orderResp.order_id, - }); - - console.log("third refund increase response", ref); - - { - let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, { - talerRefundUri: ref.talerRefundUri, - }); - console.log(wr); - } - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:10"); - - console.log(JSON.stringify(orderStatus, undefined, 2)); - - await wallet.runUntilDone(); - - const bal = await wallet.client.call(WalletApiOperation.GetBalances, {}); - console.log(JSON.stringify(bal, undefined, 2)); - - { - const txs = await wallet.client.call( - WalletApiOperation.GetTransactions, - {}, - ); - console.log(JSON.stringify(txs, undefined, 2)); - - const txTypes = txs.transactions.map((x) => x.type); - t.assertDeepEqual(txTypes, [ - "withdrawal", - "payment", - "refund", - "refund", - "refund", - ]); - - for (const tx of txs.transactions) { - if (tx.type !== TransactionType.Refund) { - continue; - } - t.assertAmountLeq(tx.amountEffective, tx.amountRaw); - } - - const raw = Amounts.sum( - txs.transactions - .filter((x) => x.type === TransactionType.Refund) - .map((x) => x.amountRaw), - ).amount; - - t.assertAmountEquals("TESTKUDOS:10", raw); - - const effective = Amounts.sum( - txs.transactions - .filter((x) => x.type === TransactionType.Refund) - .map((x) => x.amountEffective), - ).amount; - - t.assertAmountEquals("TESTKUDOS:8.33", effective); - } - - await t.shutdown(); -} - -runRefundIncrementalTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-refund.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund.ts deleted file mode 100644 index f11771922..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { durationFromSpec } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runRefundTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); - - // Set up order. - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - summary: "Buy me!", - amount: "TESTKUDOS:5", - fulfillment_url: "taler://fulfillment-success/thx", - }, - refund_delay: durationFromSpec({ minutes: 5 }), - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, { - talerPayUri: orderStatus.taler_pay_uri, - }); - - await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: r1.proposalId, - }); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, { - orderId: orderResp.order_id, - }); - - t.assertTrue(orderStatus.order_status === "paid"); - - const ref = await MerchantPrivateApi.giveRefund(merchant, { - amount: "TESTKUDOS:5", - instance: "default", - justification: "foo", - orderId: orderResp.order_id, - }); - - console.log(ref); - - let r = await wallet.client.call(WalletApiOperation.ApplyRefund, { - talerRefundUri: ref.talerRefundUri, - }); - console.log(r); - - await wallet.runUntilDone(); - - { - const r2 = await wallet.client.call(WalletApiOperation.GetBalances, {}); - console.log(JSON.stringify(r2, undefined, 2)); - } - { - const r2 = await wallet.client.call(WalletApiOperation.GetTransactions, {}); - console.log(JSON.stringify(r2, undefined, 2)); - } - - await t.shutdown(); -} - -runRefundTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts b/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts deleted file mode 100644 index 276c532b5..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts +++ /dev/null @@ -1,212 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { CoinConfig } from "../harness/denomStructures"; -import { - GlobalTestState, - ExchangeService, - MerchantService, - WalletCli, - setupDb, - BankService, - delayMs, -} from "../harness/harness.js"; -import { - withdrawViaBank, - makeTestPayment, - SimpleTestEnvironment, -} from "../harness/helpers.js"; - -async function revokeAllWalletCoins(req: { - wallet: WalletCli; - exchange: ExchangeService; - merchant: MerchantService; -}): Promise<void> { - const { wallet, exchange, merchant } = req; - const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {}); - console.log(coinDump); - const usedDenomHashes = new Set<string>(); - for (const coin of coinDump.coins) { - usedDenomHashes.add(coin.denom_pub_hash); - } - for (const x of usedDenomHashes.values()) { - await exchange.revokeDenomination(x); - } - await delayMs(1000); - await exchange.keyup(); - await delayMs(1000); - await merchant.stop(); - await merchant.start(); - await merchant.pingUntilAvailable(); -} - -async function createTestEnvironment( - t: GlobalTestState, -): Promise<SimpleTestEnvironment> { - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - const coin_u1: CoinConfig = { - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, - name: `TESTKUDOS_u1`, - value: `TESTKUDOS:1`, - feeDeposit: `TESTKUDOS:0`, - feeRefresh: `TESTKUDOS:0`, - feeRefund: `TESTKUDOS:0`, - feeWithdraw: `TESTKUDOS:0`, - }; - - exchange.addCoinConfigList([coin_u1]); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - }; -} - -/** - * Basic time travel test. - */ -export async function runRevocationTest(t: GlobalTestState) { - // Set up test environment - - const { wallet, bank, exchange, merchant } = await createTestEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - - console.log("revoking first time"); - await revokeAllWalletCoins({ wallet, exchange, merchant }); - - // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565 - // is implemented. - await wallet.client.call(WalletApiOperation.AddExchange, { - exchangeBaseUrl: exchange.baseUrl, - forceUpdate: true, - }); - await wallet.runUntilDone(); - await wallet.runUntilDone(); - const bal = await wallet.client.call(WalletApiOperation.GetBalances, {}); - console.log("wallet balance", bal); - - const order = { - summary: "Buy me!", - amount: "TESTKUDOS:10", - fulfillment_url: "taler://fulfillment-success/thx", - }; - - await makeTestPayment(t, { wallet, merchant, order }); - - wallet.deleteDatabase(); - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - - const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {}); - console.log(coinDump); - const coinPubList = coinDump.coins.map((x) => x.coin_pub); - await wallet.client.call(WalletApiOperation.ForceRefresh, { - coinPubList, - }); - await wallet.runUntilDone(); - - console.log("revoking second time"); - await revokeAllWalletCoins({ wallet, exchange, merchant }); - - // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565 - // is implemented. - await wallet.client.call(WalletApiOperation.AddExchange, { - exchangeBaseUrl: exchange.baseUrl, - forceUpdate: true, - }); - await wallet.runUntilDone(); - await wallet.runUntilDone(); - { - const bal = await wallet.client.call(WalletApiOperation.GetBalances, {}); - console.log("wallet balance", bal); - } - - await makeTestPayment(t, { wallet, merchant, order }); -} - -runRevocationTest.timeoutMs = 120000; -runRevocationTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts deleted file mode 100644 index e20d8bdad..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts +++ /dev/null @@ -1,215 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - ConfirmPayResultType, - Duration, - durationFromSpec, - PreparePayResultType, -} from "@gnu-taler/taler-util"; -import { - PendingOperationsResponse, - WalletApiOperation, -} from "@gnu-taler/taler-wallet-core"; -import { makeNoFeeCoinConfig } from "../harness/denomStructures"; -import { - BankService, - ExchangeService, - GlobalTestState, - MerchantPrivateApi, - MerchantService, - setupDb, - WalletCli, -} from "../harness/harness.js"; -import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js"; - -async function applyTimeTravel( - timetravelDuration: Duration, - s: { - exchange?: ExchangeService; - merchant?: MerchantService; - wallet?: WalletCli; - }, -): Promise<void> { - if (s.exchange) { - await s.exchange.stop(); - s.exchange.setTimetravel(timetravelDuration); - await s.exchange.start(); - await s.exchange.pingUntilAvailable(); - } - - if (s.merchant) { - await s.merchant.stop(); - s.merchant.setTimetravel(timetravelDuration); - await s.merchant.start(); - await s.merchant.pingUntilAvailable(); - } - - if (s.wallet) { - console.log("setting wallet time travel to", timetravelDuration); - s.wallet.setTimetravel(timetravelDuration); - } -} - -/** - * Basic time travel test. - */ -export async function runTimetravelAutorefreshTest(t: GlobalTestState) { - // Set up test environment - - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addCoinConfigList(makeNoFeeCoinConfig("TESTKUDOS")); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - await merchant.start(); - await merchant.pingUntilAvailable(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [`payto://x-taler-bank/merchant-default`], - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: ["payto://x-taler-bank/minst1"], - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - - // Travel into the future, the deposit expiration is two years - // into the future. - console.log("applying first time travel"); - await applyTimeTravel(durationFromSpec({ days: 400 }), { - wallet, - exchange, - merchant, - }); - - await wallet.runUntilDone(); - - let p: PendingOperationsResponse; - p = await wallet.client.call(WalletApiOperation.GetPendingOperations, {}); - - console.log("pending operations after first time travel"); - console.log(JSON.stringify(p, undefined, 2)); - - await startWithdrawViaBank(t, { - wallet, - bank, - exchange, - amount: "TESTKUDOS:20", - }); - - await wallet.runUntilDone(); - - // Travel into the future, the deposit expiration is two years - // into the future. - console.log("applying second time travel"); - await applyTimeTravel(durationFromSpec({ years: 2, months: 6 }), { - wallet, - exchange, - merchant, - }); - - // At this point, the original coins should've been refreshed. - // It would be too late to refresh them now, as we're past - // the two year deposit expiration. - - await wallet.runUntilDone(); - - const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", { - order: { - fulfillment_url: "http://example.com", - summary: "foo", - amount: "TESTKUDOS:30", - }, - }); - - const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - instance: "default", - }, - ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - const r = await wallet.client.call(WalletApiOperation.PreparePayForUri, { - talerPayUri: orderStatus.taler_pay_uri, - }); - - console.log(r); - - t.assertTrue(r.status === PreparePayResultType.PaymentPossible); - - const cpr = await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: r.proposalId, - }); - - t.assertTrue(cpr.type === ConfirmPayResultType.Done); -} - -runTimetravelAutorefreshTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts deleted file mode 100644 index 2ff857057..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState } from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironment, - withdrawViaBank, - startWithdrawViaBank, -} from "../harness/helpers.js"; -import { Duration, TransactionType } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Basic time travel test. - */ -export async function runTimetravelWithdrawTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - } = await createSimpleTestkudosEnvironment(t); - - // Withdraw digital cash into the wallet. - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" }); - - // Travel 400 days into the future, - // as the deposit expiration is two years - // into the future. - const timetravelDuration: Duration = { - d_ms: 1000 * 60 * 60 * 24 * 400, - }; - - await exchange.stop(); - exchange.setTimetravel(timetravelDuration); - await exchange.start(); - await exchange.pingUntilAvailable(); - await exchange.keyup(); - - await merchant.stop(); - merchant.setTimetravel(timetravelDuration); - await merchant.start(); - await merchant.pingUntilAvailable(); - - // This should fail, as the wallet didn't time travel yet. - await startWithdrawViaBank(t, { - wallet, - bank, - exchange, - amount: "TESTKUDOS:20", - }); - - // Check that transactions are correct for the failed withdrawal - { - await wallet.runUntilDone({ maxRetries: 5 }); - const transactions = await wallet.client.call( - WalletApiOperation.GetTransactions, - {}, - ); - console.log(transactions); - const types = transactions.transactions.map((x) => x.type); - t.assertDeepEqual(types, ["withdrawal", "withdrawal"]); - const wtrans = transactions.transactions[1]; - t.assertTrue(wtrans.type === TransactionType.Withdrawal); - t.assertTrue(wtrans.pending); - } - - // Now we also let the wallet time travel - - wallet.setTimetravel(timetravelDuration); - - // This doesn't work yet, see https://bugs.taler.net/n/6585 - - // await wallet.runUntilDone({ maxRetries: 5 }); -} - -runTimetravelWithdrawTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts deleted file mode 100644 index c6a7f8402..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts +++ /dev/null @@ -1,130 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, MerchantPrivateApi, BankApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runTippingTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - merchant, - exchangeBankAccount, - } = await createSimpleTestkudosEnvironment(t); - - const mbu = await BankApi.createRandomBankUser(bank); - - const tipReserveResp = await MerchantPrivateApi.createTippingReserve( - merchant, - "default", - { - exchange_url: exchange.baseUrl, - initial_balance: "TESTKUDOS:10", - wire_method: "x-taler-bank", - }, - ); - - console.log("tipReserveResp:", tipReserveResp); - - t.assertDeepEqual( - tipReserveResp.payto_uri, - exchangeBankAccount.accountPaytoUri, - ); - - await BankApi.adminAddIncoming(bank, { - amount: "TESTKUDOS:10", - debitAccountPayto: mbu.accountPaytoUri, - exchangeBankAccount, - reservePub: tipReserveResp.reserve_pub, - }); - - await exchange.runWirewatchOnce(); - - await merchant.stop(); - await merchant.start(); - await merchant.pingUntilAvailable(); - - const r = await MerchantPrivateApi.queryTippingReserves(merchant, "default"); - console.log("tipping reserves:", JSON.stringify(r, undefined, 2)); - - t.assertTrue(r.reserves.length === 1); - t.assertDeepEqual( - r.reserves[0].exchange_initial_amount, - r.reserves[0].merchant_initial_amount, - ); - - const tip = await MerchantPrivateApi.giveTip(merchant, "default", { - amount: "TESTKUDOS:5", - justification: "why not?", - next_url: "https://example.com/after-tip", - }); - - console.log("created tip", tip); - - const doTip = async (): Promise<void> => { - const ptr = await wallet.client.call(WalletApiOperation.PrepareTip, { - talerTipUri: tip.taler_tip_uri, - }); - - console.log(ptr); - - t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5"); - t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85"); - - await wallet.client.call(WalletApiOperation.AcceptTip, { - walletTipId: ptr.walletTipId, - }); - - await wallet.runUntilDone(); - - const bal = await wallet.client.call(WalletApiOperation.GetBalances, {}); - - console.log(bal); - - t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:4.85"); - - const txns = await wallet.client.call( - WalletApiOperation.GetTransactions, - {}, - ); - - console.log("Transactions:", JSON.stringify(txns, undefined, 2)); - - t.assertDeepEqual(txns.transactions[0].type, "tip"); - t.assertDeepEqual(txns.transactions[0].pending, false); - t.assertAmountEquals( - txns.transactions[0].amountEffective, - "TESTKUDOS:4.85", - ); - t.assertAmountEquals(txns.transactions[0].amountRaw, "TESTKUDOS:5.0"); - }; - - // Check twice so make sure tip handling is idempotent - await doTip(); - await doTip(); -} - -runTippingTest.suites = ["wallet", "wallet-tipping"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts deleted file mode 100644 index 23e01e5e1..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts +++ /dev/null @@ -1,151 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, WalletCli } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; -import { SyncService } from "../harness/sync"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runWalletBackupBasicTest(t: GlobalTestState) { - // Set up test environment - - const { - commonDb, - merchant, - wallet, - bank, - exchange, - } = await createSimpleTestkudosEnvironment(t); - - const sync = await SyncService.create(t, { - currency: "TESTKUDOS", - annualFee: "TESTKUDOS:0.5", - database: commonDb.connStr, - fulfillmentUrl: "taler://fulfillment-success", - httpPort: 8089, - name: "sync1", - paymentBackendUrl: merchant.makeInstanceBaseUrl(), - uploadLimitMb: 10, - }); - - await sync.start(); - await sync.pingUntilAvailable(); - - await wallet.client.call(WalletApiOperation.AddBackupProvider, { - backupProviderBaseUrl: sync.baseUrl, - activate: false, - name: sync.baseUrl, - }); - - { - const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {}); - t.assertDeepEqual(bi.providers[0].active, false); - } - - await wallet.client.call(WalletApiOperation.AddBackupProvider, { - backupProviderBaseUrl: sync.baseUrl, - activate: true, - name: sync.baseUrl, - }); - - { - const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {}); - t.assertDeepEqual(bi.providers[0].active, true); - } - - await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); - - { - const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {}); - console.log(bi); - t.assertDeepEqual( - bi.providers[0].paymentStatus.type, - "insufficient-balance", - ); - } - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:10" }); - - await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); - - { - const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {}); - console.log(bi); - } - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:5" }); - - await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); - - { - const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {}); - console.log(bi); - } - - const backupRecovery = await wallet.client.call( - WalletApiOperation.ExportBackupRecovery, - {}, - ); - - const wallet2 = new WalletCli(t, "wallet2"); - - // Check that the second wallet is a fresh wallet. - { - const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {}); - t.assertTrue(bal.balances.length === 0); - } - - await wallet2.client.call(WalletApiOperation.ImportBackupRecovery, { - recovery: backupRecovery, - }); - - await wallet2.client.call(WalletApiOperation.RunBackupCycle, {}); - - // Check that now the old balance is available! - { - const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {}); - t.assertTrue(bal.balances.length === 1); - console.log(bal); - } - - // Now do some basic checks that the restored wallet is still functional - { - const bal1 = await wallet2.client.call(WalletApiOperation.GetBalances, {}); - - t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:14.1"); - - await withdrawViaBank(t, { - wallet: wallet2, - bank, - exchange, - amount: "TESTKUDOS:10", - }); - - await wallet2.runUntilDone(); - - const bal2 = await wallet2.client.call(WalletApiOperation.GetBalances, {}); - - t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:23.82"); - } -} - -runWalletBackupBasicTest.suites = ["wallet", "wallet-backup"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts deleted file mode 100644 index 8c20dcc2b..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { PreparePayResultType } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, WalletCli, MerchantPrivateApi } from "../harness/harness.js"; -import { - createSimpleTestkudosEnvironment, - makeTestPayment, - withdrawViaBank, -} from "../harness/helpers.js"; -import { SyncService } from "../harness/sync"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runWalletBackupDoublespendTest(t: GlobalTestState) { - // Set up test environment - - const { - commonDb, - merchant, - wallet, - bank, - exchange, - } = await createSimpleTestkudosEnvironment(t); - - const sync = await SyncService.create(t, { - currency: "TESTKUDOS", - annualFee: "TESTKUDOS:0.5", - database: commonDb.connStr, - fulfillmentUrl: "taler://fulfillment-success", - httpPort: 8089, - name: "sync1", - paymentBackendUrl: merchant.makeInstanceBaseUrl(), - uploadLimitMb: 10, - }); - - await sync.start(); - await sync.pingUntilAvailable(); - - await wallet.client.call(WalletApiOperation.AddBackupProvider, { - backupProviderBaseUrl: sync.baseUrl, - activate: true, - name: sync.baseUrl, - }); - - await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:10" }); - - await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); - await wallet.runUntilDone(); - await wallet.client.call(WalletApiOperation.RunBackupCycle, {}); - - const backupRecovery = await wallet.client.call( - WalletApiOperation.ExportBackupRecovery, - {}, - ); - - const wallet2 = new WalletCli(t, "wallet2"); - - await wallet2.client.call(WalletApiOperation.ImportBackupRecovery, { - recovery: backupRecovery, - }); - - await wallet2.client.call(WalletApiOperation.RunBackupCycle, {}); - - console.log( - "wallet1 balance before spend:", - await wallet.client.call(WalletApiOperation.GetBalances, {}), - ); - - await makeTestPayment(t, { - merchant, - wallet, - order: { - summary: "foo", - amount: "TESTKUDOS:7", - }, - }); - - await wallet.runUntilDone(); - - console.log( - "wallet1 balance after spend:", - await wallet.client.call(WalletApiOperation.GetBalances, {}), - ); - - { - console.log( - "wallet2 balance:", - await wallet2.client.call(WalletApiOperation.GetBalances, {}), - ); - } - - // Now we double-spend with the second wallet - - { - const instance = "default"; - - const orderResp = await MerchantPrivateApi.createOrder(merchant, instance, { - order: { - amount: "TESTKUDOS:8", - summary: "bla", - fulfillment_url: "taler://fulfillment-success", - }, - }); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - ); - - t.assertTrue(orderStatus.order_status === "unpaid"); - - // Make wallet pay for the order - - const preparePayResult = await wallet2.client.call( - WalletApiOperation.PreparePayForUri, - { - talerPayUri: orderStatus.taler_pay_uri, - }, - ); - - t.assertTrue( - preparePayResult.status === PreparePayResultType.PaymentPossible, - ); - - const res = await wallet2.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - - console.log(res); - - // FIXME: wait for a notification that indicates insufficient funds! - - await withdrawViaBank(t, { - wallet: wallet2, - bank, - exchange, - amount: "TESTKUDOS:50", - }); - - const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {}); - console.log("bal", bal); - - await wallet2.runUntilDone(); - } -} - -runWalletBackupDoublespendTest.suites = ["wallet", "wallet-backup"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts deleted file mode 100644 index c21a7279b..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts +++ /dev/null @@ -1,226 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Integration test for the wallet testing functionality used by the exchange - * test cases. - */ - -/** - * Imports. - */ -import { Amounts } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; -import { - BankService, - ExchangeService, - GlobalTestState, - MerchantService, - setupDb, - WalletCli, -} from "../harness/harness.js"; -import { SimpleTestEnvironment } from "../harness/helpers.js"; - -const merchantAuthToken = "secret-token:sandbox"; - -/** - * Run a test case with a simple TESTKUDOS Taler environment, consisting - * of one exchange, one bank and one merchant. - */ -export async function createMyEnvironment( - t: GlobalTestState, - coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), -): Promise<SimpleTestEnvironment> { - const db = await setupDb(t); - - const bank = await BankService.create(t, { - allowRegistrations: true, - currency: "TESTKUDOS", - database: db.connStr, - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - const merchant = await MerchantService.create(t, { - name: "testmerchant-1", - currency: "TESTKUDOS", - httpPort: 8083, - database: db.connStr, - }); - - const exchangeBankAccount = await bank.createExchangeAccount( - "MyExchange", - "x", - ); - exchange.addBankAccount("1", exchangeBankAccount); - - bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); - - await bank.start(); - - await bank.pingUntilAvailable(); - - exchange.addCoinConfigList(coinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - merchant.addExchange(exchange); - - 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); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - }; -} - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runWallettestingTest(t: GlobalTestState) { - const { wallet, bank, exchange, merchant } = await createMyEnvironment(t); - - await wallet.client.call(WalletApiOperation.RunIntegrationTest, { - amountToSpend: "TESTKUDOS:5", - amountToWithdraw: "TESTKUDOS:10", - bankBaseUrl: bank.baseUrl, - exchangeBaseUrl: exchange.baseUrl, - merchantAuthToken: merchantAuthToken, - merchantBaseUrl: merchant.makeInstanceBaseUrl(), - }); - - let txns = await wallet.client.call(WalletApiOperation.GetTransactions, {}); - console.log(JSON.stringify(txns, undefined, 2)); - let txTypes = txns.transactions.map((x) => x.type); - - t.assertDeepEqual(txTypes, [ - "withdrawal", - "payment", - "withdrawal", - "payment", - "refund", - "payment", - ]); - - wallet.deleteDatabase(); - - await wallet.client.call(WalletApiOperation.WithdrawTestBalance, { - amount: "TESTKUDOS:10", - bankBaseUrl: bank.baseUrl, - exchangeBaseUrl: exchange.baseUrl, - }); - - await wallet.runUntilDone(); - - await wallet.client.call(WalletApiOperation.TestPay, { - amount: "TESTKUDOS:5", - merchantAuthToken: merchantAuthToken, - merchantBaseUrl: merchant.makeInstanceBaseUrl(), - summary: "foo", - }); - - await wallet.runUntilDone(); - - txns = await wallet.client.call(WalletApiOperation.GetTransactions, {}); - console.log(JSON.stringify(txns, undefined, 2)); - txTypes = txns.transactions.map((x) => x.type); - - t.assertDeepEqual(txTypes, ["withdrawal", "payment"]); - - wallet.deleteDatabase(); - - await wallet.client.call(WalletApiOperation.WithdrawTestBalance, { - amount: "TESTKUDOS:10", - bankBaseUrl: bank.baseUrl, - exchangeBaseUrl: exchange.baseUrl, - }); - - await wallet.runUntilDone(); - - const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {}); - - console.log("coin dump:", JSON.stringify(coinDump, undefined, 2)); - - let susp: string | undefined; - { - for (const c of coinDump.coins) { - if (0 === Amounts.cmp(c.remaining_value, "TESTKUDOS:8")) { - susp = c.coin_pub; - } - } - } - - t.assertTrue(susp !== undefined); - - console.log("suspending coin"); - - await wallet.client.call(WalletApiOperation.SetCoinSuspended, { - coinPub: susp, - suspended: true, - }); - - // This should fail, as we've suspended a coin that we need - // to pay. - await t.assertThrowsAsync(async () => { - await wallet.client.call(WalletApiOperation.TestPay, { - amount: "TESTKUDOS:5", - merchantAuthToken: merchantAuthToken, - merchantBaseUrl: merchant.makeInstanceBaseUrl(), - summary: "foo", - }); - }); - - console.log("unsuspending coin"); - - await wallet.client.call(WalletApiOperation.SetCoinSuspended, { - coinPub: susp, - suspended: false, - }); - - await wallet.client.call(WalletApiOperation.TestPay, { - amount: "TESTKUDOS:5", - merchantAuthToken: merchantAuthToken, - merchantBaseUrl: merchant.makeInstanceBaseUrl(), - summary: "foo", - }); - - await t.shutdown(); -} - -runWallettestingTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts deleted file mode 100644 index fe719ea62..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { TalerErrorCode } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, BankApi, BankAccessApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runWithdrawalAbortBankTest(t: GlobalTestState) { - // Set up test environment - - const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t); - - // Create a withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, - "TESTKUDOS:10", - ); - - // Hand it to the wallet - - await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - - await wallet.runPending(); - - // Confirm it - - await BankApi.abortWithdrawalOperation(bank, user, wop); - - // Withdraw - - const e = await t.assertThrowsOperationErrorAsync(async () => { - await wallet.client.call( - WalletApiOperation.AcceptBankIntegratedWithdrawal, - { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }, - ); - }); - t.assertDeepEqual( - e.operationError.code, - TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK, - ); - - await t.shutdown(); -} - -runWithdrawalAbortBankTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts deleted file mode 100644 index 35969c78f..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, BankApi, BankAccessApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; -import { codecForBalancesResponse } from "@gnu-taler/taler-util"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { - // Set up test environment - - const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t); - - // Create a withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation( - bank, - user, - "TESTKUDOS:10", - ); - - // Hand it to the wallet - - const r1 = await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { - talerWithdrawUri: wop.taler_withdraw_uri, - }); - - await wallet.runPending(); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bank, user, wop); - - // Withdraw - - const r2 = await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - }); - await wallet.runUntilDone(); - - // Check balance - - const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {}); - t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); - - await t.shutdown(); -} - -runWithdrawalBankIntegratedTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts deleted file mode 100644 index 97beba1bf..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts +++ /dev/null @@ -1,96 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - GlobalTestState, - BankApi, - WalletCli, - setupDb, - ExchangeService, - FakeBankService, -} from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; -import { URL } from "@gnu-taler/taler-util"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runTestWithdrawalFakebankTest(t: GlobalTestState) { - // Set up test environment - - const db = await setupDb(t); - - const bank = await FakeBankService.create(t, { - currency: "TESTKUDOS", - httpPort: 8082, - }); - - const exchange = ExchangeService.create(t, { - name: "testexchange-1", - currency: "TESTKUDOS", - httpPort: 8081, - database: db.connStr, - }); - - exchange.addBankAccount("1", { - accountName: "exchange", - accountPassword: "x", - wireGatewayApiBaseUrl: new URL("/exchange/", bank.baseUrl).href, - accountPaytoUri: "payto://x-taler-bank/localhost/exchange", - }); - - await bank.start(); - - await bank.pingUntilAvailable(); - - const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); - exchange.addCoinConfigList(coinConfig); - - await exchange.start(); - await exchange.pingUntilAvailable(); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - await wallet.client.call(WalletApiOperation.AddExchange, { - exchangeBaseUrl: exchange.baseUrl, - }); - - await wallet.client.call(WalletApiOperation.WithdrawFakebank, { - exchange: exchange.baseUrl, - amount: "TESTKUDOS:10", - bank: bank.baseUrl, - }); - - await exchange.runWirewatchOnce(); - - await wallet.runUntilDone(); - - // Check balance - - const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {}); - t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); - - await t.shutdown(); -} - -runTestWithdrawalFakebankTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts deleted file mode 100644 index b93d1b500..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { GlobalTestState, BankApi } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; -import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; - -/** - * Run test for basic, bank-integrated withdrawal. - */ -export async function runTestWithdrawalManualTest(t: GlobalTestState) { - // Set up test environment - - const { - wallet, - bank, - exchange, - exchangeBankAccount, - } = await createSimpleTestkudosEnvironment(t); - - // Create a withdrawal operation - - const user = await BankApi.createRandomBankUser(bank); - - await wallet.client.call(WalletApiOperation.AddExchange, { - exchangeBaseUrl: exchange.baseUrl, - }); - - - const wres = await wallet.client.call(WalletApiOperation.AcceptManualWithdrawal, { - exchangeBaseUrl: exchange.baseUrl, - amount: "TESTKUDOS:10", - }); - - const reservePub: string = wres.reservePub; - - await BankApi.adminAddIncoming(bank, { - exchangeBankAccount, - amount: "TESTKUDOS:10", - debitAccountPayto: user.accountPaytoUri, - reservePub: reservePub, - }); - - await exchange.runWirewatchOnce(); - - await wallet.runUntilDone(); - - // Check balance - - const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {}); - t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); - - await t.shutdown(); -} - -runTestWithdrawalManualTest.suites = ["wallet"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts deleted file mode 100644 index d985ed67f..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts +++ /dev/null @@ -1,482 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -import { - minimatch -} from "@gnu-taler/taler-util"; -import { - GlobalTestState, - runTestWithState, - shouldLingerInTest, - TestRunResult, -} from "../harness/harness.js"; -import { runPaymentTest } from "./test-payment"; -import { runPaymentDemoTest } from "./test-payment-on-demo"; -import * as fs from "fs"; -import * as path from "path"; -import * as os from "os"; -import * as child_process from "child_process"; -import { runBankApiTest } from "./test-bank-api"; -import { runClaimLoopTest } from "./test-claim-loop"; -import { runExchangeManagementTest } from "./test-exchange-management"; -import { runFeeRegressionTest } from "./test-fee-regression"; -import { runMerchantLongpollingTest } from "./test-merchant-longpolling"; -import { runMerchantRefundApiTest } from "./test-merchant-refund-api"; -import { runPayAbortTest } from "./test-pay-abort"; -import { runPayPaidTest } from "./test-pay-paid"; -import { runPaymentClaimTest } from "./test-payment-claim"; -import { runPaymentFaultTest } from "./test-payment-fault"; -import { runPaymentIdempotencyTest } from "./test-payment-idempotency"; -import { runPaymentMultipleTest } from "./test-payment-multiple"; -import { runPaymentTransientTest } from "./test-payment-transient"; -import { runPaywallFlowTest } from "./test-paywall-flow"; -import { runRefundAutoTest } from "./test-refund-auto"; -import { runRefundGoneTest } from "./test-refund-gone"; -import { runRefundIncrementalTest } from "./test-refund-incremental"; -import { runRefundTest } from "./test-refund"; -import { runRevocationTest } from "./test-revocation"; -import { runTimetravelAutorefreshTest } from "./test-timetravel-autorefresh"; -import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw"; -import { runTippingTest } from "./test-tipping"; -import { runWallettestingTest } from "./test-wallettesting"; -import { runTestWithdrawalManualTest } from "./test-withdrawal-manual"; -import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank"; -import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated"; -import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion"; -import { runLibeufinBasicTest } from "./test-libeufin-basic"; -import { runLibeufinC5xTest } from "./test-libeufin-c5x"; -import { runLibeufinNexusBalanceTest } from "./test-libeufin-nexus-balance"; -import { runLibeufinBadGatewayTest } from "./test-libeufin-bad-gateway"; -import { runLibeufinKeyrotationTest } from "./test-libeufin-keyrotation"; -import { runLibeufinRefundTest } from "./test-libeufin-refund"; -import { runLibeufinRefundMultipleUsersTest } from "./test-libeufin-refund-multiple-users"; -import { runLibeufinTutorialTest } from "./test-libeufin-tutorial"; -import { runLibeufinApiPermissionsTest } from "./test-libeufin-api-permissions"; -import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade"; -import { runLibeufinApiFacadeBadRequestTest } from "./test-libeufin-api-facade-bad-request"; -import { runLibeufinAnastasisFacadeTest } from "./test-libeufin-facade-anastasis"; -import { runLibeufinApiSchedulingTest } from "./test-libeufin-api-scheduling"; -import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection"; -import { runLibeufinApiUsersTest } from "./test-libeufin-api-users"; -import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount"; -import { runLibeufinApiSandboxTransactionsTest } from "./test-libeufin-api-sandbox-transactions"; -import { runLibeufinApiSandboxCamtTest } from "./test-libeufin-api-sandbox-camt"; -import { runLibeufinSandboxWireTransferCliTest } from "./test-libeufin-sandbox-wire-transfer-cli"; -import { runDepositTest } from "./test-deposit"; -import CancellationToken from "cancellationtoken"; -import { runMerchantInstancesTest } from "./test-merchant-instances"; -import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls"; -import { runWalletBackupBasicTest } from "./test-wallet-backup-basic"; -import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete"; -import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend"; -import { runPaymentForgettableTest } from "./test-payment-forgettable.js"; -import { runPaymentZeroTest } from "./test-payment-zero.js"; -import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js"; -import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js"; -import { runDenomUnofferedTest } from "./test-denom-unoffered.js"; -import { runTestWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js"; - -/** - * Test runner. - */ - -/** - * Spec for one test. - */ -interface TestMainFunction { - (t: GlobalTestState): Promise<void>; - timeoutMs?: number; - excludeByDefault?: boolean; - suites?: string[]; -} - -const allTests: TestMainFunction[] = [ - runBankApiTest, - runClaimLoopTest, - runDepositTest, - runDenomUnofferedTest, - runExchangeManagementTest, - runExchangeTimetravelTest, - runFeeRegressionTest, - runLibeufinBasicTest, - runLibeufinKeyrotationTest, - runLibeufinTutorialTest, - runLibeufinRefundTest, - runLibeufinC5xTest, - runLibeufinNexusBalanceTest, - runLibeufinBadGatewayTest, - runLibeufinRefundMultipleUsersTest, - runLibeufinApiPermissionsTest, - runLibeufinApiFacadeTest, - runLibeufinApiFacadeBadRequestTest, - runLibeufinAnastasisFacadeTest, - runLibeufinApiSchedulingTest, - runLibeufinApiUsersTest, - runLibeufinApiBankaccountTest, - runLibeufinApiBankconnectionTest, - runLibeufinApiSandboxTransactionsTest, - runLibeufinApiSandboxCamtTest, - runLibeufinSandboxWireTransferCliTest, - runMerchantExchangeConfusionTest, - runMerchantInstancesTest, - runMerchantInstancesDeleteTest, - runMerchantInstancesUrlsTest, - runMerchantLongpollingTest, - runMerchantSpecPublicOrdersTest, - runMerchantRefundApiTest, - runPayAbortTest, - runPaymentClaimTest, - runPaymentFaultTest, - runPaymentForgettableTest, - runPaymentIdempotencyTest, - runPaymentMultipleTest, - runPaymentTest, - runPaymentDemoTest, - runPaymentTransientTest, - runPaymentZeroTest, - runPayPaidTest, - runPaywallFlowTest, - runRefundAutoTest, - runRefundGoneTest, - runRefundIncrementalTest, - runRefundTest, - runRevocationTest, - runTestWithdrawalManualTest, - runTestWithdrawalFakebankTest, - runTimetravelAutorefreshTest, - runTimetravelWithdrawTest, - runTippingTest, - runWalletBackupBasicTest, - runWalletBackupDoublespendTest, - runWallettestingTest, - runWithdrawalAbortBankTest, - runWithdrawalBankIntegratedTest, -]; - -export interface TestRunSpec { - includePattern?: string; - suiteSpec?: string; - dryRun?: boolean; - verbosity: number; -} - -export interface TestInfo { - name: string; - suites: string[]; - excludeByDefault: boolean; -} - -function updateCurrentSymlink(testDir: string): void { - const currLink = path.join( - os.tmpdir(), - `taler-integrationtests-${os.userInfo().username}-current`, - ); - try { - fs.unlinkSync(currLink); - } catch (e) { - // Ignore - } - try { - fs.symlinkSync(testDir, currLink); - } catch (e) { - console.log(e); - // Ignore - } -} - -export function getTestName(tf: TestMainFunction): string { - const res = tf.name.match(/run([a-zA-Z0-9]*)Test/); - if (!res) { - throw Error("invalid test name, must be 'run${NAME}Test'"); - } - return res[1] - .replace(/[a-z0-9][A-Z]/g, (x) => { - return x[0] + "-" + x[1]; - }) - .toLowerCase(); -} - -interface RunTestChildInstruction { - testName: string; - testRootDir: string; -} - -export async function runTests(spec: TestRunSpec) { - const testRootDir = fs.mkdtempSync( - path.join(os.tmpdir(), "taler-integrationtests-"), - ); - updateCurrentSymlink(testRootDir); - console.log("testsuite root directory: ", testRootDir); - - const testResults: TestRunResult[] = []; - - let currentChild: child_process.ChildProcess | undefined; - - const handleSignal = (s: NodeJS.Signals) => { - console.log(`received signal ${s} in test parent`); - if (currentChild) { - currentChild.kill("SIGTERM"); - } - reportAndQuit(testRootDir, testResults, true); - }; - - process.on("SIGINT", (s) => handleSignal(s)); - process.on("SIGTERM", (s) => handleSignal(s)); - //process.on("unhandledRejection", handleSignal); - //process.on("uncaughtException", handleSignal); - - let suites: Set<string> | undefined; - - if (spec.suiteSpec) { - suites = new Set(spec.suiteSpec.split(",").map((x) => x.trim())); - } - - for (const [n, testCase] of allTests.entries()) { - const testName = getTestName(testCase); - if (spec.includePattern && !minimatch(testName, spec.includePattern)) { - continue; - } - - if (suites) { - const ts = new Set(testCase.suites ?? []); - const intersection = new Set([...suites].filter((x) => ts.has(x))); - if (intersection.size === 0) { - continue; - } - } else { - if (testCase.excludeByDefault) { - continue; - } - } - - if (spec.dryRun) { - console.log(`dry run: would run test ${testName}`); - continue; - } - - const testInstr: RunTestChildInstruction = { - testName, - testRootDir, - }; - - currentChild = child_process.fork(__filename, ["__TWCLI_TESTWORKER"], { - env: { - TWCLI_RUN_TEST_INSTRUCTION: JSON.stringify(testInstr), - ...process.env, - }, - stdio: ["pipe", "pipe", "pipe", "ipc"], - }); - - const testDir = path.join(testRootDir, testName); - fs.mkdirSync(testDir, { recursive: true }); - - const harnessLogFilename = path.join(testRootDir, testName, "harness.log"); - const harnessLogStream = fs.createWriteStream(harnessLogFilename); - - if (spec.verbosity > 0) { - currentChild.stderr?.pipe(process.stderr); - currentChild.stdout?.pipe(process.stdout); - } - - currentChild.stdout?.pipe(harnessLogStream); - currentChild.stderr?.pipe(harnessLogStream); - - const defaultTimeout = 60000; - const testTimeoutMs = testCase.timeoutMs ?? defaultTimeout; - - console.log(`running ${testName} with timeout ${testTimeoutMs}ms`); - - const { token } = CancellationToken.timeout(testTimeoutMs); - - const resultPromise: Promise<TestRunResult> = new Promise( - (resolve, reject) => { - let msg: TestRunResult | undefined; - currentChild!.on("message", (m) => { - if (token.isCancelled) { - return; - } - msg = m as TestRunResult; - }); - currentChild!.on("exit", (code, signal) => { - if (token.isCancelled) { - return; - } - console.log(`process exited code=${code} signal=${signal}`); - if (signal) { - reject(new Error(`test worker exited with signal ${signal}`)); - } else if (code != 0) { - reject(new Error(`test worker exited with code ${code}`)); - } else if (!msg) { - reject( - new Error( - `test worker exited without giving back the test results`, - ), - ); - } else { - resolve(msg); - } - }); - currentChild!.on("error", (err) => { - if (token.isCancelled) { - return; - } - reject(err); - }); - }, - ); - - let result: TestRunResult; - - try { - result = await token.racePromise(resultPromise); - } catch (e: any) { - console.error(`test ${testName} timed out`); - if (token.isCancelled) { - result = { - status: "fail", - reason: "timeout", - timeSec: testTimeoutMs / 1000, - name: testName, - }; - currentChild.kill("SIGTERM"); - } else { - throw Error(e); - } - } - - harnessLogStream.close(); - - console.log(`parent: got result ${JSON.stringify(result)}`); - - testResults.push(result); - } - - reportAndQuit(testRootDir, testResults); -} - -export function reportAndQuit( - testRootDir: string, - testResults: TestRunResult[], - interrupted: boolean = false, -): never { - let numTotal = 0; - let numFail = 0; - let numSkip = 0; - let numPass = 0; - - for (const result of testResults) { - numTotal++; - if (result.status === "fail") { - numFail++; - } else if (result.status === "skip") { - numSkip++; - } else if (result.status === "pass") { - numPass++; - } - } - - const resultsFile = path.join(testRootDir, "results.json"); - fs.writeFileSync( - path.join(testRootDir, "results.json"), - JSON.stringify({ testResults, interrupted }, undefined, 2), - ); - if (interrupted) { - console.log("test suite was interrupted"); - } - console.log(`See ${resultsFile} for details`); - console.log(`Skipped: ${numSkip}/${numTotal}`); - console.log(`Failed: ${numFail}/${numTotal}`); - console.log(`Passed: ${numPass}/${numTotal}`); - - if (interrupted) { - process.exit(3); - } else if (numPass < numTotal - numSkip) { - process.exit(1); - } else { - process.exit(0); - } -} - -export function getTestInfo(): TestInfo[] { - return allTests.map((x) => ({ - name: getTestName(x), - suites: x.suites ?? [], - excludeByDefault: x.excludeByDefault ?? false, - })); -} - -const runTestInstrStr = process.env["TWCLI_RUN_TEST_INSTRUCTION"]; -if (runTestInstrStr && process.argv.includes("__TWCLI_TESTWORKER")) { - // Test will call taler-wallet-cli, so we must not propagate this variable. - delete process.env["TWCLI_RUN_TEST_INSTRUCTION"]; - const { testRootDir, testName } = JSON.parse( - runTestInstrStr, - ) as RunTestChildInstruction; - console.log(`running test ${testName} in worker process`); - - process.on("disconnect", () => { - console.log("got disconnect from parent"); - process.exit(3); - }); - - try { - require("source-map-support").install(); - } catch (e) { - // Do nothing. - } - - const runTest = async () => { - let testMain: TestMainFunction | undefined; - for (const t of allTests) { - if (getTestName(t) === testName) { - testMain = t; - break; - } - } - - if (!process.send) { - console.error("can't communicate with parent"); - process.exit(2); - } - - if (!testMain) { - console.log(`test ${testName} not found`); - process.exit(2); - } - - const testDir = path.join(testRootDir, testName); - console.log(`running test ${testName}`); - const gc = new GlobalTestState({ - testDir, - }); - const testResult = await runTestWithState(gc, testMain, testName); - process.send(testResult); - }; - - runTest() - .then(() => { - console.log(`test ${testName} finished in worker`); - if (shouldLingerInTest()) { - console.log("lingering ..."); - return; - } - process.exit(0); - }) - .catch((e) => { - console.log(e); - process.exit(1); - }); -} diff --git a/packages/taler-wallet-cli/src/lint.ts b/packages/taler-wallet-cli/src/lint.ts deleted file mode 100644 index 2b888ccf4..000000000 --- a/packages/taler-wallet-cli/src/lint.ts +++ /dev/null @@ -1,534 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * The deployment linter implements checks for a deployment - * of the GNU Taler exchange. It is meant to help sysadmins - * when setting up an exchange. - * - * The linter does checks in the configuration and uses - * various tools of the exchange in test mode (-t). - * - * To be able to run the tools as the right user, the linter should be - * run as root. - * - * @author Florian Dold <dold@taler.net> - */ - -/** - * Imports. - */ -import { - codecForExchangeKeysJson, - codecForKeysManagementResponse, - Configuration, - decodeCrock, -} from "@gnu-taler/taler-util"; -import { - NodeHttpLib, - readSuccessResponseJsonOrThrow, -} from "@gnu-taler/taler-wallet-core"; -import { URL } from "url"; -import { spawn } from "child_process"; -import { delayMs } from "./harness/harness.js"; - -interface BasicConf { - mainCurrency: string; -} - -interface PubkeyConf { - masterPublicKey: string; -} - -const httpLib = new NodeHttpLib(); - -interface ShellResult { - stdout: string; - stderr: string; - status: number; -} - -interface LintContext { - /** - * Be more verbose. - */ - verbose: boolean; - - /** - * Always continue even after errors. - */ - cont: boolean; - - cfg: Configuration; - - numErr: number; -} - -/** - * Run a shell command, return stdout. - */ -export async function sh( - context: LintContext, - command: string, - env: { [index: string]: string | undefined } = process.env, -): Promise<ShellResult> { - if (context.verbose) { - console.log("executing command:", command); - } - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const stderrChunks: Buffer[] = []; - const proc = spawn(command, { - stdio: ["inherit", "pipe", "pipe"], - shell: true, - env: env, - }); - proc.stdout.on("data", (x) => { - if (x instanceof Buffer) { - stdoutChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - proc.stderr.on("data", (x) => { - if (x instanceof Buffer) { - stderrChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - proc.on("exit", (code, signal) => { - if (code != 0 && context.verbose) { - console.log(`child process exited (${code} / ${signal})`); - } - const bOut = Buffer.concat(stdoutChunks).toString("utf-8"); - const bErr = Buffer.concat(stderrChunks).toString("utf-8"); - resolve({ - status: code ?? -1, - stderr: bErr, - stdout: bOut, - }); - }); - proc.on("error", () => { - reject(Error("Child process had error")); - }); - }); -} - -function checkBasicConf(context: LintContext): BasicConf { - const cfg = context.cfg; - const currencyEntry = cfg.getString("taler", "currency"); - let mainCurrency: string | undefined; - - if (!currencyEntry.value) { - context.numErr++; - console.log("error: currency not defined in section TALER option CURRENCY"); - console.log("Aborting further checks."); - process.exit(1); - } else { - mainCurrency = currencyEntry.value.toUpperCase(); - } - - if (mainCurrency === "KUDOS") { - console.log( - "warning: section TALER option CURRENCY contains toy currency value KUDOS", - ); - } - - const roundUnit = cfg.getAmount("taler", "currency_round_unit"); - const ru = roundUnit.required(); - if (ru.currency.toLowerCase() != mainCurrency.toLowerCase()) { - context.numErr++; - console.log( - "error: [TALER]/CURRENCY_ROUND_UNIT: currency does not match main currency", - ); - } - return { mainCurrency }; -} - -function checkCoinConfig(context: LintContext, basic: BasicConf): void { - const cfg = context.cfg; - const coinPrefix1 = "COIN_"; - const coinPrefix2 = "COIN-"; - let numCoins = 0; - - for (const secName of cfg.getSectionNames()) { - if (!(secName.startsWith(coinPrefix1) || secName.startsWith(coinPrefix2))) { - continue; - } - numCoins++; - - // FIXME: check that section is well-formed - } - - if (numCoins == 0) { - context.numErr++; - console.log( - "error: no coin denomination configured, please configure [coin-*] sections", - ); - } -} - -async function checkWireConfig(context: LintContext): Promise<void> { - const cfg = context.cfg; - const accountPrefix = "EXCHANGE-ACCOUNT-"; - const accountCredentialsPrefix = "EXCHANGE-ACCOUNTCREDENTIALS-"; - - let accounts = new Set<string>(); - let credentials = new Set<string>(); - - for (const secName of cfg.getSectionNames()) { - if (secName.startsWith(accountPrefix)) { - accounts.add(secName.slice(accountPrefix.length)); - // FIXME: check settings - } - - if (secName.startsWith(accountCredentialsPrefix)) { - credentials.add(secName.slice(accountCredentialsPrefix.length)); - // FIXME: check settings - } - } - - if (accounts.size === 0) { - context.numErr++; - console.log( - "error: No accounts configured (no sections EXCHANGE-ACCOUNT-*).", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - for (const acc of accounts) { - if (!credentials.has(acc)) { - console.log( - `warning: no credentials configured for exchange-account-${acc}`, - ); - } - } - - for (const acc of accounts) { - // test credit history - { - const res = await sh( - context, - "su -l --shell /bin/sh " + - `-c 'taler-exchange-wire-gateway-client -s exchange-accountcredentials-${acc} --credit-history' ` + - "taler-exchange-wire", - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log( - "error: Could not run taler-exchange-wire-gateway-client. Please review logs above.", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - } - - // TWG client - { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-wirewatch -t' taler-exchange-wire`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run wirewatch. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - - // Wirewatch - { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-wirewatch -t' taler-exchange-wire`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run wirewatch. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - - // Closer - { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run closer. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } -} - -async function checkAggregatorConfig(context: LintContext) { - const res = await sh( - context, - "su -l --shell /bin/sh -c 'taler-exchange-aggregator -t' taler-exchange-aggregator", - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run aggregator. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } -} - -async function checkCloserConfig(context: LintContext) { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run closer. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } -} - -function checkMasterPublicKeyConfig(context: LintContext): PubkeyConf { - const cfg = context.cfg; - const pub = cfg.getString("exchange", "master_public_key"); - - const pubDecoded = decodeCrock(pub.required()); - - if (pubDecoded.length != 32) { - context.numErr++; - console.log("error: invalid master public key"); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - return { - masterPublicKey: pub.required(), - }; -} - -export async function checkExchangeHttpd( - context: LintContext, - pubConf: PubkeyConf, -): Promise<void> { - const cfg = context.cfg; - const baseUrlEntry = cfg.getString("exchange", "base_url"); - - if (!baseUrlEntry.isDefined) { - context.numErr++; - console.log( - "error: configuration needs to specify section EXCHANGE option BASE_URL", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - const baseUrl = baseUrlEntry.required(); - - if (!baseUrl.startsWith("http")) { - context.numErr++; - console.log( - "error: section EXCHANGE option BASE_URL needs to be an http or https URL", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - if (!baseUrl.endsWith("/")) { - context.numErr++; - console.log( - "error: section EXCHANGE option BASE_URL needs to end with a slash", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - if (!baseUrl.startsWith("https://")) { - console.log( - "warning: section EXCHANGE option BASE_URL: it is recommended to serve the exchange via HTTPS", - ); - } - - { - const mgmtUrl = new URL("management/keys", baseUrl); - const resp = await httpLib.get(mgmtUrl.href); - - const futureKeys = await readSuccessResponseJsonOrThrow( - resp, - codecForKeysManagementResponse(), - ); - - if (futureKeys.future_denoms.length > 0) { - console.log( - `warning: exchange has denomination keys that need to be signed by the offline signing procedure`, - ); - } - - if (futureKeys.future_signkeys.length > 0) { - console.log( - `warning: exchange has signing keys that need to be signed by the offline signing procedure`, - ); - } - } - - // Check if we can use /keys already - { - const keysUrl = new URL("keys", baseUrl); - - const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); - - if (!resp) { - context.numErr++; - console.log( - "error: request to /keys timed out. " + - "Make sure to sign and upload denomination and signing keys " + - "with taler-exchange-offline.", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } else { - const keys = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeKeysJson(), - ); - - if (keys.master_public_key !== pubConf.masterPublicKey) { - context.numErr++; - console.log( - "error: master public key of exchange does not match public key of live exchange", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - } - - // Check /wire - { - const keysUrl = new URL("wire", baseUrl); - - const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); - - if (!resp) { - context.numErr++; - console.log( - "error: request to /wire timed out. " + - "Make sure to sign and upload accounts and wire fees " + - "using the taler-exchange-offline tool.", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } else { - if (resp.status !== 200) { - console.log( - "error: Can't access exchange /wire. Please check " + - "the logs of taler-exchange-httpd for further information.", - ); - } - } - } -} - -/** - * Do some basic checks in the configuration of a Taler deployment. - */ -export async function lintExchangeDeployment( - verbose: boolean, - cont: boolean, -): Promise<void> { - if (process.getuid() != 0) { - console.log( - "warning: the exchange deployment linter is designed to be run as root", - ); - } - - const cfg = Configuration.load(); - - const context: LintContext = { - cont, - verbose, - cfg, - numErr: 0, - }; - - const basic = checkBasicConf(context); - - checkCoinConfig(context, basic); - - await checkWireConfig(context); - - await checkAggregatorConfig(context); - - await checkCloserConfig(context); - - const pubConf = checkMasterPublicKeyConfig(context); - - await checkExchangeHttpd(context, pubConf); - - if (context.numErr == 0) { - console.log("Linting completed without errors."); - process.exit(0); - } else { - console.log(`Linting completed with ${context.numErr} errors.`); - process.exit(1); - } -} |