integration testing tweaks, rerun-payment-multiple scenario

This commit is contained in:
Florian Dold 2020-08-07 23:06:52 +05:30
parent 4525942777
commit 3321e40bff
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 316 additions and 91 deletions

View File

@ -46,6 +46,7 @@ import {
PostOrderRequest,
PostOrderResponse,
} from "./merchantApiTypes";
import { EddsaKeyPair } from "taler-wallet-core/lib/crypto/talerCrypto";
const exec = util.promisify(require("child_process").exec);
@ -77,7 +78,6 @@ export async function sh(
shell: true,
});
proc.stdout.on("data", (x) => {
console.log("child process got data chunk");
if (x instanceof Buffer) {
stdoutChunks.push(x);
} else {
@ -363,8 +363,6 @@ export interface BankConfig {
currency: string;
httpPort: number;
database: string;
suggestedExchange: string | undefined;
suggestedExchangePayto: string | undefined;
allowRegistrations: boolean;
}
@ -397,8 +395,48 @@ function setCoin(config: Configuration, c: CoinConfig) {
config.setString(s, "rsa_keysize", `${c.rsaKeySize}`);
}
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) {
console.log(`service ${serviceName} not ready:`, e.toString());
await delay(1000);
}
if (!proc || proc.proc.exitCode !== null) {
throw Error(`service process ${serviceName} stopped unexpectedly`);
}
}
}
export class BankService {
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,
@ -414,21 +452,17 @@ export class BankService {
"allow_registrations",
bc.allowRegistrations ? "yes" : "no",
);
if (bc.suggestedExchange) {
config.setString("bank", "suggested_exchange", bc.suggestedExchange);
}
if (bc.suggestedExchangePayto) {
config.setString(
"bank",
"suggested_exchange_payto",
bc.suggestedExchangePayto,
);
}
const cfgFilename = gc.testDir + "/bank.conf";
config.write(cfgFilename);
return new BankService(gc, bc, cfgFilename);
}
setSuggestedExchange(e: ExchangeService, exchangePayto: string) {
const config = Configuration.load(this.configFile);
config.setString("bank", "suggested_exchange", e.baseUrl);
config.setString("bank", "suggested_exchange_payto", exchangePayto);
}
get port() {
return this.bankConfig.httpPort;
}
@ -449,16 +483,7 @@ export class BankService {
async pingUntilAvailable(): Promise<void> {
const url = `http://localhost:${this.bankConfig.httpPort}/config`;
while (true) {
try {
console.log("pinging bank");
const resp = await axios.get(url);
return;
} catch (e) {
console.log("bank not ready:", e.toString());
await delay(1000);
}
}
await pingProc(this.proc, url, "bank");
}
async createAccount(username: string, password: string): Promise<void> {
@ -546,7 +571,6 @@ export interface ExchangeConfig {
roundUnit?: string;
httpPort: number;
database: string;
coinConfig?: ((curr: string) => CoinConfig)[];
}
export interface ExchangeServiceInterface {
@ -557,6 +581,27 @@ export interface ExchangeServiceInterface {
}
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: talerCrypto.eddsaGetPublic(eddsaPriv),
};
return new ExchangeService(gc, ec, cfgFilename, keyPair);
}
static create(gc: GlobalTestState, e: ExchangeConfig) {
const config = new Configuration();
config.setString("taler", "currency", e.currency);
@ -586,7 +631,6 @@ export class ExchangeService implements ExchangeServiceInterface {
);
config.setString("exchange", "serve", "tcp");
config.setString("exchange", "port", `${e.httpPort}`);
config.setString("exchange", "port", `${e.httpPort}`);
config.setString("exchange", "signkey_duration", "4 weeks");
config.setString("exchange", "legal_duraction", "2 years");
config.setString("exchange", "lookahead_sign", "32 weeks 1 day");
@ -607,10 +651,6 @@ export class ExchangeService implements ExchangeServiceInterface {
config.setString("exchangedb-postgres", "config", e.database);
const coinConfig = e.coinConfig ?? defaultCoinConfig;
coinConfig.forEach((cc) => setCoin(config, cc(e.currency)));
const exchangeMasterKey = talerCrypto.createEddsaKeyPair();
config.setString(
@ -632,6 +672,14 @@ export class ExchangeService implements ExchangeServiceInterface {
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);
}
get masterPub() {
return talerCrypto.encodeCrock(this.keyPair.eddsaPub);
}
@ -713,16 +761,7 @@ export class ExchangeService implements ExchangeServiceInterface {
async pingUntilAvailable(): Promise<void> {
const url = `http://localhost:${this.exchangeConfig.httpPort}/keys`;
while (true) {
try {
console.log("pinging exchange");
const resp = await axios.get(url);
return;
} catch (e) {
console.log("exchange not ready:", e.toString());
await delay(1000);
}
}
await pingProc(this.exchangeHttpProc, url, `exchange (${this.name})`);
}
}
@ -734,6 +773,18 @@ export interface MerchantConfig {
}
export class MerchantService {
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(
@ -844,16 +895,7 @@ export class MerchantService {
async pingUntilAvailable(): Promise<void> {
const url = `http://localhost:${this.merchantConfig.httpPort}/config`;
while (true) {
try {
console.log("pinging merchant");
const resp = await axios.get(url);
return;
} catch (e) {
console.log("merchant not ready", e.toString());
await delay(1000);
}
}
await pingProc(this.proc, url, `merchant (${this.merchantConfig.name})`);
}
}
@ -903,16 +945,13 @@ function updateCurrentSymlink(testDir: string): void {
}
}
export function runTest(testMain: (gc: GlobalTestState) => Promise<void>) {
export function runTestWithState(
gc: GlobalTestState,
testMain: (t: GlobalTestState) => Promise<void>,
) {
const main = async () => {
let gc: GlobalTestState | undefined;
let ret = 0;
try {
gc = new GlobalTestState({
testDir: fs.mkdtempSync(
path.join(os.tmpdir(), "taler-integrationtest-"),
),
});
updateCurrentSymlink(gc.testDir);
console.log("running test in directory", gc.testDir);
await testMain(gc);
@ -936,6 +975,15 @@ export function runTest(testMain: (gc: GlobalTestState) => Promise<void>) {
main();
}
export function runTest(
testMain: (gc: GlobalTestState) => Promise<void>,
): void {
const gc = new GlobalTestState({
testDir: fs.mkdtempSync(path.join(os.tmpdir(), "taler-integrationtest-")),
});
runTestWithState(gc, testMain);
}
function shellWrap(s: string) {
return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'";
}

View File

@ -31,6 +31,7 @@ import {
MerchantService,
setupDb,
BankService,
defaultCoinConfig,
} from "./harness";
import { AmountString } from "taler-wallet-core/lib/types/talerTypes";
@ -56,14 +57,8 @@ export async function createSimpleTestkudosEnvironment(
currency: "TESTKUDOS",
database: db.connStr,
httpPort: 8082,
suggestedExchange: "http://localhost:8081/",
suggestedExchangePayto: "payto://x-taler-bank/MyExchange",
});
await bank.start();
await bank.pingUntilAvailable();
const exchange = ExchangeService.create(t, {
name: "testexchange-1",
currency: "TESTKUDOS",
@ -71,11 +66,6 @@ export async function createSimpleTestkudosEnvironment(
database: db.connStr,
});
await exchange.setupTestBankAccount(bank, "1", "MyExchange", "x");
await exchange.start();
await exchange.pingUntilAvailable();
const merchant = await MerchantService.create(t, {
name: "testmerchant-1",
currency: "TESTKUDOS",
@ -83,6 +73,18 @@ export async function createSimpleTestkudosEnvironment(
database: db.connStr,
});
bank.setSuggestedExchange(exchange, "payto://x-taler-bank/MyExchange");
await bank.start();
await bank.pingUntilAvailable();
await exchange.setupTestBankAccount(bank, "1", "MyExchange", "x");
exchange.addOfferedCoins(defaultCoinConfig);
await exchange.start();
await exchange.pingUntilAvailable();
merchant.addExchange(exchange);
await merchant.start();

View File

@ -0,0 +1,109 @@
/*
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,
BankService,
ExchangeService,
MerchantService,
WalletCli,
runTestWithState,
} from "./harness";
import { withdrawViaBank } from "./helpers";
import fs from "fs";
let existingTestDir =
process.env["TALER_TEST_OLD_DIR"] ?? "/tmp/taler-integrationtest-current";
if (!fs.existsSync(existingTestDir)) {
throw Error("old test dir not found");
}
existingTestDir = fs.realpathSync(existingTestDir);
const prevT = new GlobalTestState({
testDir: existingTestDir,
});
/**
* Run test.
*/
runTestWithState(prevT, async (t: GlobalTestState) => {
// Set up test environment
const bank = BankService.fromExistingConfig(t);
const exchange = ExchangeService.fromExistingConfig(t, "testexchange-1");
const merchant = MerchantService.fromExistingConfig(t, "testmerchant-1");
await bank.start();
await exchange.start();
await merchant.start();
await Promise.all([
bank.pingUntilAvailable(),
merchant.pingUntilAvailable(),
exchange.pingUntilAvailable(),
]);
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 merchant.createOrder("default", {
order: {
summary: "Buy me!",
amount: "TESTKUDOS:80",
fulfillment_url: "taler://fulfillment-success/thx",
},
});
let orderStatus = await merchant.queryPrivateOrderStatus(
"default",
orderResp.order_id,
);
t.assertTrue(orderStatus.order_status === "unpaid");
// Make wallet pay for the order
const r1 = await wallet.apiRequest("preparePay", {
talerPayUri: orderStatus.taler_pay_uri,
});
t.assertTrue(r1.type === "response");
const r2 = await wallet.apiRequest("confirmPay", {
// FIXME: should be validated, don't cast!
proposalId: (r1.result as any).proposalId,
});
t.assertTrue(r2.type === "response");
// Check if payment was successful.
orderStatus = await merchant.queryPrivateOrderStatus(
"default",
orderResp.order_id,
);
t.assertTrue(orderStatus.order_status === "paid");
await t.shutdown();
});

View File

@ -29,6 +29,7 @@ import {
setupDb,
BankService,
WalletCli,
defaultCoinConfig,
} from "./harness";
import { FaultInjectedExchangeService, FaultInjectionRequestContext, FaultInjectionResponseContext } from "./faultInjection";
import { CoreApiResponse } from "taler-wallet-core/lib/walletCoreApiHandler";
@ -46,14 +47,8 @@ runTest(async (t: GlobalTestState) => {
currency: "TESTKUDOS",
database: db.connStr,
httpPort: 8082,
suggestedExchange: "http://localhost:8091/",
suggestedExchangePayto: "payto://x-taler-bank/MyExchange",
});
await bank.start();
await bank.pingUntilAvailable();
const exchange = ExchangeService.create(t, {
name: "testexchange-1",
currency: "TESTKUDOS",
@ -61,7 +56,14 @@ runTest(async (t: GlobalTestState) => {
database: db.connStr,
});
bank.setSuggestedExchange(exchange, "payto://x-taler-bank/MyExchange");
await bank.start();
await bank.pingUntilAvailable();
await exchange.setupTestBankAccount(bank, "1", "MyExchange", "x");
exchange.addOfferedCoins(defaultCoinConfig);
await exchange.start();
await exchange.pingUntilAvailable();

View File

@ -28,16 +28,13 @@ import {
coin_ct10,
coin_u1,
} from "./harness";
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
/**
* Run test.
*
* This test uses a very sub-optimal denomination structure.
*/
runTest(async (t: GlobalTestState) => {
// Set up test environment
import { withdrawViaBank } from "./helpers";
async function setupTest(t: GlobalTestState): Promise<{
merchant: MerchantService,
exchange: ExchangeService,
bank: BankService,
}> {
const db = await setupDb(t);
const bank = await BankService.create(t, {
@ -45,22 +42,23 @@ runTest(async (t: GlobalTestState) => {
currency: "TESTKUDOS",
database: db.connStr,
httpPort: 8082,
suggestedExchange: "http://localhost:8081/",
suggestedExchangePayto: "payto://x-taler-bank/MyExchange",
});
await bank.start();
await bank.pingUntilAvailable();
const exchange = ExchangeService.create(t, {
name: "testexchange-1",
currency: "TESTKUDOS",
httpPort: 8081,
database: db.connStr,
coinConfig: [coin_ct10, coin_u1],
});
exchange.addOfferedCoins([coin_ct10, coin_u1]);
bank.setSuggestedExchange(exchange, "payto://x-taler-bank/MyExchange");
await bank.start();
await bank.pingUntilAvailable();
await exchange.setupTestBankAccount(bank, "1", "MyExchange", "x");
await exchange.start();
@ -92,6 +90,23 @@ runTest(async (t: GlobalTestState) => {
console.log("setup done!");
return {
merchant,
bank,
exchange,
}
}
/**
* Run test.
*
* This test uses a very sub-optimal denomination structure.
*/
runTest(async (t: GlobalTestState) => {
// Set up test environment
const { merchant, bank, exchange } = await setupTest(t);
const wallet = new WalletCli(t);
// Withdraw digital cash into the wallet.

View File

@ -26,7 +26,6 @@
import { AmountJson } from "./amounts";
import * as Amounts from "./amounts";
import fs from "fs";
import { acceptExchangeTermsOfService } from "../operations/exchanges";
export class ConfigError extends Error {
constructor(message: string) {
@ -56,6 +55,26 @@ export class ConfigValue<T> {
}
return this.converter(this.val);
}
orUndefined(): T | undefined {
if (this.val !== undefined) {
return this.converter(this.val);
} else {
return undefined;
}
}
orDefault(v: T): T | undefined {
if (this.val !== undefined) {
return this.converter(this.val);
} else {
return v;
}
}
isDefined(): boolean {
return this.val !== undefined;
}
}
/**
@ -197,7 +216,7 @@ export class Configuration {
getString(section: string, option: string): ConfigValue<string> {
const secNorm = section.toUpperCase();
const optNorm = option.toUpperCase();
const val = (this.sectionMap[section] ?? {})[optNorm];
const val = (this.sectionMap[secNorm] ?? {})[optNorm];
return new ConfigValue(secNorm, optNorm, val, (x) => x);
}
@ -210,6 +229,36 @@ export class Configuration {
);
}
getYesNo(section: string, option: string): ConfigValue<boolean> {
const secNorm = section.toUpperCase();
const optNorm = option.toUpperCase();
const val = (this.sectionMap[secNorm] ?? {})[optNorm];
const convert = (x: string): boolean => {
x = x.toLowerCase();
if (x === "yes") {
return true;
} else if (x === "no") {
return false;
}
throw Error(`invalid config value for [${secNorm}]/${optNorm}, expected yes/no`);
};
return new ConfigValue(secNorm, optNorm, val, convert);
}
getNumber(section: string, option: string): ConfigValue<number> {
const secNorm = section.toUpperCase();
const optNorm = option.toUpperCase();
const val = (this.sectionMap[secNorm] ?? {})[optNorm];
const convert = (x: string): number => {
try {
return Number.parseInt(x, 10);
} catch (e) {
throw Error(`invalid config value for [${secNorm}]/${optNorm}, expected number`);
}
};
return new ConfigValue(secNorm, optNorm, val, convert);
}
lookupVariable(x: string, depth: number = 0): string | undefined {
// We loop up options in PATHS in upper case, as option names
// are case insensitive