implement the big LibEuFin integration test

This commit is contained in:
Florian Dold 2021-01-17 01:18:37 +01:00
parent 94431fc6d2
commit 9aa9742d0e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
4 changed files with 794 additions and 23 deletions

View File

@ -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);
};

View File

@ -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",
},
},
);
}
}

View File

@ -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,
);
}

View File

@ -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) => {