add exchange management test case

This commit is contained in:
Florian Dold 2020-08-12 19:45:34 +05:30
parent 11fa339705
commit c5ec341368
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
7 changed files with 306 additions and 4 deletions

View File

@ -182,7 +182,7 @@ export class FaultProxy {
this.currentFaultSpecs.push(f); this.currentFaultSpecs.push(f);
} }
clearFault() { clearAllFaults() {
this.currentFaultSpecs = []; this.currentFaultSpecs = [];
} }
} }

View File

@ -44,6 +44,12 @@ import {
codecForPreparePayResultPaymentPossible, codecForPreparePayResultPaymentPossible,
codecForPreparePayResult, codecForPreparePayResult,
OperationFailedError, OperationFailedError,
AddExchangeRequest,
ExchangesListRespose,
codecForExchangesListResponse,
GetWithdrawalDetailsForUriRequest,
WithdrawUriInfoResponse,
codecForWithdrawUriInfoResponse,
} from "taler-wallet-core"; } from "taler-wallet-core";
import { URL } from "url"; import { URL } from "url";
import axios from "axios"; import axios from "axios";
@ -60,6 +66,7 @@ import {
eddsaGetPublic, eddsaGetPublic,
createEddsaKeyPair, createEddsaKeyPair,
} from "taler-wallet-core/lib/crypto/talerCrypto"; } from "taler-wallet-core/lib/crypto/talerCrypto";
import { WithdrawalDetails } from "taler-wallet-core/lib/types/transactions";
const exec = util.promisify(require("child_process").exec); const exec = util.promisify(require("child_process").exec);
@ -244,6 +251,22 @@ export class GlobalTestState {
process.on("uncaughtException", () => this.shutdownSync()); process.on("uncaughtException", () => this.shutdownSync());
} }
async assertThrowsOperationErrorAsync(
block: () => Promise<void>,
): Promise<OperationFailedError> {
try {
await block();
} catch (e) {
if (e instanceof OperationFailedError) {
return e;
}
throw Error(`expected OperationFailedError to be thrown, but got ${e}`);
}
throw Error(
`expected OperationFailedError to be thrown, but block finished without throwing`,
);
}
assertTrue(b: boolean): asserts b { assertTrue(b: boolean): asserts b {
if (!b) { if (!b) {
throw Error("test assertion failed"); throw Error("test assertion failed");
@ -488,7 +511,7 @@ export class BankService {
return new BankService(gc, bc, cfgFilename); return new BankService(gc, bc, cfgFilename);
} }
setSuggestedExchange(e: ExchangeService, exchangePayto: string) { setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) {
const config = Configuration.load(this.configFile); const config = Configuration.load(this.configFile);
config.setString("bank", "suggested_exchange", e.baseUrl); config.setString("bank", "suggested_exchange", e.baseUrl);
config.setString("bank", "suggested_exchange_payto", exchangePayto); config.setString("bank", "suggested_exchange_payto", exchangePayto);
@ -1153,4 +1176,30 @@ export class WalletCli {
} }
throw new OperationFailedError(resp.error); throw new OperationFailedError(resp.error);
} }
async addExchange(req: AddExchangeRequest): Promise<void> {
const resp = await this.apiRequest("addExchange", req);
if (resp.type === "response") {
return;
}
throw new OperationFailedError(resp.error);
}
async listExchanges(): Promise<ExchangesListRespose> {
const resp = await this.apiRequest("listExchanges", {});
if (resp.type === "response") {
return codecForExchangesListResponse().decode(resp.result);
}
throw new OperationFailedError(resp.error);
}
async getWithdrawalDetailsForUri(
req: GetWithdrawalDetailsForUriRequest,
): Promise<WithdrawUriInfoResponse> {
const resp = await this.apiRequest("getWithdrawalDetailsForUri", req);
if (resp.type === "response") {
return codecForWithdrawUriInfoResponse().decode(resp.result);
}
throw new OperationFailedError(resp.error);
}
} }

View File

