use wallet's http lib for test balance withdrawal, remove redundant integration tests

This commit is contained in:
Florian Dold 2020-08-01 13:52:08 +05:30
parent b37c98346d
commit aa481e4267
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
9 changed files with 36 additions and 951 deletions

View File

@ -1,143 +0,0 @@
/*
This file is part of GNU Taler
(C) 2019 GNUnet e.V.
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/>
*/
/**
* Helper functions to deal with the GNU Taler demo bank.
*
* Mostly useful for automated tests.
*/
/**
* Imports.
*/
import Axios from "axios";
export interface BankUser {
username: string;
password: string;
}
/**
* Generate a random alphanumeric ID. Does *not* use cryptographically
* secure randomness.
*/
function makeId(length: number): string {
let result = "";
const characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
/**
* Helper function to generate the "Authorization" HTTP header.
*/
function makeAuth(username: string, password: string): string {
const auth = `${username}:${password}`;
const authEncoded: string = Buffer.from(auth).toString("base64");
return `Basic ${authEncoded}`;
}
/**
* Client for the Taler bank access API.
*/
export class Bank {
constructor(private bankBaseUrl: string) {}
async generateWithdrawUri(
bankUser: BankUser,
amount: string,
): Promise<string> {
const body = {
amount,
};
const reqUrl = new URL("api/withdraw-headless-uri", this.bankBaseUrl).href;
const resp = await Axios({
method: "post",
url: reqUrl,
data: body,
responseType: "json",
headers: {
Authorization: makeAuth(bankUser.username, bankUser.password),
},
});
if (resp.status != 200) {
throw Error("failed to create bank reserve");
}
const withdrawUri = resp.data["taler_withdraw_uri"];
if (!withdrawUri) {
throw Error("Bank's response did not include withdraw URI");
}
return withdrawUri;
}
async createReserve(
bankUser: BankUser,
amount: string,
reservePub: string,
exchangePaytoUri: string,
): Promise<void> {
const reqUrl = new URL("testing/withdraw", this.bankBaseUrl).href;
const body = {
username: bankUser,
amount,
reserve_pub: reservePub,
exchange_payto_uri: exchangePaytoUri,
};
const resp = await Axios({
method: "post",
url: reqUrl,
data: body,
responseType: "json",
headers: {
Authorization: makeAuth(bankUser.username, bankUser.password),
},
});
if (resp.status != 200) {
throw Error("failed to create bank reserve");
}
}
async registerRandomUser(): Promise<BankUser> {
const reqUrl = new URL("testing/register", this.bankBaseUrl).href;
const randId = makeId(8);
const bankUser: BankUser = {
username: `testuser-${randId}`,
password: `testpw-${randId}`,
};
const resp = await Axios({
method: "post",
url: reqUrl,
data: bankUser,
responseType: "json",
});
if (resp.status != 200) {
throw Error("could not register bank user");
}
return bankUser;
}
}

View File

@ -26,18 +26,15 @@ import { Wallet } from "../wallet";
import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge"; import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge";
import { openTalerDatabase } from "../db"; import { openTalerDatabase } from "../db";
import { HttpRequestLibrary } from "../util/http"; import { HttpRequestLibrary } from "../util/http";
import { Bank } from "./bank";
import fs from "fs"; import fs from "fs";
import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorker"; import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorker";
import { WalletNotification, NotificationType } from "../types/notifications"; import { WalletNotification } from "../types/notifications";
import { Database } from "../util/query"; import { Database } from "../util/query";
import { NodeHttpLib } from "./NodeHttpLib"; import { NodeHttpLib } from "./NodeHttpLib";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker"; import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker";
import { WithdrawalSourceType } from "../types/dbTypes";
import { Amounts } from "../util/amounts";
const logger = new Logger("helpers.ts"); const logger = new Logger("headless/helpers.ts");
export interface DefaultNodeWalletArgs { export interface DefaultNodeWalletArgs {
/** /**
@ -135,46 +132,3 @@ export async function getDefaultNodeWallet(
} }
return w; return w;
} }
export async function withdrawTestBalance(
myWallet: Wallet,
amount = "TESTKUDOS:10",
bankBaseUrl = "https://bank.test.taler.net/",
exchangeBaseUrl = "https://exchange.test.taler.net/",
): Promise<void> {
await myWallet.updateExchangeFromUrl(exchangeBaseUrl, true);
const reserveResponse = await myWallet.acceptManualWithdrawal(
exchangeBaseUrl,
Amounts.parseOrThrow(amount),
);
const reservePub = reserveResponse.reservePub;
const bank = new Bank(bankBaseUrl);
const bankUser = await bank.registerRandomUser();
logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
const exchangePaytoUri = await myWallet.getExchangePaytoUri(exchangeBaseUrl, [
"x-taler-bank",
]);
const donePromise = new Promise((resolve, reject) => {
myWallet.runRetryLoop().catch((x) => {
reject(x);
});
myWallet.addNotificationListener((n) => {
if (
n.type === NotificationType.WithdrawGroupFinished &&
n.withdrawalSource.type === WithdrawalSourceType.Reserve &&
n.withdrawalSource.reservePub === reservePub
) {
resolve();
}
});
});
await bank.createReserve(bankUser, amount, reservePub, exchangePaytoUri);
await donePromise;
}

View File

@ -1,327 +0,0 @@
/*
This file is part of GNU Taler
(C) 2019 GNUnet e.V.
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/>
*/
/**
* Integration tests against real Taler bank/exchange/merchant deployments.
*/
import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers";
import { MerchantBackendConnection } from "./merchant";
import { Logger } from "../util/logging";
import { NodeHttpLib } from "./NodeHttpLib";
import { Wallet } from "../wallet";
import { Configuration } from "../util/talerconfig";
import { Amounts, AmountJson } from "../util/amounts";
const logger = new Logger("integrationtest.ts");
export interface IntegrationTestArgs {
exchangeBaseUrl: string;
bankBaseUrl: string;
merchantBaseUrl: string;
merchantApiKey: string;
amountToWithdraw: string;
amountToSpend: string;
}
async function makePayment(
wallet: Wallet,
merchant: MerchantBackendConnection,
amount: string,
summary: string,
): Promise<{ orderId: string }> {
const orderResp = await merchant.createOrder(
amount,
summary,
"taler://fulfillment-success/thx",
);
console.log("created order with orderId", orderResp.orderId);
let paymentStatus = await merchant.checkPayment(orderResp.orderId);
console.log("payment status", paymentStatus);
const talerPayUri = paymentStatus.taler_pay_uri;
if (!talerPayUri) {
throw Error("no taler://pay/ URI in payment response");
}
const preparePayResult = await wallet.preparePayForUri(talerPayUri);
console.log("prepare pay result", preparePayResult);
if (preparePayResult.status != "payment-possible") {
throw Error("payment not possible");
}
const confirmPayResult = await wallet.confirmPay(
preparePayResult.proposalId,
undefined,
);
console.log("confirmPayResult", confirmPayResult);
paymentStatus = await merchant.checkPayment(orderResp.orderId);
if (paymentStatus.order_status !== "paid") {
console.log("payment status:", paymentStatus);
throw Error("payment did not succeed");
}
return {
orderId: orderResp.orderId,
};
}
export async function runIntegrationTest(
args: IntegrationTestArgs,
): Promise<void> {
logger.info("running test with arguments", args);
const parsedSpendAmount = Amounts.parseOrThrow(args.amountToSpend);
const currency = parsedSpendAmount.currency;
const myHttpLib = new NodeHttpLib();
myHttpLib.setThrottling(false);
const myWallet = await getDefaultNodeWallet({ httpLib: myHttpLib });
myWallet.runRetryLoop().catch((e) => {
console.error("exception during retry loop:", e);
});
logger.info("withdrawing test balance");
await withdrawTestBalance(
myWallet,
args.amountToWithdraw,
args.bankBaseUrl,
args.exchangeBaseUrl,
);
logger.info("done withdrawing test balance");
const myMerchant = new MerchantBackendConnection(
args.merchantBaseUrl,
args.merchantApiKey,
);
await makePayment(myWallet, myMerchant, args.amountToSpend, "hello world");
// Wait until the refresh is done
await myWallet.runUntilDone();
console.log("withdrawing test balance for refund");
const withdrawAmountTwo: AmountJson = {
currency,
value: 18,
fraction: 0,
};
const spendAmountTwo: AmountJson = {
currency,
value: 7,
fraction: 0,
};
const refundAmount: AmountJson = {
currency,
value: 6,
fraction: 0,
};
const spendAmountThree: AmountJson = {
currency,
value: 3,
fraction: 0,
};
await withdrawTestBalance(
myWallet,
Amounts.stringify(withdrawAmountTwo),
args.bankBaseUrl,
args.exchangeBaseUrl,
);
// Wait until the withdraw is done
await myWallet.runUntilDone();
const { orderId: refundOrderId } = await makePayment(
myWallet,
myMerchant,
Amounts.stringify(spendAmountTwo),
"order that will be refunded",
);
const refundUri = await myMerchant.refund(
refundOrderId,
"test refund",
Amounts.stringify(refundAmount),
);
console.log("refund URI", refundUri);
await myWallet.applyRefund(refundUri);
// Wait until the refund is done
await myWallet.runUntilDone();
await makePayment(
myWallet,
myMerchant,
Amounts.stringify(spendAmountThree),
"payment after refund",
);
await myWallet.runUntilDone();
}
export async function runIntegrationTestBasic(
cfg: Configuration,
): Promise<void> {
const walletDbPath = cfg.getString("integrationtest", "walletdb").required();
const bankBaseUrl = cfg
.getString("integrationtest", "bank_base_url")
.required();
const exchangeBaseUrl = cfg
.getString("integrationtest", "exchange_base_url")
.required();
const merchantBaseUrl = cfg
.getString("integrationtest", "merchant_base_url")
.required();
const merchantApiKey = cfg
.getString("integrationtest", "merchant_api_key")
.required();
const parsedWithdrawAmount = cfg
.getAmount("integrationtest-basic", "amount_withdraw")
.required();
const parsedSpendAmount = cfg
.getAmount("integrationtest-basic", "amount_spend")
.required();
const currency = parsedSpendAmount.currency;
const myHttpLib = new NodeHttpLib();
myHttpLib.setThrottling(false);
const myWallet = await getDefaultNodeWallet({
httpLib: myHttpLib,
persistentStoragePath: walletDbPath,
});
myWallet.runRetryLoop().catch((e) => {
console.error("exception during retry loop:", e);
});
logger.info("withdrawing test balance");
await withdrawTestBalance(
myWallet,
Amounts.stringify(parsedWithdrawAmount),
bankBaseUrl,
exchangeBaseUrl,
);
logger.info("done withdrawing test balance");
const balance = await myWallet.getBalances();
console.log(JSON.stringify(balance, null, 2));
const myMerchant = new MerchantBackendConnection(
merchantBaseUrl,
merchantApiKey,
);
await makePayment(
myWallet,
myMerchant,
Amounts.stringify(parsedSpendAmount),
"hello world",
);
// Wait until the refresh is done
await myWallet.runUntilDone();
console.log("withdrawing test balance for refund");
const withdrawAmountTwo: AmountJson = {
currency,
value: 18,
fraction: 0,
};
const spendAmountTwo: AmountJson = {
currency,
value: 7,
fraction: 0,
};
const refundAmount: AmountJson = {
currency,
value: 6,
fraction: 0,
};
const spendAmountThree: AmountJson = {
currency,
value: 3,
fraction: 0,
};
await withdrawTestBalance(
myWallet,
Amounts.stringify(withdrawAmountTwo),
bankBaseUrl,
exchangeBaseUrl,
);
// Wait until the withdraw is done
await myWallet.runUntilDone();
const { orderId: refundOrderId } = await makePayment(
myWallet,
myMerchant,
Amounts.stringify(spendAmountTwo),
"order that will be refunded",
);
const refundUri = await myMerchant.refund(
refundOrderId,
"test refund",
Amounts.stringify(refundAmount),
);
console.log("refund URI", refundUri);
await myWallet.applyRefund(refundUri);
// Wait until the refund is done
await myWallet.runUntilDone();
await makePayment(
myWallet,
myMerchant,
Amounts.stringify(spendAmountThree),
"payment after refund",
);
await myWallet.runUntilDone();
console.log(
"history after integration test:",
JSON.stringify(history, undefined, 2),
);
}

View File

@ -1,145 +0,0 @@
/*
This file is part of GNU Taler
(C) 2019 GNUnet e.V.
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/>
*/
/**
* Helpers for talking to the GNU Taler merchant backend.
* Used mostly for integration tests.
*/
/**
* Imports.
*/
import axios from "axios";
import {
CheckPaymentResponse,
codecForCheckPaymentResponse,
} from "../types/talerTypes";
/**
* Connection to the *internal* merchant backend.
*/
export class MerchantBackendConnection {
async refund(
orderId: string,
reason: string,
refundAmount: string,
): Promise<string> {
const reqUrl = new URL(
`private/orders/${orderId}/refund`,
this.merchantBaseUrl,
);
const refundReq = {
reason,
refund: refundAmount,
};
const resp = await axios({
method: "post",
url: reqUrl.href,
data: refundReq,
responseType: "json",
headers: {
Authorization: `ApiKey ${this.apiKey}`,
},
});
if (resp.status != 200) {
throw Error("failed to do refund");
}
console.log("response", resp.data);
const refundUri = resp.data.taler_refund_uri;
if (!refundUri) {
throw Error("no refund URI in response");
}
return refundUri;
}
constructor(public merchantBaseUrl: string, public apiKey: string) {}
async authorizeTip(amount: string, justification: string): Promise<string> {
const reqUrl = new URL("private/tips", this.merchantBaseUrl).href;
const tipReq = {
amount,
justification,
next_url: "about:blank",
};
const resp = await axios({
method: "post",
url: reqUrl,
data: tipReq,
responseType: "json",
headers: {
Authorization: `ApiKey ${this.apiKey}`,
},
});
const tipUri = resp.data.taler_tip_uri;
if (!tipUri) {
throw Error("response does not contain tip URI");
}
return tipUri;
}
async createOrder(
amount: string,
summary: string,
fulfillmentUrl: string,
): Promise<{ orderId: string }> {
const t = Math.floor(new Date().getTime() / 1000) + 15 * 60;
const reqUrl = new URL("private/orders", this.merchantBaseUrl).href;
const orderReq = {
order: {
amount,
summary,
fulfillment_url: fulfillmentUrl,
refund_deadline: { t_ms: t * 1000 },
wire_transfer_deadline: { t_ms: t * 1000 },
},
};
const resp = await axios({
method: "post",
url: reqUrl,
data: orderReq,
responseType: "json",
headers: {
Authorization: `ApiKey ${this.apiKey}`,
},
});
if (resp.status != 200) {
throw Error("failed to create bank reserve");
}
const orderId = resp.data.order_id;
if (!orderId) {
throw Error("no order id in response");
}
return { orderId };
}
async checkPayment(orderId: string): Promise<CheckPaymentResponse> {
const reqUrl = new URL(`private/orders/${orderId}`, this.merchantBaseUrl)
.href;
const resp = await axios({
method: "get",
url: reqUrl,
responseType: "json",
headers: {
Authorization: `ApiKey ${this.apiKey}`,
},
});
if (resp.status != 200) {
throw Error("failed to check payment");
}
return codecForCheckPaymentResponse().decode(resp.data);
}
}

View File

@ -16,9 +16,7 @@
import os from "os"; import os from "os";
import fs from "fs"; import fs from "fs";
import { getDefaultNodeWallet, withdrawTestBalance } from "./helpers"; import { getDefaultNodeWallet } from "./helpers";
import { MerchantBackendConnection } from "./merchant";
import { runIntegrationTest, runIntegrationTestBasic } from "./integrationtest";
import { Wallet } from "../wallet"; import { Wallet } from "../wallet";
import qrcodeGenerator from "qrcode-generator"; import qrcodeGenerator from "qrcode-generator";
import * as clk from "./clk"; import * as clk from "./clk";
@ -34,7 +32,6 @@ import {
OperationFailedAndReportedError, OperationFailedAndReportedError,
OperationFailedError, OperationFailedError,
} from "../operations/errors"; } from "../operations/errors";
import { Bank } from "./bank";
import { classifyTalerUri, TalerUriType } from "../util/taleruri"; import { classifyTalerUri, TalerUriType } from "../util/taleruri";
import { Configuration } from "../util/talerconfig"; import { Configuration } from "../util/talerconfig";
import { setDangerousTimetravel } from "../util/time"; import { setDangerousTimetravel } from "../util/time";
@ -658,285 +655,4 @@ testCli.subcommand("vectors", "vectors").action(async (args) => {
console.log(` (out) coin pub: ${encodeCrock(p.coinPub)}`); console.log(` (out) coin pub: ${encodeCrock(p.coinPub)}`);
}); });
testCli
.subcommand("integrationtestBasic", "integrationtest-basic")
.requiredArgument("cfgfile", clk.STRING)
.action(async (args) => {
const cfgStr = fs.readFileSync(args.integrationtestBasic.cfgfile, "utf8");
const cfg = new Configuration();
cfg.loadFromString(cfgStr);
try {
await runIntegrationTestBasic(cfg);
} catch (e) {
console.log("integration test failed");
console.log(e);
process.exit(1);
}
process.exit(0);
});
testCli
.subcommand("testPayCmd", "test-pay", { help: "Create contract and pay." })
.requiredOption("merchant", ["-m", "--mechant-url"], clk.STRING)
.requiredOption("apikey", ["-k", "--mechant-api-key"], clk.STRING)
.requiredOption("amount", ["-a", "--amount"], clk.STRING)
.requiredOption("summary", ["-s", "--summary"], clk.STRING, {
default: "Test Payment",
})
.action(async (args) => {
const cmdArgs = args.testPayCmd;
console.log("creating order");
const merchantBackend = new MerchantBackendConnection(
args.testPayCmd.merchant,
args.testPayCmd.apikey,
);
const orderResp = await merchantBackend.createOrder(
cmdArgs.amount,
cmdArgs.summary,
"",
);
console.log("created new order with order ID", orderResp.orderId);
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
const talerPayUri = checkPayResp.taler_pay_uri;
if (!talerPayUri) {
console.error("fatal: no taler pay URI received from backend");
process.exit(1);
return;
}
console.log("taler pay URI:", talerPayUri);
await withWallet(args, async (wallet) => {
await doPay(wallet, talerPayUri, { alwaysYes: true });
});
});
testCli
.subcommand("integrationtestCmd", "integrationtest", {
help: "Run integration test with bank, exchange and merchant.",
})
.requiredOption("exchange", ["-e", "--exchange"], clk.STRING, {
default: "https://exchange.test.taler.net/",
})
.requiredOption("merchant", ["-m", "--merchant"], clk.STRING, {
default: "https://backend.test.taler.net/",
})
.requiredOption("merchantApiKey", ["-k", "--merchant-api-key"], clk.STRING, {
default: "sandbox",
})
.requiredOption("bank", ["-b", "--bank"], clk.STRING, {
default: "https://bank.test.taler.net/",
})
.requiredOption("withdrawAmount", ["-w", "--amount"], clk.STRING, {
default: "TESTKUDOS:10",
})
.requiredOption("spendAmount", ["-s", "--spend-amount"], clk.STRING, {
default: "TESTKUDOS:4",
})
.action(async (args) => {
applyVerbose(args.wallet.verbose);
const cmdObj = args.integrationtestCmd;
try {
await runIntegrationTest({
amountToSpend: cmdObj.spendAmount,
amountToWithdraw: cmdObj.withdrawAmount,
bankBaseUrl: cmdObj.bank,
exchangeBaseUrl: cmdObj.exchange,
merchantApiKey: cmdObj.merchantApiKey,
merchantBaseUrl: cmdObj.merchant,
}).catch((err) => {
console.error("Integration test failed with exception:");
console.error(err);
process.exit(1);
});
process.exit(0);
} catch (e) {
console.error(e);
process.exit(1);
}
});
testCli
.subcommand("genTipUri", "gen-tip-uri", {
help: "Generate a taler://tip URI.",
})
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:10",
})
.maybeOption("merchant", ["-m", "--merchant"], clk.STRING, {
default: "https://backend.test.taler.net/",
})
.maybeOption("merchantApiKey", ["-k", "--merchant-api-key"], clk.STRING, {
default: "sandbox",
})
.action(async (args) => {
const merchantBackend = new MerchantBackendConnection(
args.genTipUri.merchant ?? "https://backend.test.taler.net/",
args.genTipUri.merchantApiKey ?? "sandbox",
);
const tipUri = await merchantBackend.authorizeTip(
args.genTipUri.amount,
"test",
);
console.log(tipUri);
});
testCli
.subcommand("genWithdrawUri", "gen-withdraw-uri", {
help: "Generate a taler://withdraw URI.",
})
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:20",
})
.requiredOption("bank", ["-b", "--bank"], clk.STRING, {
default: "https://bank.test.taler.net/",
})
.action(async (args) => {
const b = new Bank(args.genWithdrawUri.bank);
const user = await b.registerRandomUser();
const url = await b.generateWithdrawUri(user, args.genWithdrawUri.amount);
console.log(url);
});
testCli
.subcommand("genRefundUri", "gen-refund-uri", {
help: "Generate a taler://refund URI.",
})
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:5",
})
.requiredOption("refundAmount", ["-r", "--refund"], clk.STRING, {
default: "TESTKUDOS:3",
})
.requiredOption("summary", ["-s", "--summary"], clk.STRING, {
default: "Test Payment (for refund)",
})
.maybeOption("merchant", ["-m", "--merchant"], clk.STRING, {
default: "https://backend.test.taler.net/",
})
.maybeOption("merchantApiKey", ["-k", "--merchant-api-key"], clk.STRING, {
default: "sandbox",
})
.action(async (args) => {
const cmdArgs = args.genRefundUri;
const merchantBackend = new MerchantBackendConnection(
cmdArgs.merchant ?? "https://backend.test.taler.net/",
cmdArgs.merchantApiKey ?? "sandbox",
);
const orderResp = await merchantBackend.createOrder(
cmdArgs.amount,
cmdArgs.summary,
"",
);
console.log("created new order with order ID", orderResp.orderId);
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
const talerPayUri = checkPayResp.taler_pay_uri;
if (!talerPayUri) {
console.error("fatal: no taler pay URI received from backend");
process.exit(1);
return;
}
await withWallet(args, async (wallet) => {
await doPay(wallet, talerPayUri, { alwaysYes: true });
});
const refundUri = await merchantBackend.refund(
orderResp.orderId,
"test refund",
cmdArgs.refundAmount,
);
console.log(refundUri);
});
testCli
.subcommand("genPayUri", "gen-pay-uri", {
help: "Generate a taler://pay URI.",
})
.flag("qrcode", ["--qr"], {
help: "Show a QR code with the taler://pay URI",
})
.flag("wait", ["--wait"], {
help: "Wait until payment has completed",
})
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:1",
})
.requiredOption("summary", ["-s", "--summary"], clk.STRING, {
default: "Test Payment",
})
.requiredOption("merchant", ["-m", "--merchant"], clk.STRING, {
default: "https://backend.test.taler.net/",
})
.requiredOption("merchantApiKey", ["-k", "--merchant-api-key"], clk.STRING, {
default: "sandbox",
})
.action(async (args) => {
const cmdArgs = args.genPayUri;
console.log("creating order");
const merchantBackend = new MerchantBackendConnection(
cmdArgs.merchant,
cmdArgs.merchantApiKey,
);
const orderResp = await merchantBackend.createOrder(
cmdArgs.amount,
cmdArgs.summary,
"",
);
console.log("created new order with order ID", orderResp.orderId);
const checkPayResp = await merchantBackend.checkPayment(orderResp.orderId);
const talerPayUri = checkPayResp.taler_pay_uri;
if (!talerPayUri) {
console.error("fatal: no taler pay URI received from backend");
process.exit(1);
return;
}
console.log("taler pay URI:", talerPayUri);
if (cmdArgs.qrcode) {
const qrcode = qrcodeGenerator(0, "M");
qrcode.addData(talerPayUri);
qrcode.make();
console.log(qrcode.createASCII());
}
if (cmdArgs.wait) {
console.log("waiting for payment ...");
while (1) {
await asyncSleep(500);
const checkPayResp2 = await merchantBackend.checkPayment(
orderResp.orderId,
);
if (checkPayResp2.order_status === "paid") {
console.log("payment successfully received!");
break;
}
}
}
});
testCli
.subcommand("withdrawArgs", "withdraw", {
help: "Withdraw from a test bank (must support test registrations).",
})
.requiredOption("amount", ["-a", "--amount"], clk.STRING, {
default: "TESTKUDOS:10",
help: "Amount to withdraw.",
})
.requiredOption("exchange", ["-e", "--exchange"], clk.STRING, {
default: "https://exchange.test.taler.net/",
help: "Exchange base URL.",
})
.requiredOption("bank", ["-b", "--bank"], clk.STRING, {
default: "https://bank.test.taler.net/",
help: "Bank base URL",
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.updateExchangeFromUrl(args.withdrawArgs.exchange, true);
await withdrawTestBalance(
wallet,
args.withdrawArgs.amount,
args.withdrawArgs.bank,
args.withdrawArgs.exchange,
);
logger.info("Withdraw done");
});
});
walletCli.run(); walletCli.run();

