diff options
Diffstat (limited to 'packages/taler-wallet-cli')
4 files changed, 794 insertions, 23 deletions
diff --git a/packages/taler-wallet-cli/src/integrationtests/harness.ts b/packages/taler-wallet-cli/src/integrationtests/harness.ts index 4a856cea4..3434b5e71 100644 --- a/packages/taler-wallet-cli/src/integrationtests/harness.ts +++ b/packages/taler-wallet-cli/src/integrationtests/harness.ts @@ -76,8 +76,8 @@ import { codecForPrepareTipResult, AcceptTipRequest, AbortPayWithRefundRequest, - handleWorkerError, openPromise, + parsePaytoUri, } from "taler-wallet-core"; import { URL } from "url"; import axios, { AxiosError } from "axios"; @@ -352,6 +352,10 @@ export class GlobalTestState { 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) { @@ -368,6 +372,10 @@ export class GlobalTestState { } } +export function shouldLingerInTest(): boolean { + return !!process.env["TALER_TEST_LINGER"]; +} + export interface TalerConfigSection { options: Record<string, string | undefined>; } @@ -427,7 +435,11 @@ function setCoin(config: Configuration, c: CoinConfig) { config.setString(s, "rsa_keysize", `${c.rsaKeySize}`); } -async function pingProc( +/** + * Send an HTTP request until it succeeds or the + * process dies. + */ +export async function pingProc( proc: ProcessWrapper | undefined, url: string, serviceName: string, @@ -814,6 +826,15 @@ export class ExchangeService implements ExchangeServiceInterface { ); } + 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); @@ -1006,11 +1027,18 @@ export class ExchangeService implements ExchangeServiceInterface { ); 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")) { - accounts.push(config.getString(sectionName, "payto_uri").required()); + 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); } } @@ -1032,22 +1060,24 @@ export class ExchangeService implements ExchangeServiceInterface { } const year = new Date().getFullYear(); - for (let i = year; i < year + 5; i++) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - "wire-fee", - `${i}`, - "x-taler-bank", - `${this.exchangeConfig.currency}:0.01`, - `${this.exchangeConfig.currency}:0.01`, - "upload", - ], - ); + 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", + ], + ); + } } } @@ -1451,10 +1481,10 @@ export async function runTestWithState( let status: TestStatus; const handleSignal = (s: string) => { - gc.shutdownSync(); console.warn( `**** received fatal proces event, terminating test ${testName}`, ); + gc.shutdownSync(); process.exit(1); }; diff --git a/packages/taler-wallet-cli/src/integrationtests/libeufin.ts b/packages/taler-wallet-cli/src/integrationtests/libeufin.ts new file mode 100644 index 000000000..cb739f52d --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/libeufin.ts @@ -0,0 +1,442 @@ +/* + 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 "taler-wallet-core"; +import { + GlobalTestState, + pingProc, + ProcessWrapper, + runCommand, +} from "./harness"; + +export interface LibeufinSandboxServiceInterface { + baseUrl: string; +} + +export interface LibeufinNexusServiceInterface { + baseUrl: string; +} + +export interface LibeufinSandboxConfig { + httpPort: number; + databaseJdbcUri: string; +} + +export interface LibeufinNexusConfig { + httpPort: number; + databaseJdbcUri: 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> { + this.sandboxProc = this.globalTestState.spawnService( + "libeufin-sandbox", + [ + "serve", + "--port", + `${this.sandboxConfig.httpPort}`, + "--db-conn-string", + this.sandboxConfig.databaseJdbcUri, + ], + "libeufin-sandbox", + ); + } + + async pingUntilAvailable(): Promise<void> { + const url = `${this.baseUrl}config`; + 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", + "--db-conn-string", + this.nexusConfig.databaseJdbcUri, + ], + ); + + this.nexusProc = this.globalTestState.spawnService( + "libeufin-nexus", + [ + "serve", + "--port", + `${this.nexusConfig.httpPort}`, + "--db-conn-string", + this.nexusConfig.databaseJdbcUri, + ], + "libeufin-nexus", + ); + } + + async pingUntilAvailable(): Promise<void> { + const url = `${this.baseUrl}config`; + await pingProc(this.nexusProc, url, "libeufin-nexus"); + } +} + +export interface CreateEbicsSubscriberRequest { + hostID: string; + userID: string; + partnerID: string; + systemID?: 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; + currency: string; +} + +export namespace LibeufinSandboxApi { + 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", + }); + } + + 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); + } + + 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); + } + + 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); + } + + 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); + return res.data as SandboxAccountTransactions; + } +} + +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 CreateTalerWireGatewayFacadeRequest { + name: string; + connectionName: string; + accountName: string; + currency: string; + reserveTransferLevel: "report" | "statement" | "notification"; +} + +export namespace LibeufinNexusApi { + 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 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 fetchAllTransactions( + libeufinNexusService: LibeufinNexusService, + accountName: string, + ): Promise<void> { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `/bank-accounts/${accountName}/fetch-transactions`, + baseUrl, + ); + await axios.post( + url.href, + { + rangeType: "all", + level: "report", + }, + { + 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", + }, + }, + ); + } +} diff --git a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts new file mode 100644 index 000000000..39980dac9 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts @@ -0,0 +1,293 @@ +/* + 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 "taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "./denomStructures"; +import { + BankService, + DbInfo, + delayMs, + ExchangeBankAccount, + ExchangeService, + GlobalTestState, + MerchantService, + setupDb, + WalletCli, +} from "./harness"; +import { makeTestPayment } from "./helpers"; +import { + LibeufinNexusApi, + LibeufinNexusService, + LibeufinSandboxApi, + LibeufinSandboxService, +} from "./libeufin"; + +const exchangeIban = "DE71500105179674997361"; +const customerIban = "DE84500105176881385584"; +const customerBic = "BELADEBEXXX"; +const merchantIban = "DE42500105171245624648"; + +export interface LibeufinTestEnvironment { + commonDb: DbInfo; + exchange: ExchangeService; + exchangeBankAccount: ExchangeBankAccount; + 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: "COBADEFXXX", + 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", + }); + + 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: ExchangeBankAccount = { + accountName: "twg-user", + accountPassword: "123", + 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); + + let wresp: CoreApiResponse; + + // FIXME: add nicer api in the harness wallet for this. + wresp = await wallet.apiRequest("addExchange", { + exchangeBaseUrl: exchange.baseUrl, + }); + + t.assertTrue(wresp.type === "response"); + + // FIXME: add nicer api in the harness wallet for this. + wresp = await wallet.apiRequest("acceptManualWithdrawal", { + exchangeBaseUrl: exchange.baseUrl, + amount: "EUR:10", + }); + + t.assertTrue(wresp.type === "response"); + + const reservePub: string = (wresp.result as any).reservePub; + + await LibeufinSandboxApi.simulateIncomingTransaction( + libeufinSandbox, + "exchangeacct", + { + amount: "15.00", + currency: "EUR", + debtorBic: customerBic, + debtorIban: customerIban, + debtorName: "Jane Customer", + subject: `Taler Top-up ${reservePub}`, + }, + ); + + await LibeufinNexusApi.fetchAllTransactions(libeufinNexus, "myacct"); + + await exchange.runWirewatchOnce(); + + await wallet.runUntilDone(); + + const bal = await wallet.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, + ); +} diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts index 2acec0627..04e803b74 100644 --- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts +++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { GlobalTestState, runTestWithState, TestRunResult } from "./harness"; +import { GlobalTestState, runTestWithState, shouldLingerInTest, TestRunResult } from "./harness"; import { runPaymentTest } from "./test-payment"; import * as fs from "fs"; import * as path from "path"; @@ -48,6 +48,7 @@ import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank"; import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated"; import M from "minimatch"; import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion"; +import { runLibeufinBasicTest } from "./test-libeufin-basic"; /** * Test runner. @@ -65,6 +66,8 @@ const allTests: TestMainFunction[] = [ runClaimLoopTest, runExchangeManagementTest, runFeeRegressionTest, + runLibeufinBasicTest, + runMerchantExchangeConfusionTest, runMerchantLongpollingTest, runMerchantRefundApiTest, runPayAbortTest, @@ -81,14 +84,13 @@ const allTests: TestMainFunction[] = [ runRefundIncrementalTest, runRefundTest, runRevocationTest, + runTestWithdrawalManualTest, runTimetravelAutorefreshTest, runTimetravelWithdrawTest, runTippingTest, runWallettestingTest, - runTestWithdrawalManualTest, runWithdrawalAbortBankTest, runWithdrawalBankIntegratedTest, - runMerchantExchangeConfusionTest, ]; export interface TestRunSpec { @@ -301,6 +303,10 @@ if (runTestInstrStr && process.argv.includes("__TWCLI_TESTWORKER")) { runTest() .then(() => { console.log(`test ${testName} finished in worker`); + if (shouldLingerInTest()) { + console.log("lingering ..."); + return; + } process.exit(0); }) .catch((e) => { |