@ -0,0 +1,232 @@
/*
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 {
runTest,
GlobalTestState,
WalletCli,
setupDb,
BankService,
ExchangeService,
MerchantService,
defaultCoinConfig,
} from "./harness";
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers";
import {
PreparePayResultType,
ExchangesListRespose,
URL,
} from "taler-wallet-core";
import {
FaultInjectedExchangeService,
FaultInjectionResponseContext,
} from "./faultInjection";
/**
* Test if the wallet handles outdated exchange versions correct.y
*/
runTest(async (t: GlobalTestState) => {
// Set up test environment
const db = await setupDb(t);
const bank = await BankService.create(t, {
allowRegistrations: true,
currency: "TESTKUDOS",
database: db.connStr,
httpPort: 8082,
});
const exchange = ExchangeService.create(t, {
name: "testexchange-1",
currency: "TESTKUDOS",
httpPort: 8081,
database: db.connStr,
});
const merchant = await MerchantService.create(t, {
name: "testmerchant-1",
currency: "TESTKUDOS",
httpPort: 8083,
database: db.connStr,
});
const exchangeBankAccount = await bank.createExchangeAccount(
"MyExchange",
"x",
);
exchange.addBankAccount("1", exchangeBankAccount);
const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091);
bank.setSuggestedExchange(
faultyExchange,
exchangeBankAccount.accountPaytoUri,
);
await bank.start();
await bank.pingUntilAvailable();
exchange.addOfferedCoins(defaultCoinConfig);
await exchange.start();
await exchange.pingUntilAvailable();
merchant.addExchange(exchange);
await merchant.start();
await merchant.pingUntilAvailable();
await merchant.addInstance({
id: "minst1",
name: "minst1",
paytoUris: ["payto://x-taler-bank/minst1"],
});
await merchant.addInstance({
id: "default",
name: "Default Instance",
paytoUris: [`payto://x-taler-bank/merchant-default`],
});
console.log("setup done!");
/*
* =========================================================================
* Check that the exchange can be added to the wallet
* (without any faults active).
* =========================================================================
*/
const wallet = new WalletCli(t);
let exchangesList: ExchangesListRespose;
exchangesList = await wallet.listExchanges();
t.assertTrue(exchangesList.exchanges.length === 0);
// Try before fault is injected
await wallet.addExchange({
exchangeBaseUrl: faultyExchange.baseUrl,
});
exchangesList = await wallet.listExchanges();
t.assertTrue(exchangesList.exchanges.length === 1);
await wallet.addExchange({
exchangeBaseUrl: faultyExchange.baseUrl,
});
console.log("listing exchanges");
exchangesList = await wallet.listExchanges();
t.assertTrue(exchangesList.exchanges.length === 1);
console.log("got list", exchangesList);
/*
* =========================================================================
* Check what happens if the exchange returns something totally
* bogus for /keys.
* =========================================================================
*/
wallet.deleteDatabase();
exchangesList = await wallet.listExchanges();
t.assertTrue(exchangesList.exchanges.length === 0);
faultyExchange.faultProxy.addFault({
modifyResponse(ctx: FaultInjectionResponseContext) {
const url = new URL(ctx.request.requestUrl);
if (url.pathname === "/keys") {
const body = {
version: "whaaat",
};
ctx.responseBody = Buffer.from(JSON.stringify(body), "utf-8");
}
},
});
await t.assertThrowsOperationErrorAsync(async () => {
await wallet.addExchange({
exchangeBaseUrl: faultyExchange.baseUrl,
});
});
exchangesList = await wallet.listExchanges();
t.assertTrue(exchangesList.exchanges.length === 0);
/*
* =========================================================================
* Check what happens if the exchange returns an old, unsupported
* version for /keys
* =========================================================================
*/
wallet.deleteDatabase();
faultyExchange.faultProxy.clearAllFaults();
faultyExchange.faultProxy.addFault({
modifyResponse(ctx: FaultInjectionResponseContext) {
const url = new URL(ctx.request.requestUrl);
if (url.pathname === "/keys") {
const keys = ctx.responseBody?.toString("utf-8");
t.assertTrue(keys != null);
const keysJson = JSON.parse(keys);
keysJson["version"] = "2:0:0";
ctx.responseBody = Buffer.from(JSON.stringify(keysJson), "utf-8");
}
},
});
await t.assertThrowsOperationErrorAsync(async () => {
await wallet.addExchange({
exchangeBaseUrl: faultyExchange.baseUrl,
});
});
exchangesList = await wallet.listExchanges();
t.assertTrue(exchangesList.exchanges.length === 0);
/*
* =========================================================================
* Check that the exchange version is also checked when
* the exchange is implicitly added via the suggested
* exchange of a bank-integrated withdrawal.
* =========================================================================
*/
// Fault from above is still active!
// Create withdrawal operation
const user = await bank.createRandomBankUser();
const wop = await bank.createWithdrawalOperation(user, "TESTKUDOS:10");
// Hand it to the wallet
const wd = await wallet.getWithdrawalDetailsForUri({
talerWithdrawUri: wop.taler_withdraw_uri,
});
// Make sure the faulty exchange isn't used for the suggestion.
t.assertTrue(wd.possibleExchanges.length === 0);
});

