/* This file is part of GNU Taler (C) 2019 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 */ /** * Imports. */ import { deepStrictEqual } from "assert"; import fs from "fs"; import os from "os"; import path from "path"; import { addPaytoQueryParams, Amounts, Configuration, decodeCrock, j2s, Logger, parsePaytoUri, rsaBlind, setGlobalLogLevelFromString, } from "@gnu-taler/taler-util"; import { runBench1 } from "./bench1.js"; import { runBench2 } from "./bench2.js"; import { runBench3 } from "./bench3.js"; import { runEnv1 } from "./env1.js"; import { GlobalTestState, MerchantApiClient, MerchantPrivateApi, runTestWithState, } from "./harness/harness.js"; import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; import { lintExchangeDeployment } from "./lint.js"; import { runEnvFull } from "./env-full.js"; import { clk } from "@gnu-taler/taler-util/clk"; import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; import { BankAccessApiClient, checkReserve, CryptoDispatcher, downloadExchangeInfo, SynchronousCryptoWorkerFactoryPlain, topupReserveWithDemobank, } from "@gnu-taler/taler-wallet-core"; const logger = new Logger("taler-harness:index.ts"); process.on("unhandledRejection", (error: any) => { logger.error("unhandledRejection", error.message); logger.error("stack", error.stack); process.exit(2); }); declare const __VERSION__: string; function printVersion(): void { console.log(__VERSION__); process.exit(0); } export const testingCli = clk .program("testing", { help: "Command line interface for the GNU Taler test/deployment harness.", }) .maybeOption("log", ["-L", "--log"], clk.STRING, { help: "configure log level (NONE, ..., TRACE)", onPresentHandler: (x) => { setGlobalLogLevelFromString(x); }, }) .flag("version", ["-v", "--version"], { onPresentHandler: printVersion, }) .flag("verbose", ["-V", "--verbose"], { help: "Enable verbose output.", }); const advancedCli = testingCli.subcommand("advancedArgs", "advanced", { help: "Subcommands for advanced operations (only use if you know what you're doing!).", }); advancedCli .subcommand("decode", "decode", { help: "Decode base32-crockford.", }) .action((args) => { const enc = fs.readFileSync(0, "utf8"); console.log(decodeCrock(enc.trim())); }); advancedCli .subcommand("bench1", "bench1", { help: "Run the 'bench1' benchmark", }) .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); }); advancedCli .subcommand("bench2", "bench2", { help: "Run the 'bench2' benchmark", }) .requiredOption("configJson", ["--config-json"], clk.STRING) .action(async (args) => { let config: any; try { config = JSON.parse(args.bench2.configJson); } catch (e) { console.log("Could not parse config JSON"); } await runBench2(config); }); advancedCli .subcommand("bench3", "bench3", { help: "Run the 'bench3' benchmark", }) .requiredOption("configJson", ["--config-json"], clk.STRING) .action(async (args) => { let config: any; try { config = JSON.parse(args.bench3.configJson); } catch (e) { console.log("Could not parse config JSON"); } await runBench3(config); }); advancedCli .subcommand("envFull", "env-full", { help: "Run a test environment for bench1", }) .action(async (args) => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env-full-")); const testState = new GlobalTestState({ testDir, }); await runTestWithState(testState, runEnvFull, "env-full", true); }); advancedCli .subcommand("env1", "env1", { help: "Run a test environment for bench1", }) .action(async (args) => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-")); const testState = new GlobalTestState({ testDir, }); await runTestWithState(testState, runEnv1, "env1", true); }); const sandcastleCli = testingCli.subcommand("sandcastleArgs", "sandcastle", { help: "Subcommands for handling GNU Taler sandcastle deployments.", }); const deploymentCli = testingCli.subcommand("deploymentArgs", "deployment", { help: "Subcommands for handling GNU Taler deployments.", }); deploymentCli .subcommand("tipTopup", "tip-topup") .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING) .requiredOption("exchangeBaseUrl", ["--exchange-url"], clk.STRING) .requiredOption("merchantApikey", ["--merchant-apikey"], clk.STRING) .requiredOption("bankAccessUrl", ["--bank-access-url"], clk.STRING) .requiredOption("bankAccount", ["--bank-account"], clk.STRING) .requiredOption("bankPassword", ["--bank-password"], clk.STRING) .requiredOption("wireMethod", ["--wire-method"], clk.STRING) .requiredOption("amount", ["--amount"], clk.STRING) .action(async (args) => { const amount = args.tipTopup.amount; const merchantClient = new MerchantApiClient( args.tipTopup.merchantBaseUrl, { method: "token", token: args.tipTopup.merchantApikey, }, ); const res = await merchantClient.getPrivateInstanceInfo(); console.log(res); const tipReserveResp = await merchantClient.createTippingReserve({ exchange_url: args.tipTopup.exchangeBaseUrl, initial_balance: amount, wire_method: args.tipTopup.wireMethod, }); console.log(tipReserveResp); const bankAccessApiClient = new BankAccessApiClient({ baseUrl: args.tipTopup.bankAccessUrl, username: args.tipTopup.bankAccount, password: args.tipTopup.bankPassword, }); const paytoUri = addPaytoQueryParams(tipReserveResp.payto_uri, { message: `tip-reserve ${tipReserveResp.reserve_pub}`, }); console.log("payto URI:", paytoUri); const transactions = await bankAccessApiClient.getTransactions(); console.log("transactions:", j2s(transactions)); await bankAccessApiClient.createTransaction({ amount, paytoUri, }); }); deploymentCli .subcommand("tipCleanup", "tip-cleanup") .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING) .requiredOption("merchantApikey", ["--merchant-apikey"], clk.STRING) .flag("dryRun", ["--dry-run"]) .action(async (args) => { const merchantClient = new MerchantApiClient( args.tipCleanup.merchantBaseUrl, { method: "token", token: args.tipCleanup.merchantApikey, }, ); const res = await merchantClient.getPrivateInstanceInfo(); console.log(res); const tipRes = await merchantClient.getPrivateTipReserves(); console.log(tipRes); for (const reserve of tipRes.reserves) { if (Amounts.isZero(reserve.exchange_initial_amount)) { if (args.tipCleanup.dryRun) { logger.info(`dry run, would purge reserve ${reserve}`); } else { await merchantClient.deleteTippingReserve({ reservePub: reserve.reserve_pub, purge: true, }); } } } // FIXME: Now delete reserves that are not filled yet }); deploymentCli .subcommand("testTalerdotnetDemo", "test-demodottalerdotnet") .action(async (args) => { const http = createPlatformHttpLib(); const cryptiDisp = new CryptoDispatcher( new SynchronousCryptoWorkerFactoryPlain(), ); const cryptoApi = cryptiDisp.cryptoApi; const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); const exchangeBaseUrl = "https://exchange.demo.taler.net/"; const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http); await topupReserveWithDemobank({ amount: "KUDOS:10", bankAccessApiBaseUrl: "https://bank.demo.taler.net/demobanks/default/access-api/", exchangeInfo, http, reservePub: reserveKeyPair.pub, }); let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl); reserveUrl.searchParams.set("timeout_ms", "30000"); console.log("requesting", reserveUrl.href); const longpollReq = http.fetch(reserveUrl.href, { method: "GET", }); const reserveStatusResp = await longpollReq; console.log("reserve status", reserveStatusResp.status); }); deploymentCli .subcommand("testDemoTestdotdalerdotnet", "test-testdottalerdotnet") .action(async (args) => { const http = createPlatformHttpLib(); const cryptiDisp = new CryptoDispatcher( new SynchronousCryptoWorkerFactoryPlain(), ); const cryptoApi = cryptiDisp.cryptoApi; const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); const exchangeBaseUrl = "https://exchange.test.taler.net/"; const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http); await topupReserveWithDemobank({ amount: "TESTKUDOS:10", bankAccessApiBaseUrl: "https://bank.test.taler.net/demobanks/default/access-api/", exchangeInfo, http, reservePub: reserveKeyPair.pub, }); let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl); reserveUrl.searchParams.set("timeout_ms", "30000"); console.log("requesting", reserveUrl.href); const longpollReq = http.fetch(reserveUrl.href, { method: "GET", }); const reserveStatusResp = await longpollReq; console.log("reserve status", reserveStatusResp.status); }); deploymentCli .subcommand("testLocalhostDemo", "test-demo-localhost") .action(async (args) => { // Run checks against the "env-full" demo deployment on localhost const http = createPlatformHttpLib(); const cryptiDisp = new CryptoDispatcher( new SynchronousCryptoWorkerFactoryPlain(), ); const cryptoApi = cryptiDisp.cryptoApi; const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); const exchangeBaseUrl = "http://localhost:8081/"; const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http); await topupReserveWithDemobank({ amount: "TESTKUDOS:10", bankAccessApiBaseUrl: "http://localhost:8082/taler-bank-access/", exchangeInfo, http, reservePub: reserveKeyPair.pub, }); let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl); reserveUrl.searchParams.set("timeout_ms", "30000"); console.log("requesting", reserveUrl.href); const longpollReq = http.fetch(reserveUrl.href, { method: "GET", }); const reserveStatusResp = await longpollReq; console.log("reserve status", reserveStatusResp.status); }); deploymentCli .subcommand("tipStatus", "tip-status") .requiredOption("merchantBaseUrl", ["--merchant-url"], clk.STRING) .requiredOption("merchantApikey", ["--merchant-apikey"], clk.STRING) .action(async (args) => { const merchantClient = new MerchantApiClient( args.tipStatus.merchantBaseUrl, { method: "token", token: args.tipStatus.merchantApikey, }, ); const res = await merchantClient.getPrivateInstanceInfo(); console.log(res); const tipRes = await merchantClient.getPrivateTipReserves(); console.log(j2s(tipRes)); }); 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 += `CIPHER = RSA\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, }), ); }); testingCli.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."); }); testingCli .subcommand("listIntegrationtests", "list-integrationtests") .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]`; } if (t.experimental) { s += ` [experimental]`; } console.log(s); } }); testingCli .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("experimental", ["--experimental"], { help: "Include tests marked as experimental", }) .flag("quiet", ["--quiet"], { help: "Produce less output.", }) .flag("noTimeout", ["--no-timeout"], { help: "Do not time out tests.", }) .action(async (args) => { await runTests({ includePattern: args.runIntegrationtests.pattern, suiteSpec: args.runIntegrationtests.suites, dryRun: args.runIntegrationtests.dryRun, verbosity: args.runIntegrationtests.quiet ? 0 : 1, includeExperimental: args.runIntegrationtests.experimental ?? false, noTimeout: args.runIntegrationtests.noTimeout, }); }); async function read(stream: NodeJS.ReadStream) { const chunks = []; for await (const chunk of stream) chunks.push(chunk); return Buffer.concat(chunks).toString("utf8"); } testingCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => { const data = await read(process.stdin); const lines = data.match(/[^\r\n]+/g); if (!lines) { throw Error("can't split lines"); } const vals: Record = {}; let inBlindSigningSection = false; 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); } vals[m[1]] = m[2]; } } console.log(vals); const req = (k: string) => { if (!vals[k]) { throw Error(`no value for ${k}`); } 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("check passed!"); }); export function main() { testingCli.run(); }