diff options
| author | Florian Dold <florian@dold.me> | 2021-01-17 01:18:37 +0100 | 
|---|---|---|
| committer | Florian Dold <florian@dold.me> | 2021-01-17 01:18:37 +0100 | 
| commit | 9aa9742d0e909609f9ce22bc1db8364ab7076db8 (patch) | |
| tree | b3247f0a50e4961d38ae302a344920488eeb0c27 | |
| parent | 94431fc6d2ad0f003dd12c100b1c7a53980f72f3 (diff) | |
implement the big LibEuFin integration test
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) => {  | 