View File

@ -75,3 +75,5 @@ export * from "./crypto/workers/nodeThreadWorker";
export * from "./types/notifications"; export * from "./types/notifications";
export { Configuration } from "./util/talerconfig"; export { Configuration } from "./util/talerconfig";
export { URL } from "./util/url";

View File

@ -88,7 +88,7 @@ async function setExchangeError(
baseUrl: string, baseUrl: string,
err: OperationErrorDetails, err: OperationErrorDetails,
): Promise<void> { ): Promise<void> {
console.log(`last error for exchange ${baseUrl}:`, err); logger.warn(`last error for exchange ${baseUrl}:`, err);
const mut = (exchange: ExchangeRecord): ExchangeRecord => { const mut = (exchange: ExchangeRecord): ExchangeRecord => {
exchange.lastError = err; exchange.lastError = err;
return exchange; return exchange;

View File

@ -47,7 +47,7 @@ import {
Duration, Duration,
codecForDuration, codecForDuration,
} from "../util/time"; } from "../util/time";
import { ExchangeListItem } from "./walletTypes"; import { ExchangeListItem, codecForExchangeListItem } from "./walletTypes";
import { codecForAmountString } from "../util/amounts"; import { codecForAmountString } from "../util/amounts";
/** /**
@ -940,6 +940,13 @@ export interface WithdrawUriInfoResponse {
possibleExchanges: ExchangeListItem[]; possibleExchanges: ExchangeListItem[];
} }
export const codecForWithdrawUriInfoResponse = (): Codec<WithdrawUriInfoResponse> =>
buildCodecForObject<WithdrawUriInfoResponse>()
.property("amount", codecForAmountString())
.property("defaultExchangeBaseUrl", codecOptional(codecForString()))
.property("possibleExchanges", codecForList(codecForExchangeListItem()))
.build("WithdrawUriInfoResponse");
/** /**
* Response body for the following endpoint: * Response body for the following endpoint:
* *

View File

@ -554,6 +554,18 @@ export interface ExchangeListItem {
paytoUris: string[]; paytoUris: string[];
} }
export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
buildCodecForObject<ExchangeListItem>()
.property("currency", codecForString())
.property("exchangeBaseUrl", codecForString())
.property("paytoUris", codecForList(codecForString()))
.build("ExchangeListItem");
export const codecForExchangesListResponse = (): Codec<ExchangesListRespose> =>
buildCodecForObject<ExchangesListRespose>()
.property("exchanges", codecForList(codecForExchangeListItem()))
.build("ExchangesListRespose");
export interface AcceptManualWithdrawalResult { export interface AcceptManualWithdrawalResult {
/** /**
* Payto URIs that can be used to fund the withdrawal. * Payto URIs that can be used to fund the withdrawal.