View File

@ -19,5 +19,4 @@
*/ */
export { Wallet } from "./wallet"; export { Wallet } from "./wallet";
export { runIntegrationTest } from "./headless/integrationtest";
export { installAndroidWalletListener } from "./android"; export { installAndroidWalletListener } from "./android";

View File

@ -300,6 +300,7 @@ export async function readSuccessResponseJsonOrThrow<T>(
throwUnexpectedRequestError(httpResponse, r.talerErrorResponse); throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
} }
export async function readSuccessResponseTextOrErrorCode<T>( export async function readSuccessResponseTextOrErrorCode<T>(
httpResponse: HttpResponse, httpResponse: HttpResponse,
): Promise<ResponseOrError<string>> { ): Promise<ResponseOrError<string>> {
@ -329,6 +330,27 @@ export async function readSuccessResponseTextOrErrorCode<T>(
}; };
} }
export async function checkSuccessResponseOrThrow(
httpResponse: HttpResponse,
): Promise<void> {
if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
const errJson = await httpResponse.json();
const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") {
throw new OperationFailedError(
makeErrorDetails(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
"Error response did not contain error code",
{
requestUrl: httpResponse.requestUrl,
},
),
);
}
throwUnexpectedRequestError(httpResponse, errJson);
}
}
export async function readSuccessResponseTextOrThrow<T>( export async function readSuccessResponseTextOrThrow<T>(
httpResponse: HttpResponse, httpResponse: HttpResponse,
): Promise<string> { ): Promise<string> {

View File

@ -113,6 +113,7 @@ import {
TransactionsResponse, TransactionsResponse,
} from "./types/transactions"; } from "./types/transactions";
import { getTransactions } from "./operations/transactions"; import { getTransactions } from "./operations/transactions";
import { withdrawTestBalance } from "./operations/testing";
const builtinCurrencies: CurrencyRecord[] = [ const builtinCurrencies: CurrencyRecord[] = [
{ {
@ -868,4 +869,13 @@ export class Wallet {
): Promise<TransactionsResponse> { ): Promise<TransactionsResponse> {
return getTransactions(this.ws, request); return getTransactions(this.ws, request);
} }
async withdrawTestBalance(
amount = "TESTKUDOS:10",
bankBaseUrl = "https://bank.test.taler.net/",
exchangeBaseUrl = "https://exchange.test.taler.net/",
): Promise<void> {
await withdrawTestBalance(this.ws, amount, bankBaseUrl, exchangeBaseUrl);
}
} }

View File

@ -21,7 +21,6 @@ import {
makeErrorDetails, makeErrorDetails,
} from "./operations/errors"; } from "./operations/errors";
import { TalerErrorCode } from "./TalerErrorCode"; import { TalerErrorCode } from "./TalerErrorCode";
import { withdrawTestBalance } from "./headless/helpers";
import { codecForTransactionsRequest } from "./types/transactions"; import { codecForTransactionsRequest } from "./types/transactions";
import { import {
makeCodecForObject, makeCodecForObject,
@ -160,7 +159,7 @@ async function dispatchRequestInternal(
): Promise<Record<string, any>> { ): Promise<Record<string, any>> {
switch (operation) { switch (operation) {
case "withdrawTestkudos": case "withdrawTestkudos":
await withdrawTestBalance(wallet); await wallet.withdrawTestBalance();
return {}; return {};
case "getTransactions": { case "getTransactions": {
const req = codecForTransactionsRequest().decode(payload); const req = codecForTransactionsRequest().decode(payload);