aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-harness/src/integrationtests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-harness/src/integrationtests')
-rw-r--r--packages/taler-harness/src/integrationtests/scenario-prompt-payment.ts60
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts201
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts116
-rw-r--r--packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts92
-rw-r--r--packages/taler-harness/src/integrationtests/test-bank-api.ts136
-rw-r--r--packages/taler-harness/src/integrationtests/test-claim-loop.ts79
-rw-r--r--packages/taler-harness/src/integrationtests/test-clause-schnorr.ts97
-rw-r--r--packages/taler-harness/src/integrationtests/test-denom-unoffered.ts126
-rw-r--r--packages/taler-harness/src/integrationtests/test-deposit.ts71
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-management.ts285
-rw-r--r--packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts240
-rw-r--r--packages/taler-harness/src/integrationtests/test-fee-regression.ts200
-rw-r--r--packages/taler-harness/src/integrationtests/test-forced-selection.ts87
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts109
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts56
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts71
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts70
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts64
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts76
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts69
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts106
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts63
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-bad-gateway.ts74
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-basic.ts308
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts147
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts169
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-keyrotation.ts79
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts118
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-refund-multiple-users.ts104
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-refund.ts101
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts85
-rw-r--r--packages/taler-harness/src/integrationtests/test-libeufin-tutorial.ts128
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts243
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts129
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts189
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-instances.ts184
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts162
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts303
-rw-r--r--packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts620
-rw-r--r--packages/taler-harness/src/integrationtests/test-pay-paid.ts222
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-claim.ts110
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-fault.ts222
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-forgettable.ts81
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-idempotency.ts121
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-multiple.ts163
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-on-demo.ts114
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-transient.ts185
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment-zero.ts72
-rw-r--r--packages/taler-harness/src/integrationtests/test-payment.ts77
-rw-r--r--packages/taler-harness/src/integrationtests/test-paywall-flow.ts252
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts101
-rw-r--r--packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts119
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-auto.ts105
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-gone.ts124
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund-incremental.ts202
-rw-r--r--packages/taler-harness/src/integrationtests/test-refund.ts106
-rw-r--r--packages/taler-harness/src/integrationtests/test-revocation.ts215
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts216
-rw-r--r--packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts98
-rw-r--r--packages/taler-harness/src/integrationtests/test-tipping.ts129
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts168
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts174
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-balance.ts144
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-cryptoworker.ts55
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallet-dbless.ts112
-rw-r--r--packages/taler-harness/src/integrationtests/test-wallettesting.ts233
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts84
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts91
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts97
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-high.ts99
-rw-r--r--packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts84
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts496
72 files changed, 10488 insertions, 0 deletions
diff --git a/packages/taler-harness/src/integrationtests/scenario-prompt-payment.ts b/packages/taler-harness/src/integrationtests/scenario-prompt-payment.ts
new file mode 100644
index 000000000..ea05de8e9
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/scenario-prompt-payment.ts
@@ -0,0 +1,60 @@
+/*
+ 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, MerchantPrivateApi } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runPromptPaymentScenario(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ // Set up order.
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ },
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ console.log(orderStatus);
+
+ // Wait "forever"
+ await new Promise(() => {});
+}
diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts
new file mode 100644
index 000000000..ff589dd79
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts
@@ -0,0 +1,201 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { BankApi, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ getWireMethodForTest,
+ GlobalTestState,
+ MerchantPrivateApi,
+ WalletCli,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+ makeTestPayment,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runAgeRestrictionsMerchantTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet: walletOne,
+ bank,
+ exchange,
+ merchant,
+ exchangeBankAccount,
+ } = await createSimpleTestkudosEnvironment(
+ t,
+ defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ {
+ ageMaskSpec: "8:10:12:14:16:18:21",
+ },
+ );
+
+ const walletTwo = new WalletCli(t, "walletTwo");
+ const walletThree = new WalletCli(t, "walletThree");
+
+ {
+ const walletZero = new WalletCli(t, "walletZero");
+
+ await withdrawViaBank(t, {
+ wallet: walletZero,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ restrictAge: 13,
+ });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ minimum_age: 9,
+ };
+
+ await makeTestPayment(t, { wallet: walletZero, merchant, order });
+ await walletZero.runUntilDone();
+ }
+
+ {
+ const wallet = walletOne;
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ restrictAge: 13,
+ });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ minimum_age: 9,
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ await wallet.runUntilDone();
+ }
+
+ {
+ const wallet = walletTwo;
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ restrictAge: 13,
+ });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ await wallet.runUntilDone();
+ }
+
+ {
+ const wallet = walletThree;
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ minimum_age: 9,
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ await wallet.runUntilDone();
+ }
+
+ // Pay with coin from tipping
+ {
+ const mbu = await BankApi.createRandomBankUser(bank);
+ const tipReserveResp = await MerchantPrivateApi.createTippingReserve(
+ merchant,
+ "default",
+ {
+ exchange_url: exchange.baseUrl,
+ initial_balance: "TESTKUDOS:10",
+ wire_method: getWireMethodForTest(),
+ },
+ );
+
+ t.assertDeepEqual(
+ tipReserveResp.payto_uri,
+ exchangeBankAccount.accountPaytoUri,
+ );
+
+ await BankApi.adminAddIncoming(bank, {
+ amount: "TESTKUDOS:10",
+ debitAccountPayto: mbu.accountPaytoUri,
+ exchangeBankAccount,
+ reservePub: tipReserveResp.reserve_pub,
+ });
+
+ await exchange.runWirewatchOnce();
+
+ const tip = await MerchantPrivateApi.giveTip(merchant, "default", {
+ amount: "TESTKUDOS:5",
+ justification: "why not?",
+ next_url: "https://example.com/after-tip",
+ });
+
+ const walletTipping = new WalletCli(t, "age-tipping");
+
+ const ptr = await walletTipping.client.call(WalletApiOperation.PrepareTip, {
+ talerTipUri: tip.taler_tip_uri,
+ });
+
+ await walletTipping.client.call(WalletApiOperation.AcceptTip, {
+ walletTipId: ptr.walletTipId,
+ });
+
+ await walletTipping.runUntilDone();
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:4",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ minimum_age: 9,
+ };
+
+ await makeTestPayment(t, { wallet: walletTipping, merchant, order });
+ await walletTipping.runUntilDone();
+ }
+}
+
+runAgeRestrictionsMerchantTest.suites = ["wallet"];
+runAgeRestrictionsMerchantTest.timeoutMs = 120 * 1000;
diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts
new file mode 100644
index 000000000..8bf71b63d
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts
@@ -0,0 +1,116 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { defaultCoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState, WalletCli } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+ makeTestPayment,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runAgeRestrictionsMixedMerchantTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet: walletOne,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironment(
+ t,
+ defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ {
+ ageMaskSpec: "8:10:12:14:16:18:21",
+ mixedAgeRestriction: true,
+ },
+ );
+
+ const walletTwo = new WalletCli(t, "walletTwo");
+ const walletThree = new WalletCli(t, "walletThree");
+
+ {
+ const wallet = walletOne;
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ restrictAge: 13,
+ });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ minimum_age: 9,
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ await wallet.runUntilDone();
+ }
+
+ {
+ const wallet = walletTwo;
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ restrictAge: 13,
+ });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ await wallet.runUntilDone();
+ }
+
+ {
+ const wallet = walletThree;
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ minimum_age: 9,
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ await wallet.runUntilDone();
+ }
+}
+
+runAgeRestrictionsMixedMerchantTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts
new file mode 100644
index 000000000..af5b4df52
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts
@@ -0,0 +1,92 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { AbsoluteTime, Duration } from "@gnu-taler/taler-util";
+import { getDefaultNodeWallet2, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState, WalletCli } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+ makeTestPayment,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet: walletOne,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironment(
+ t,
+ defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ {
+ ageMaskSpec: "8:10:12:14:16:18:21",
+ },
+ );
+
+ const walletTwo = new WalletCli(t, "walletTwo");
+ const walletThree = new WalletCli(t, "walletThree");
+
+ {
+ const wallet = walletOne;
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ restrictAge: 13,
+ });
+
+ const purse_expiration = AbsoluteTime.toTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const initResp = await wallet.client.call(WalletApiOperation.InitiatePeerPushPayment, {
+ partialContractTerms: {
+ summary: "Hello, World",
+ amount: "TESTKUDOS:1",
+ purse_expiration,
+ },
+ });
+
+ await wallet.runUntilDone();
+
+ const checkResp = await walletTwo.client.call(WalletApiOperation.CheckPeerPushPayment, {
+ talerUri: initResp.talerUri,
+ });
+
+ await walletTwo.client.call(WalletApiOperation.AcceptPeerPushPayment, {
+ peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId,
+ });
+
+ await walletTwo.runUntilDone();
+ }
+}
+
+runAgeRestrictionsPeerTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-bank-api.ts b/packages/taler-harness/src/integrationtests/test-bank-api.ts
new file mode 100644
index 000000000..c7a23d3ce
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-bank-api.ts
@@ -0,0 +1,136 @@
+/*
+ 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,
+ WalletCli,
+ ExchangeService,
+ setupDb,
+ BankService,
+ MerchantService,
+ getPayto,
+} from "../harness/harness.js";
+import { createEddsaKeyPair, encodeCrock } from "@gnu-taler/taler-util";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankApi,
+ BankAccessApi,
+ CreditDebitIndicator,
+} from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runBankApiTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ currency: "TESTKUDOS",
+ httpPort: 8082,
+ database: db.connStr,
+ allowRegistrations: true,
+ });
+
+ 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);
+
+ bank.setSuggestedExchange(exchange, 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.addDefaultInstance();
+ await merchant.addInstance({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getPayto("minst1")],
+ });
+
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ });
+
+ console.log("setup done!");
+
+ const bankUser = await BankApi.registerAccount(bank, "user1", "pw1");
+
+ // Make sure that registering twice results in a 409 Conflict
+ {
+ const e = await t.assertThrowsTalerErrorAsync(async () => {
+ await BankApi.registerAccount(bank, "user1", "pw2");
+ });
+ t.assertTrue(e.errorDetail.httpStatusCode === 409);
+ }
+
+ let balResp = await BankAccessApi.getAccountBalance(bank, bankUser);
+
+ console.log(balResp);
+
+ // Check that we got the sign-up bonus.
+ t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:100");
+ t.assertTrue(
+ balResp.balance.credit_debit_indicator === CreditDebitIndicator.Credit,
+ );
+
+ const res = createEddsaKeyPair();
+
+ await BankApi.adminAddIncoming(bank, {
+ amount: "TESTKUDOS:115",
+ debitAccountPayto: bankUser.accountPaytoUri,
+ exchangeBankAccount: exchangeBankAccount,
+ reservePub: encodeCrock(res.eddsaPub),
+ });
+
+ balResp = await BankAccessApi.getAccountBalance(bank, bankUser);
+ t.assertAmountEquals(balResp.balance.amount, "TESTKUDOS:15");
+ t.assertTrue(
+ balResp.balance.credit_debit_indicator === CreditDebitIndicator.Debit,
+ );
+}
diff --git a/packages/taler-harness/src/integrationtests/test-claim-loop.ts b/packages/taler-harness/src/integrationtests/test-claim-loop.ts
new file mode 100644
index 000000000..a509e3b19
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-claim-loop.ts
@@ -0,0 +1,79 @@
+/*
+ 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, MerchantPrivateApi } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
+import { URL } from "url";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for the merchant's order lifecycle.
+ *
+ * FIXME: Is this test still necessary? We initially wrote if to confirm/document
+ * assumptions about how the merchant should work.
+ */
+export async function runClaimLoopTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironment(t);
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ // Set up order.
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ },
+ });
+
+ // Query private order status before claiming it.
+ let orderStatusBefore = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderResp.order_id,
+ },
+ );
+ t.assertTrue(orderStatusBefore.order_status === "unpaid");
+ let statusUrlBefore = new URL(orderStatusBefore.order_status_url);
+
+ // Make wallet claim the unpaid order.
+ t.assertTrue(orderStatusBefore.order_status === "unpaid");
+ const talerPayUri = orderStatusBefore.taler_pay_uri;
+ await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri,
+ });
+
+ // Query private order status after claiming it.
+ let orderStatusAfter = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderResp.order_id,
+ },
+ );
+ t.assertTrue(orderStatusAfter.order_status === "claimed");
+
+ await t.shutdown();
+}
diff --git a/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts b/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts
new file mode 100644
index 000000000..bf42dc4c6
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-clause-schnorr.ts
@@ -0,0 +1,97 @@
+/*
+ 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 { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+ makeTestPayment,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runClauseSchnorrTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => {
+ return {
+ ...x("TESTKUDOS"),
+ cipher: "CS",
+ };
+ });
+
+ // We need to have at least one RSA denom configured
+ coinConfig.push({
+ cipher: "RSA",
+ rsaKeySize: 1024,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ feeDeposit: "TESTKUDOS:42",
+ value: "TESTKUDOS:0.0001",
+ feeWithdraw: "TESTKUDOS:42",
+ feeRefresh: "TESTKUDOS:42",
+ feeRefund: "TESTKUDOS:42",
+ name: "rsa_dummy",
+ });
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t, coinConfig);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ await wallet.runUntilDone();
+
+ // Test JSON normalization of contract terms: Does the wallet
+ // agree with the merchant?
+ const order2 = {
+ summary: "Testing “unicode” characters",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order: order2 });
+ await wallet.runUntilDone();
+
+ // Test JSON normalization of contract terms: Does the wallet
+ // agree with the merchant?
+ const order3 = {
+ summary: "Testing\nNewlines\rAnd\tStuff\nHere\b",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order: order3 });
+
+ await wallet.runUntilDone();
+}
+
+runClauseSchnorrTest.suites = ["experimental-wallet"];
+runClauseSchnorrTest.excludeByDefault = true;
diff --git a/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
new file mode 100644
index 000000000..b5ecbee4a
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-denom-unoffered.ts
@@ -0,0 +1,126 @@
+/*
+ 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 { PreparePayResultType, TalerErrorCode } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+
+export async function runDenomUnofferedTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ // Make the exchange forget the denomination.
+ // Effectively we completely reset the exchange,
+ // but keep the exchange master public key.
+
+ await exchange.stop();
+ await exchange.purgeDatabase();
+ await exchange.purgeSecmodKeys();
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ await merchant.stop();
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ {
+ const orderResp = await MerchantPrivateApi.createOrder(
+ merchant,
+ "default",
+ {
+ order: order,
+ },
+ );
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderResp.order_id,
+ },
+ );
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ const exc = await t.assertThrowsTalerErrorAsync(async () => {
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: preparePayResult.proposalId,
+ });
+ });
+
+ t.assertTrue(
+ exc.hasErrorCode(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED),
+ );
+
+ // FIXME: We might want a more specific error code here!
+ t.assertDeepEqual(
+ exc.errorDetail.innerError.code,
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ );
+ const merchantErrorCode = (exc.errorDetail.innerError.errorResponse as any)
+ .code;
+ t.assertDeepEqual(
+ merchantErrorCode,
+ TalerErrorCode.MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND,
+ );
+ }
+
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ forceUpdate: true,
+ });
+
+ // Now withdrawal should work again.
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ await wallet.runUntilDone();
+
+ const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ console.log(JSON.stringify(txs, undefined, 2));
+}
+
+runDenomUnofferedTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-deposit.ts b/packages/taler-harness/src/integrationtests/test-deposit.ts
new file mode 100644
index 000000000..07382c43e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-deposit.ts
@@ -0,0 +1,71 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, getPayto } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runDepositTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ await wallet.runUntilDone();
+
+ const { depositGroupId } = await wallet.client.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:10",
+ depositPaytoUri: getPayto("foo"),
+ },
+ );
+
+ await wallet.runUntilDone();
+
+ const transactions = await wallet.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log("transactions", JSON.stringify(transactions, undefined, 2));
+ t.assertDeepEqual(transactions.transactions[0].type, "withdrawal");
+ t.assertTrue(!transactions.transactions[0].pending);
+ t.assertDeepEqual(transactions.transactions[1].type, "deposit");
+ t.assertTrue(!transactions.transactions[1].pending);
+ // The raw amount is what ends up on the bank account, which includes
+ // deposit and wire fees.
+ t.assertDeepEqual(transactions.transactions[1].amountRaw, "TESTKUDOS:9.79");
+
+ const trackResult = wallet.client.call(WalletApiOperation.TrackDepositGroup, {
+ depositGroupId,
+ });
+
+ console.log(JSON.stringify(trackResult, undefined, 2));
+}
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management.ts b/packages/taler-harness/src/integrationtests/test-exchange-management.ts
new file mode 100644
index 000000000..6b63c3741
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-exchange-management.ts
@@ -0,0 +1,285 @@
+/*
+ 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,
+ WalletCli,
+ setupDb,
+ BankService,
+ ExchangeService,
+ MerchantService,
+ getPayto,
+} from "../harness/harness.js";
+import {
+ WalletApiOperation,
+ BankApi,
+ BankAccessApi,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ ExchangesListResponse,
+ URL,
+ TalerErrorCode,
+ j2s,
+} from "@gnu-taler/taler-util";
+import {
+ FaultInjectedExchangeService,
+ FaultInjectionResponseContext,
+} from "../harness/faultInjection.js";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+
+/**
+ * Test if the wallet handles outdated exchange versions correct.y
+ */
+export async function runExchangeManagementTest(
+ t: GlobalTestState,
+): Promise<void> {
+ // 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: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ });
+
+ await merchant.addInstance({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getPayto("minst1")],
+ });
+
+ 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: ExchangesListResponse;
+
+ exchangesList = await wallet.client.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+ console.log("exchanges list:", j2s(exchangesList));
+ t.assertTrue(exchangesList.exchanges.length === 0);
+
+ // Try before fault is injected
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: faultyExchange.baseUrl,
+ });
+
+ exchangesList = await wallet.client.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+ t.assertTrue(exchangesList.exchanges.length === 1);
+
+ await wallet.client.call(WalletApiOperation.ListExchanges, {});
+
+ console.log("listing exchanges");
+
+ exchangesList = await wallet.client.call(
+ WalletApiOperation.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.client.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+ t.assertTrue(exchangesList.exchanges.length === 0);
+
+ faultyExchange.faultProxy.addFault({
+ async 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");
+ }
+ },
+ });
+
+ const err1 = await t.assertThrowsTalerErrorAsync(async () => {
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: faultyExchange.baseUrl,
+ });
+ });
+
+ // Response is malformed, since it didn't even contain a version code
+ // in a format the wallet can understand.
+ t.assertTrue(
+ err1.errorDetail.code === TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ );
+ exchangesList = await wallet.client.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+ console.log("exchanges list", j2s(exchangesList));
+ t.assertTrue(exchangesList.exchanges.length === 1);
+ t.assertTrue(
+ exchangesList.exchanges[0].lastUpdateErrorInfo?.error.code ===
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ );
+
+ /*
+ * =========================================================================
+ * Check what happens if the exchange returns an old, unsupported
+ * version for /keys
+ * =========================================================================
+ */
+
+ wallet.deleteDatabase();
+ faultyExchange.faultProxy.clearAllFaults();
+
+ faultyExchange.faultProxy.addFault({
+ async 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");
+ }
+ },
+ });
+
+ const err2 = await t.assertThrowsTalerErrorAsync(async () => {
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: faultyExchange.baseUrl,
+ });
+ });
+
+ t.assertTrue(
+ err2.hasErrorCode(
+ TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
+ ),
+ );
+
+ exchangesList = await wallet.client.call(
+ WalletApiOperation.ListExchanges,
+ {},
+ );
+ t.assertTrue(exchangesList.exchanges.length === 1);
+ t.assertTrue(
+ exchangesList.exchanges[0].lastUpdateErrorInfo?.error.code ===
+ TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
+ );
+
+ /*
+ * =========================================================================
+ * 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 BankApi.createRandomBankUser(bank);
+ const wop = await BankAccessApi.createWithdrawalOperation(
+ bank,
+ user,
+ "TESTKUDOS:10",
+ );
+
+ // Hand it to the wallet
+
+ const wd = await wallet.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ // Make sure the faulty exchange isn't used for the suggestion.
+ t.assertTrue(wd.possibleExchanges.length === 0);
+}
+
+runExchangeManagementTest.suites = ["wallet", "exchange"];
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
new file mode 100644
index 000000000..074126e9f
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts
@@ -0,0 +1,240 @@
+/*
+ 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 {
+ AbsoluteTime,
+ codecForExchangeKeysJson,
+ DenominationPubKey,
+ Duration,
+ durationFromSpec,
+} from "@gnu-taler/taler-util";
+import {
+ NodeHttpLib,
+ readSuccessResponseJsonOrThrow,
+} from "@gnu-taler/taler-wallet-core";
+import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantPrivateApi,
+ MerchantService,
+ setupDb,
+ WalletCli,
+ getPayto,
+} from "../harness/harness.js";
+import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js";
+
+async function applyTimeTravel(
+ timetravelDuration: Duration,
+ s: {
+ exchange?: ExchangeService;
+ merchant?: MerchantService;
+ wallet?: WalletCli;
+ },
+): Promise<void> {
+ if (s.exchange) {
+ await s.exchange.stop();
+ s.exchange.setTimetravel(timetravelDuration);
+ await s.exchange.start();
+ await s.exchange.pingUntilAvailable();
+ }
+
+ if (s.merchant) {
+ await s.merchant.stop();
+ s.merchant.setTimetravel(timetravelDuration);
+ await s.merchant.start();
+ await s.merchant.pingUntilAvailable();
+ }
+
+ if (s.wallet) {
+ console.log("setting wallet time travel to", timetravelDuration);
+ s.wallet.setTimetravel(timetravelDuration);
+ }
+}
+
+const http = new NodeHttpLib();
+
+/**
+ * Basic time travel test.
+ */
+export async function runExchangeTimetravelTest(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);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ exchange.addCoinConfigList(makeNoFeeCoinConfig("TESTKUDOS"));
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ });
+
+ await merchant.addInstance({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
+
+ const keysResp1 = await http.get(exchange.baseUrl + "keys");
+ const keys1 = await readSuccessResponseJsonOrThrow(
+ keysResp1,
+ codecForExchangeKeysJson(),
+ );
+ console.log(
+ "keys 1 (before time travel):",
+ JSON.stringify(keys1, undefined, 2),
+ );
+
+ // Travel into the future, the deposit expiration is two years
+ // into the future.
+ console.log("applying first time travel");
+ await applyTimeTravel(durationFromSpec({ days: 400 }), {
+ wallet,
+ exchange,
+ merchant,
+ });
+
+ const keysResp2 = await http.get(exchange.baseUrl + "keys");
+ const keys2 = await readSuccessResponseJsonOrThrow(
+ keysResp2,
+ codecForExchangeKeysJson(),
+ );
+ console.log(
+ "keys 2 (after time travel):",
+ JSON.stringify(keys2, undefined, 2),
+ );
+
+ const denomPubs1 = keys1.denoms.map((x) => {
+ return {
+ denomPub: x.denom_pub,
+ expireDeposit: AbsoluteTime.stringify(
+ AbsoluteTime.fromTimestamp(x.stamp_expire_deposit),
+ ),
+ };
+ });
+
+ const denomPubs2 = keys2.denoms.map((x) => {
+ return {
+ denomPub: x.denom_pub,
+ expireDeposit: AbsoluteTime.stringify(
+ AbsoluteTime.fromTimestamp(x.stamp_expire_deposit),
+ ),
+ };
+ });
+ const dps2 = new Set(denomPubs2.map((x) => x.denomPub));
+
+ console.log("=== KEYS RESPONSE 1 ===");
+
+ console.log(
+ "list issue date",
+ AbsoluteTime.stringify(AbsoluteTime.fromTimestamp(keys1.list_issue_date)),
+ );
+ console.log("num denoms", keys1.denoms.length);
+ console.log("denoms", JSON.stringify(denomPubs1, undefined, 2));
+
+ console.log("=== KEYS RESPONSE 2 ===");
+
+ console.log(
+ "list issue date",
+ AbsoluteTime.stringify(AbsoluteTime.fromTimestamp(keys2.list_issue_date)),
+ );
+ console.log("num denoms", keys2.denoms.length);
+ console.log("denoms", JSON.stringify(denomPubs2, undefined, 2));
+
+ for (const da of denomPubs1) {
+ let found = false;
+ for (const db of denomPubs2) {
+ const d1 = da.denomPub;
+ const d2 = db.denomPub;
+ if (DenominationPubKey.cmp(d1, d2) === 0) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ console.log("=== ERROR ===");
+ console.log(
+ `denomination with public key ${da.denomPub} is not present in new /keys response`,
+ );
+ console.log(
+ `the new /keys response was issued ${AbsoluteTime.stringify(
+ AbsoluteTime.fromTimestamp(keys2.list_issue_date),
+ )}`,
+ );
+ console.log(
+ `however, the missing denomination has stamp_expire_deposit ${da.expireDeposit}`,
+ );
+ console.log("see above for the verbatim /keys responses");
+ t.assertTrue(false);
+ }
+ }
+}
+
+runExchangeTimetravelTest.suites = ["exchange"];
diff --git a/packages/taler-harness/src/integrationtests/test-fee-regression.ts b/packages/taler-harness/src/integrationtests/test-fee-regression.ts
new file mode 100644
index 000000000..8c5a5bea4
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-fee-regression.ts
@@ -0,0 +1,200 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ GlobalTestState,
+ BankService,
+ ExchangeService,
+ MerchantService,
+ setupDb,
+ WalletCli,
+ getPayto,
+} from "../harness/harness.js";
+import {
+ withdrawViaBank,
+ makeTestPayment,
+ SimpleTestEnvironment,
+} from "../harness/helpers.js";
+
+/**
+ * Run a test case with a simple TESTKUDOS Taler environment, consisting
+ * of one exchange, one bank and one merchant.
+ */
+export async function createMyTestkudosEnvironment(
+ t: GlobalTestState,
+): Promise<SimpleTestEnvironment> {
+ 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);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinCommon = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ rsaKeySize: 1024,
+ feeDeposit: "TESTKUDOS:0.0025",
+ feeWithdraw: "TESTKUDOS:0",
+ feeRefresh: "TESTKUDOS:0",
+ feeRefund: "TESTKUDOS:0",
+ };
+
+ exchange.addCoinConfigList([
+ {
+ ...coinCommon,
+ name: "c1",
+ value: "TESTKUDOS:1.28",
+ },
+ {
+ ...coinCommon,
+ name: "c2",
+ value: "TESTKUDOS:0.64",
+ },
+ {
+ ...coinCommon,
+ name: "c3",
+ value: "TESTKUDOS:0.32",
+ },
+ {
+ ...coinCommon,
+ name: "c4",
+ value: "TESTKUDOS:0.16",
+ },
+ {
+ ...coinCommon,
+ name: "c5",
+ value: "TESTKUDOS:0.08",
+ },
+ {
+ ...coinCommon,
+ name: "c5",
+ value: "TESTKUDOS:0.04",
+ },
+ {
+ ...coinCommon,
+ name: "c6",
+ value: "TESTKUDOS:0.02",
+ },
+ {
+ ...coinCommon,
+ name: "c7",
+ value: "TESTKUDOS:0.01",
+ },
+ ]);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addDefaultInstance();
+ await merchant.addInstance({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ wallet,
+ bank,
+ exchangeBankAccount,
+ };
+}
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runFeeRegressionTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createMyTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:1.92",
+ });
+
+ const coins = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+
+ // Make sure we really withdraw one 0.64 and one 1.28 coin.
+ t.assertTrue(coins.coins.length === 2);
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:1.30",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+
+ await wallet.runUntilDone();
+
+ const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ t.assertAmountEquals(txs.transactions[1].amountEffective, "TESTKUDOS:1.30");
+ console.log(txs);
+}
+
+runFeeRegressionTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-forced-selection.ts b/packages/taler-harness/src/integrationtests/test-forced-selection.ts
new file mode 100644
index 000000000..91be11a82
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-forced-selection.ts
@@ -0,0 +1,87 @@
+/*
+ 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 { j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+
+/**
+ * Run test for forced denom/coin selection.
+ */
+export async function runForcedSelectionTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ exchangeBaseUrl: exchange.baseUrl,
+ amount: "TESTKUDOS:10",
+ bankBaseUrl: bank.baseUrl,
+ bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ forcedDenomSel: {
+ denoms: [
+ {
+ value: "TESTKUDOS:2",
+ count: 3,
+ },
+ ],
+ },
+ });
+
+ await wallet.runUntilDone();
+
+ const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+ console.log(coinDump);
+ t.assertDeepEqual(coinDump.coins.length, 3);
+
+ const payResp = await wallet.client.call(WalletApiOperation.TestPay, {
+ amount: "TESTKUDOS:3",
+ merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+ summary: "bla",
+ forcedCoinSel: {
+ coins: [
+ {
+ value: "TESTKUDOS:2",
+ contribution: "TESTKUDOS:1",
+ },
+ {
+ value: "TESTKUDOS:2",
+ contribution: "TESTKUDOS:1",
+ },
+ {
+ value: "TESTKUDOS:2",
+ contribution: "TESTKUDOS:1",
+ },
+ ],
+ },
+ });
+
+ console.log(j2s(payResp));
+
+ // Without forced selection, we would only use 2 coins.
+ t.assertDeepEqual(payResp.payCoinSelection.coinContributions.length, 3);
+}
+
+runForcedSelectionTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts
new file mode 100644
index 000000000..c3cbc0608
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts
@@ -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 } from "../harness/harness.js";
+import {
+ NexusUserBundle,
+ LibeufinNexusApi,
+ LibeufinNexusService,
+ LibeufinSandboxService,
+ LibeufinSandboxApi,
+ findNexusPayment,
+} from "../harness/libeufin.js";
+
+/**
+ * Run basic test with LibEuFin.
+ */
+export async function runLibeufinApiBankaccountTest(t: GlobalTestState) {
+ const nexus = await LibeufinNexusService.create(t, {
+ httpPort: 5011,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
+ });
+ await nexus.start();
+ await nexus.pingUntilAvailable();
+
+ await LibeufinNexusApi.createUser(nexus, {
+ username: "one",
+ password: "testing-the-bankaccount-api",
+ });
+ const sandbox = await LibeufinSandboxService.create(t, {
+ httpPort: 5012,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
+ });
+ await sandbox.start();
+ await sandbox.pingUntilAvailable();
+ await LibeufinSandboxApi.createEbicsHost(sandbox, "mock");
+ await LibeufinSandboxApi.createDemobankAccount(
+ "mock",
+ "password-unused",
+ { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
+ "DE71500105179674997361"
+ );
+ await LibeufinSandboxApi.createDemobankEbicsSubscriber(
+ {
+ hostID: "mock",
+ partnerID: "mock",
+ userID: "mock",
+ },
+ "mock",
+ { baseUrl: sandbox.baseUrl + "/demobanks/default/" }
+ );
+ await LibeufinNexusApi.createEbicsBankConnection(nexus, {
+ name: "bankaccount-api-test-connection",
+ ebicsURL: "http://localhost:5012/ebicsweb",
+ hostID: "mock",
+ userID: "mock",
+ partnerID: "mock",
+ });
+ await LibeufinNexusApi.connectBankConnection(
+ nexus,
+ "bankaccount-api-test-connection",
+ );
+ await LibeufinNexusApi.fetchAccounts(
+ nexus,
+ "bankaccount-api-test-connection",
+ );
+
+ await LibeufinNexusApi.importConnectionAccount(
+ nexus,
+ "bankaccount-api-test-connection",
+ "mock",
+ "local-mock",
+ );
+ await LibeufinSandboxApi.simulateIncomingTransaction(
+ sandbox,
+ "mock", // creditor bankaccount label
+ {
+ debtorIban: "DE84500105176881385584",
+ debtorBic: "BELADEBEXXX",
+ debtorName: "mock2",
+ amount: "1",
+ subject: "mock subject",
+ },
+ );
+ await LibeufinNexusApi.fetchTransactions(nexus, "local-mock");
+ let transactions = await LibeufinNexusApi.getAccountTransactions(
+ nexus,
+ "local-mock",
+ );
+ let el = findNexusPayment("mock subject", transactions.data);
+ t.assertTrue(el instanceof Object);
+}
+
+runLibeufinApiBankaccountTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts
new file mode 100644
index 000000000..912b7b2ac
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts
@@ -0,0 +1,56 @@
+/*
+ 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 } from "../harness/harness.js";
+import { LibeufinNexusApi, LibeufinNexusService } from "../harness/libeufin.js";
+
+/**
+ * Run basic test with LibEuFin.
+ */
+export async function runLibeufinApiBankconnectionTest(t: GlobalTestState) {
+ const nexus = await LibeufinNexusService.create(t, {
+ httpPort: 5011,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
+ });
+ await nexus.start();
+ await nexus.pingUntilAvailable();
+
+ await LibeufinNexusApi.createUser(nexus, {
+ username: "one",
+ password: "testing-the-bankconnection-api",
+ });
+
+ await LibeufinNexusApi.createEbicsBankConnection(nexus, {
+ name: "bankconnection-api-test-connection",
+ ebicsURL: "http://localhost:5012/ebicsweb",
+ hostID: "mock",
+ userID: "mock",
+ partnerID: "mock",
+ });
+
+ let connections = await LibeufinNexusApi.getAllConnections(nexus);
+ t.assertTrue(connections.data["bankConnections"].length == 1);
+
+ await LibeufinNexusApi.deleteBankConnection(nexus, {
+ bankConnectionId: "bankconnection-api-test-connection",
+ });
+ connections = await LibeufinNexusApi.getAllConnections(nexus);
+ t.assertTrue(connections.data["bankConnections"].length == 0);
+}
+runLibeufinApiBankconnectionTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts
new file mode 100644
index 000000000..a1da9e0da
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts
@@ -0,0 +1,71 @@
+/*
+ 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 { URL } from "@gnu-taler/taler-util";
+import axiosImp from "axios";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ launchLibeufinServices,
+ NexusUserBundle,
+ SandboxUserBundle,
+} from "../harness/libeufin.js";
+
+const axios = axiosImp.default;
+
+export async function runLibeufinApiFacadeBadRequestTest(t: GlobalTestState) {
+ /**
+ * User saltetd "01"
+ */
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user01sandbox = new SandboxUserBundle("01");
+
+ /**
+ * Launch Sandbox and Nexus.
+ */
+ const libeufinServices = await launchLibeufinServices(
+ t,
+ [user01nexus],
+ [user01sandbox],
+ ["twg"],
+ );
+ console.log("malformed facade");
+ const baseUrl = libeufinServices.libeufinNexus.baseUrl;
+ let url = new URL("facades", baseUrl);
+ let resp = await axios.post(
+ url.href,
+ {
+ name: "malformed-facade",
+ type: "taler-wire-gateway",
+ config: {}, // malformation here.
+ },
+ {
+ auth: {
+ username: "admin",
+ password: "test",
+ },
+ validateStatus: () => true,
+ },
+ );
+ t.assertTrue(resp.status == 400);
+}
+
+runLibeufinApiFacadeBadRequestTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts
new file mode 100644
index 000000000..946c565d4
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts
@@ -0,0 +1,70 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ SandboxUserBundle,
+ NexusUserBundle,
+ launchLibeufinServices,
+ LibeufinNexusApi,
+} from "../harness/libeufin.js";
+
+/**
+ * Run basic test with LibEuFin.
+ */
+export async function runLibeufinApiFacadeTest(t: GlobalTestState) {
+ /**
+ * User saltetd "01"
+ */
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user01sandbox = new SandboxUserBundle("01");
+
+ /**
+ * Launch Sandbox and Nexus.
+ */
+ const libeufinServices = await launchLibeufinServices(
+ t,
+ [user01nexus],
+ [user01sandbox],
+ ["twg"],
+ );
+ let resp = await LibeufinNexusApi.getAllFacades(
+ libeufinServices.libeufinNexus,
+ );
+ // check that original facade shows up.
+ t.assertTrue(resp.data["facades"][0]["name"] == user01nexus.twgReq["name"]);
+
+ const twgBaseUrl: string = resp.data["facades"][0]["baseUrl"];
+ t.assertTrue(typeof twgBaseUrl === "string");
+ t.assertTrue(twgBaseUrl.startsWith("http://"));
+ t.assertTrue(twgBaseUrl.endsWith("/"));
+
+ // delete it.
+ resp = await LibeufinNexusApi.deleteFacade(
+ libeufinServices.libeufinNexus,
+ user01nexus.twgReq["name"],
+ );
+ // check that no facades show up.
+ t.assertTrue(!resp.data.hasOwnProperty("facades"));
+}
+
+runLibeufinApiFacadeTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts
new file mode 100644
index 000000000..f8f2d7d80
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts
@@ -0,0 +1,64 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ NexusUserBundle,
+ LibeufinNexusApi,
+ LibeufinNexusService,
+} from "../harness/libeufin.js";
+
+/**
+ * Run basic test with LibEuFin.
+ */
+export async function runLibeufinApiPermissionsTest(t: GlobalTestState) {
+ const nexus = await LibeufinNexusService.create(t, {
+ httpPort: 5011,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
+ });
+ await nexus.start();
+ await nexus.pingUntilAvailable();
+
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+
+ await LibeufinNexusApi.createUser(nexus, user01nexus.userReq);
+ await LibeufinNexusApi.postPermission(
+ nexus,
+ user01nexus.twgTransferPermission,
+ );
+ let transferPermission = await LibeufinNexusApi.getAllPermissions(nexus);
+ let element = transferPermission.data["permissions"].pop();
+ t.assertTrue(
+ element["permissionName"] == "facade.talerwiregateway.transfer" &&
+ element["subjectId"] == "username-01",
+ );
+ let denyTransfer = user01nexus.twgTransferPermission;
+
+ // Now revoke permission.
+ denyTransfer["action"] = "revoke";
+ await LibeufinNexusApi.postPermission(nexus, denyTransfer);
+
+ transferPermission = await LibeufinNexusApi.getAllPermissions(nexus);
+ t.assertTrue(transferPermission.data["permissions"].length == 0);
+}
+
+runLibeufinApiPermissionsTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts
new file mode 100644
index 000000000..cb85c1ffc
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts
@@ -0,0 +1,76 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ LibeufinSandboxApi,
+ LibeufinSandboxService,
+} from "../harness/libeufin.js";
+
+// This test only checks that LibEuFin doesn't fail when
+// it generates Camt statements - no assertions take place.
+// Furthermore, it prints the Camt.053 being generated.
+export async function runLibeufinApiSandboxCamtTest(t: GlobalTestState) {
+ const sandbox = await LibeufinSandboxService.create(t, {
+ httpPort: 5012,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
+ });
+ await sandbox.start();
+ await sandbox.pingUntilAvailable();
+
+ await LibeufinSandboxApi.createDemobankAccount(
+ "mock-account-0",
+ "password-unused",
+ { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" }
+ );
+ await LibeufinSandboxApi.createDemobankAccount(
+ "mock-account-1",
+ "password-unused",
+ { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" }
+ );
+ await sandbox.makeTransaction(
+ "mock-account-0",
+ "mock-account-1",
+ "EUR:1",
+ "+1",
+ );
+ await sandbox.makeTransaction(
+ "mock-account-0",
+ "mock-account-1",
+ "EUR:1",
+ "+1",
+ );
+ await sandbox.makeTransaction(
+ "mock-account-0",
+ "mock-account-1",
+ "EUR:1",
+ "+1",
+ );
+ await sandbox.makeTransaction(
+ "mock-account-1",
+ "mock-account-0",
+ "EUR:5",
+ "minus 5",
+ );
+ await sandbox.c53tick();
+ let ret = await LibeufinSandboxApi.getCamt053(sandbox, "mock-account-1");
+ console.log(ret);
+}
+runLibeufinApiSandboxCamtTest.excludeByDefault = true;
+runLibeufinApiSandboxCamtTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts
new file mode 100644
index 000000000..24fd9d3ef
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts
@@ -0,0 +1,69 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ LibeufinSandboxApi,
+ LibeufinSandboxService,
+} from "../harness/libeufin.js";
+
+export async function runLibeufinApiSandboxTransactionsTest(
+ t: GlobalTestState,
+) {
+ const sandbox = await LibeufinSandboxService.create(t, {
+ httpPort: 5012,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
+ });
+ await sandbox.start();
+ await sandbox.pingUntilAvailable();
+ await LibeufinSandboxApi.createDemobankAccount(
+ "mock-account",
+ "password-unused",
+ { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
+ "DE71500105179674997361"
+ );
+ await LibeufinSandboxApi.simulateIncomingTransaction(
+ sandbox,
+ "mock-account",
+ {
+ debtorIban: "DE84500105176881385584",
+ debtorBic: "BELADEBEXXX",
+ debtorName: "mock2",
+ subject: "mock subject",
+ amount: "1", // EUR is default.
+ },
+ );
+ await LibeufinSandboxApi.simulateIncomingTransaction(
+ sandbox,
+ "mock-account",
+ {
+ debtorIban: "DE84500105176881385584",
+ debtorBic: "BELADEBEXXX",
+ debtorName: "mock2",
+ subject: "mock subject 2",
+ amount: "1.1", // EUR is default.
+ },
+ );
+ let ret = await LibeufinSandboxApi.getAccountInfoWithBalance(
+ sandbox,
+ "mock-account",
+ );
+ t.assertAmountEquals(ret.data.balance, "EUR:2.1");
+}
+runLibeufinApiSandboxTransactionsTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts
new file mode 100644
index 000000000..95f4bfaa0
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts
@@ -0,0 +1,106 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ launchLibeufinServices,
+ LibeufinNexusApi,
+ LibeufinNexusService,
+ NexusUserBundle,
+ SandboxUserBundle,
+} from "../harness/libeufin.js";
+
+/**
+ * Test Nexus scheduling API. It creates a task, check whether it shows
+ * up, then deletes it, and check if it's gone. Ideally, a check over the
+ * _liveliness_ of a scheduled task should happen.
+ */
+export async function runLibeufinApiSchedulingTest(t: GlobalTestState) {
+ const nexus = await LibeufinNexusService.create(t, {
+ httpPort: 5011,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
+ });
+ await nexus.start();
+ await nexus.pingUntilAvailable();
+
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user01sandbox = new SandboxUserBundle("01");
+ await launchLibeufinServices(t, [user01nexus], [user01sandbox]);
+ await LibeufinNexusApi.postTask(nexus, user01nexus.localAccountName, {
+ name: "test-task",
+ cronspec: "* * *",
+ type: "fetch",
+ params: {
+ level: "all",
+ rangeType: "all",
+ },
+ });
+ let resp = await LibeufinNexusApi.getTasks(
+ nexus,
+ user01nexus.localAccountName,
+ "test-task",
+ );
+ t.assertTrue(resp.data["taskName"] == "test-task");
+ await LibeufinNexusApi.deleteTask(
+ nexus,
+ user01nexus.localAccountName,
+ "test-task",
+ );
+ try {
+ await LibeufinNexusApi.getTasks(
+ nexus,
+ user01nexus.localAccountName,
+ "test-task",
+ );
+ } catch (err: any) {
+ t.assertTrue(err.response.status == 404);
+ }
+
+ // Same with submit task.
+ await LibeufinNexusApi.postTask(nexus, user01nexus.localAccountName, {
+ name: "test-task",
+ cronspec: "* * *",
+ type: "submit",
+ params: {},
+ });
+ resp = await LibeufinNexusApi.getTasks(
+ nexus,
+ user01nexus.localAccountName,
+ "test-task",
+ );
+ t.assertTrue(resp.data["taskName"] == "test-task");
+ await LibeufinNexusApi.deleteTask(
+ nexus,
+ user01nexus.localAccountName,
+ "test-task",
+ );
+ try {
+ await LibeufinNexusApi.getTasks(
+ nexus,
+ user01nexus.localAccountName,
+ "test-task",
+ );
+ } catch (err: any) {
+ t.assertTrue(err.response.status == 404);
+ }
+}
+runLibeufinApiSchedulingTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts b/packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts
new file mode 100644
index 000000000..bc3103c7e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts
@@ -0,0 +1,63 @@
+/*
+ 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 } from "../harness/harness.js";
+import { LibeufinNexusApi, LibeufinNexusService } from "../harness/libeufin.js";
+
+/**
+ * Run basic test with LibEuFin.
+ */
+export async function runLibeufinApiUsersTest(t: GlobalTestState) {
+ const nexus = await LibeufinNexusService.create(t, {
+ httpPort: 5011,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
+ });
+ await nexus.start();
+ await nexus.pingUntilAvailable();
+
+ await LibeufinNexusApi.createUser(nexus, {
+ username: "one",
+ password: "will-be-changed",
+ });
+
+ await LibeufinNexusApi.changePassword(
+ nexus,
+ "one",
+ {
+ newPassword: "got-changed",
+ },
+ {
+ auth: {
+ username: "admin",
+ password: "test",
+ },
+ },
+ );
+
+ let resp = await LibeufinNexusApi.getUser(nexus, {
+ auth: {
+ username: "one",
+ password: "got-changed",
+ },
+ });
+ console.log(resp.data);
+ t.assertTrue(resp.data["username"] == "one" && !resp.data["superuser"]);
+}
+
+runLibeufinApiUsersTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-bad-gateway.ts b/packages/taler-harness/src/integrationtests/test-libeufin-bad-gateway.ts
new file mode 100644
index 000000000..53aacca84
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-bad-gateway.ts
@@ -0,0 +1,74 @@
+/*
+ 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, delayMs } from "../harness/harness.js";
+import {
+ NexusUserBundle,
+ LibeufinNexusApi,
+ LibeufinNexusService,
+ LibeufinSandboxService,
+} from "../harness/libeufin.js";
+
+/**
+ * Testing how Nexus reacts when the Sandbox is unreachable.
+ * Typically, because the user specified a wrong EBICS endpoint.
+ */
+export async function runLibeufinBadGatewayTest(t: GlobalTestState) {
+ /**
+ * User saltetd "01"
+ */
+ const user01nexus = new NexusUserBundle(
+ "01", "http://localhost:5010/not-found", // the EBICS endpoint at Sandbox
+ );
+
+ // Start Nexus
+ const libeufinNexus = await LibeufinNexusService.create(t, {
+ httpPort: 5011,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
+ });
+ await libeufinNexus.start();
+ await libeufinNexus.pingUntilAvailable();
+
+ // Start Sandbox
+ const libeufinSandbox = await LibeufinSandboxService.create(t, {
+ httpPort: 5010,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
+ });
+ await libeufinSandbox.start();
+ await libeufinSandbox.pingUntilAvailable();
+
+ // Connecting to a non-existent Sandbox endpoint.
+ await LibeufinNexusApi.createEbicsBankConnection(
+ libeufinNexus,
+ user01nexus.connReq
+ );
+
+ // 502 Bad Gateway expected.
+ try {
+ await LibeufinNexusApi.connectBankConnection(
+ libeufinNexus,
+ user01nexus.connReq.name,
+ );
+ } catch(e: any) {
+ t.assertTrue(e.response.status == 502);
+ return;
+ }
+ t.assertTrue(false);
+}
+runLibeufinBadGatewayTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-basic.ts b/packages/taler-harness/src/integrationtests/test-libeufin-basic.ts
new file mode 100644
index 000000000..94fd76683
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-basic.ts
@@ -0,0 +1,308 @@
+/*
+ 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 { AbsoluteTime, MerchantContractTerms, Duration } from "@gnu-taler/taler-util";
+import {
+ WalletApiOperation,
+ HarnessExchangeBankAccount,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ DbInfo,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ setupDb,
+ WalletCli,
+} from "../harness/harness.js";
+import { makeTestPayment } from "../harness/helpers.js";
+import {
+ LibeufinNexusApi,
+ LibeufinNexusService,
+ LibeufinSandboxApi,
+ LibeufinSandboxService,
+} from "../harness/libeufin.js";
+
+const exchangeIban = "DE71500105179674997361";
+const customerIban = "DE84500105176881385584";
+const customerBic = "BELADEBEXXX";
+const merchantIban = "DE42500105171245624648";
+
+export interface LibeufinTestEnvironment {
+ commonDb: DbInfo;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ 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.createDemobankAccount(
+ "exchangeacct",
+ "password-unused",
+ { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/access-api/" },
+ exchangeIban
+ );
+ await LibeufinSandboxApi.createDemobankEbicsSubscriber(
+ {
+ hostID: "host01",
+ partnerID: "partner01",
+ userID: "user01",
+ },
+ "exchangeacct",
+ { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/" }
+ );
+
+ await LibeufinSandboxApi.createDemobankAccount(
+ "merchantacct",
+ "password-unused",
+ { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/access-api/" },
+ merchantIban
+ );
+ await LibeufinSandboxApi.createDemobankEbicsSubscriber(
+ {
+ hostID: "host01",
+ partnerID: "partner02",
+ userID: "user02",
+ },
+ "merchantacct",
+ { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/" },
+ );
+
+ 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",
+ });
+
+ await LibeufinNexusApi.createUser(libeufinNexus, {
+ username: "twguser",
+ password: "twgpw",
+ });
+
+ await LibeufinNexusApi.postPermission(libeufinNexus, {
+ action: "grant",
+ permission: {
+ subjectType: "user",
+ subjectId: "twguser",
+ resourceType: "facade",
+ resourceId: "twg1",
+ permissionName: "facade.talerWireGateway.history",
+ },
+ });
+
+ await LibeufinNexusApi.postPermission(libeufinNexus, {
+ action: "grant",
+ permission: {
+ subjectType: "user",
+ subjectId: "twguser",
+ resourceType: "facade",
+ resourceId: "twg1",
+ permissionName: "facade.talerWireGateway.transfer",
+ },
+ });
+
+ 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: HarnessExchangeBankAccount = {
+ accountName: "twguser",
+ accountPassword: "twgpw",
+ 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: Duration.toTalerProtocolDuration(
+ Duration.getZero(),
+ ),
+ });
+
+ 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);
+
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const wr = await wallet.client.call(
+ WalletApiOperation.AcceptManualWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ amount: "EUR:15",
+ },
+ );
+
+ const reservePub: string = wr.reservePub;
+
+ await LibeufinSandboxApi.simulateIncomingTransaction(
+ libeufinSandbox,
+ "exchangeacct",
+ {
+ amount: "15.00",
+ debtorBic: customerBic,
+ debtorIban: customerIban,
+ debtorName: "Jane Customer",
+ subject: `Taler Top-up ${reservePub}`,
+ },
+ );
+
+ await LibeufinNexusApi.fetchTransactions(libeufinNexus, "myacct");
+
+ await exchange.runWirewatchOnce();
+
+ await wallet.runUntilDone();
+
+ const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ console.log("balances", JSON.stringify(bal, undefined, 2));
+ t.assertAmountEquals(bal.balances[0].available, "EUR:14.7");
+
+ const order: Partial<MerchantContractTerms> = {
+ summary: "Buy me!",
+ amount: "EUR:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ wire_transfer_deadline: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
+ };
+
+ 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,
+ );
+}
+runLibeufinBasicTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts b/packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts
new file mode 100644
index 000000000..2ba29656a
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts
@@ -0,0 +1,147 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ launchLibeufinServices,
+ LibeufinNexusApi,
+ NexusUserBundle,
+ SandboxUserBundle,
+} from "../harness/libeufin.js";
+
+/**
+ * This test checks how the C52 and C53 coordinate. It'll test
+ * whether fresh transactions stop showing as C52 after they get
+ * included in a bank statement.
+ */
+export async function runLibeufinC5xTest(t: GlobalTestState) {
+ /**
+ * User saltetd "01"
+ */
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user01sandbox = new SandboxUserBundle("01");
+
+ /**
+ * User saltetd "02".
+ */
+ const user02nexus = new NexusUserBundle(
+ "02",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user02sandbox = new SandboxUserBundle("02");
+
+ /**
+ * Launch Sandbox and Nexus.
+ */
+ const libeufinServices = await launchLibeufinServices(
+ t,
+ [user01nexus, user02nexus],
+ [user01sandbox, user02sandbox],
+ ["twg"],
+ );
+
+ // Check that C52 and C53 have zero entries.
+
+ // C52
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ "all", // range
+ "report", // level
+ );
+ // C53
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ "all", // range
+ "statement", // level
+ );
+ const nexusTxs = await LibeufinNexusApi.getAccountTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+ t.assertTrue(nexusTxs.data["transactions"].length == 0);
+
+ // Addressing one payment to user 01
+ await libeufinServices.libeufinSandbox.makeTransaction(
+ user02sandbox.ebicsBankAccount.label, // debit
+ user01sandbox.ebicsBankAccount.label, // credit
+ "EUR:10",
+ "first payment",
+ );
+
+ let expectOne = await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ "all", // range
+ "report", // C52
+ );
+ t.assertTrue(expectOne.data.newTransactions == 1);
+ t.assertTrue(expectOne.data.downloadedTransactions == 1);
+
+ /* Expect zero payments being downloaded because the
+ * previous request consumed already the one pending
+ * payment.
+ */
+ let expectZero = await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ "all", // range
+ "report", // C52
+ );
+ t.assertTrue(expectZero.data.newTransactions == 0);
+ t.assertTrue(expectZero.data.downloadedTransactions == 0);
+
+ /**
+ * A statement should still account zero payments because
+ * so far the payment made before is still pending.
+ */
+ expectZero = await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ "all", // range
+ "statement", // C53
+ );
+ t.assertTrue(expectZero.data.newTransactions == 0);
+ t.assertTrue(expectZero.data.downloadedTransactions == 0);
+
+ /**
+ * Ticking now. That books any pending transaction.
+ */
+ await libeufinServices.libeufinSandbox.c53tick();
+
+ /**
+ * A statement is now expected to download the transaction,
+ * although that got already ingested along the report
+ * earlier. Thus the transaction counts as downloaded but
+ * not as new.
+ */
+ expectOne = await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ "all", // range
+ "statement", // C53
+ );
+ t.assertTrue(expectOne.data.downloadedTransactions == 1);
+ t.assertTrue(expectOne.data.newTransactions == 0);
+}
+runLibeufinC5xTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts b/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts
new file mode 100644
index 000000000..1ed258c3a
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts
@@ -0,0 +1,169 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ SandboxUserBundle,
+ NexusUserBundle,
+ launchLibeufinServices,
+ LibeufinNexusApi,
+ LibeufinSandboxApi,
+} from "../harness/libeufin.js";
+
+/**
+ * Testing the Anastasis API, offered by the Anastasis facade.
+ */
+export async function runLibeufinAnastasisFacadeTest(t: GlobalTestState) {
+ /**
+ * User saltetd "01"
+ */
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user01sandbox = new SandboxUserBundle("01");
+
+ /**
+ * Launch Sandbox and Nexus.
+ */
+ const libeufinServices = await launchLibeufinServices(
+ t,
+ [user01nexus],
+ [user01sandbox],
+ ["anastasis"], // create only one Anastasis facade.
+ );
+ let resp = await LibeufinNexusApi.getAllFacades(
+ libeufinServices.libeufinNexus,
+ );
+ // check that original facade shows up.
+ t.assertTrue(resp.data["facades"][0]["name"] == user01nexus.anastasisReq["name"]);
+ const anastasisBaseUrl: string = resp.data["facades"][0]["baseUrl"];
+ t.assertTrue(typeof anastasisBaseUrl === "string");
+ t.assertTrue(anastasisBaseUrl.startsWith("http://"));
+ t.assertTrue(anastasisBaseUrl.endsWith("/"));
+
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+
+ await LibeufinNexusApi.postPermission(
+ libeufinServices.libeufinNexus, {
+ action: "grant",
+ permission: {
+ subjectId: user01nexus.userReq.username,
+ subjectType: "user",
+ resourceType: "facade",
+ resourceId: user01nexus.anastasisReq.name,
+ permissionName: "facade.anastasis.history",
+ },
+ }
+ );
+
+ // check if empty.
+ let txsEmpty = await LibeufinNexusApi.getAnastasisTransactions(
+ libeufinServices.libeufinNexus,
+ anastasisBaseUrl, {delta: 5})
+
+ t.assertTrue(txsEmpty.data.incoming_transactions.length == 0);
+
+ LibeufinSandboxApi.simulateIncomingTransaction(
+ libeufinServices.libeufinSandbox,
+ user01sandbox.ebicsBankAccount.label,
+ {
+ debtorIban: "ES3314655813489414469157",
+ debtorBic: "BCMAESM1XXX",
+ debtorName: "Mock Donor",
+ subject: "Anastasis donation",
+ amount: "3", // Sandbox takes currency from its 'config'
+ },
+ )
+
+ LibeufinSandboxApi.simulateIncomingTransaction(
+ libeufinServices.libeufinSandbox,
+ user01sandbox.ebicsBankAccount.label,
+ {
+ debtorIban: "ES3314655813489414469157",
+ debtorBic: "BCMAESM1XXX",
+ debtorName: "Mock Donor",
+ subject: "another Anastasis donation",
+ amount: "1", // Sandbox takes currency from its "config"
+ },
+ )
+
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+
+ let txs = await LibeufinNexusApi.getAnastasisTransactions(
+ libeufinServices.libeufinNexus,
+ anastasisBaseUrl,
+ {delta: 5},
+ user01nexus.userReq.username,
+ user01nexus.userReq.password,
+ );
+
+ // check the two payments show up
+ let txsList = txs.data.incoming_transactions
+ t.assertTrue(txsList.length == 2);
+ t.assertTrue([txsList[0].subject, txsList[1].subject].includes("Anastasis donation"));
+ t.assertTrue([txsList[0].subject, txsList[1].subject].includes("another Anastasis donation"));
+ t.assertTrue(txsList[0].row_id == 1)
+ t.assertTrue(txsList[1].row_id == 2)
+
+ LibeufinSandboxApi.simulateIncomingTransaction(
+ libeufinServices.libeufinSandbox,
+ user01sandbox.ebicsBankAccount.label,
+ {
+ debtorIban: "ES3314655813489414469157",
+ debtorBic: "BCMAESM1XXX",
+ debtorName: "Mock Donor",
+ subject: "last Anastasis donation",
+ amount: "10.10", // Sandbox takes currency from its "config"
+ },
+ )
+
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+
+ let txsLast = await LibeufinNexusApi.getAnastasisTransactions(
+ libeufinServices.libeufinNexus,
+ anastasisBaseUrl,
+ {delta: 5, start: 2},
+ user01nexus.userReq.username,
+ user01nexus.userReq.password,
+ );
+ console.log(txsLast.data.incoming_transactions[0].subject == "last Anastasis donation");
+
+ let txsReverse = await LibeufinNexusApi.getAnastasisTransactions(
+ libeufinServices.libeufinNexus,
+ anastasisBaseUrl,
+ {delta: -5, start: 4},
+ user01nexus.userReq.username,
+ user01nexus.userReq.password,
+ );
+ t.assertTrue(txsReverse.data.incoming_transactions[0].row_id == 3);
+ t.assertTrue(txsReverse.data.incoming_transactions[1].row_id == 2);
+ t.assertTrue(txsReverse.data.incoming_transactions[2].row_id == 1);
+}
+
+runLibeufinAnastasisFacadeTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-keyrotation.ts b/packages/taler-harness/src/integrationtests/test-libeufin-keyrotation.ts
new file mode 100644
index 000000000..21bf07de2
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-keyrotation.ts
@@ -0,0 +1,79 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ SandboxUserBundle,
+ NexusUserBundle,
+ launchLibeufinServices,
+ LibeufinSandboxApi,
+ LibeufinNexusApi,
+} from "../harness/libeufin.js";
+
+/**
+ * Run basic test with LibEuFin.
+ */
+export async function runLibeufinKeyrotationTest(t: GlobalTestState) {
+ /**
+ * User saltetd "01"
+ */
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user01sandbox = new SandboxUserBundle("01");
+
+ /**
+ * Launch Sandbox and Nexus.
+ */
+ const libeufinServices = await launchLibeufinServices(
+ t, [user01nexus], [user01sandbox],
+ );
+
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+
+ /* Rotate the Sandbox keys, and fetch the transactions again */
+ await LibeufinSandboxApi.rotateKeys(
+ libeufinServices.libeufinSandbox,
+ user01sandbox.ebicsBankAccount.subscriber.hostID,
+ );
+
+ try {
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+ } catch (e: any) {
+ /**
+ * Asserting that Nexus responded with a 500 Internal server
+ * error, because the bank signed the last response with a new
+ * key pair that was never downloaded by Nexus.
+ *
+ * NOTE: the bank accepted the request addressed to the old
+ * public key. Should it in this case reject the request even
+ * before trying to verify it?
+ */
+ t.assertTrue(e.response.status == 500);
+ t.assertTrue(e.response.data.code == 9000);
+ }
+}
+runLibeufinKeyrotationTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts b/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts
new file mode 100644
index 000000000..850b0f1d9
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts
@@ -0,0 +1,118 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ SandboxUserBundle,
+ NexusUserBundle,
+ launchLibeufinServices,
+ LibeufinNexusApi,
+ LibeufinCli
+} from "../harness/libeufin.js";
+
+/**
+ * This test checks how the C52 and C53 coordinate. It'll test
+ * whether fresh transactions stop showing as C52 after they get
+ * included in a bank statement.
+ */
+export async function runLibeufinNexusBalanceTest(t: GlobalTestState) {
+ /**
+ * User saltetd "01"
+ */
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user01sandbox = new SandboxUserBundle("01");
+
+ /**
+ * User saltetd "02".
+ */
+ const user02nexus = new NexusUserBundle(
+ "02",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user02sandbox = new SandboxUserBundle("02");
+
+ /**
+ * Launch Sandbox and Nexus.
+ */
+ const libeufinServices = await launchLibeufinServices(
+ t,
+ [user01nexus, user02nexus],
+ [user01sandbox, user02sandbox],
+ ["twg"],
+ );
+
+ // user 01 gets 10
+ await libeufinServices.libeufinSandbox.makeTransaction(
+ user02sandbox.ebicsBankAccount.label, // debit
+ user01sandbox.ebicsBankAccount.label, // credit
+ "EUR:10",
+ "first payment",
+ );
+ // user 01 gets another 10
+ await libeufinServices.libeufinSandbox.makeTransaction(
+ user02sandbox.ebicsBankAccount.label, // debit
+ user01sandbox.ebicsBankAccount.label, // credit
+ "EUR:10",
+ "second payment",
+ );
+
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ "all", // range
+ "report", // level
+ );
+
+ // Check that user 01 has 20, via Nexus.
+ let accountInfo = await LibeufinNexusApi.getBankAccount(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+ t.assertAmountEquals(accountInfo.data.lastSeenBalance, "EUR:20");
+
+ // Booking the first two transactions.
+ await libeufinServices.libeufinSandbox.c53tick();
+
+ // user 01 gives 30
+ await libeufinServices.libeufinSandbox.makeTransaction(
+ user01sandbox.ebicsBankAccount.label,
+ user02sandbox.ebicsBankAccount.label,
+ "EUR:30",
+ "third payment",
+ );
+
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ "all", // range
+ "report", // level
+ );
+
+ let accountInfoDebit = await LibeufinNexusApi.getBankAccount(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+ t.assertDeepEqual(accountInfoDebit.data.lastSeenBalance, "-EUR:10");
+}
+
+runLibeufinNexusBalanceTest.suites = ["libeufin"];
+runLibeufinNexusBalanceTest.excludeByDefault = true;
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-refund-multiple-users.ts b/packages/taler-harness/src/integrationtests/test-libeufin-refund-multiple-users.ts
new file mode 100644
index 000000000..245f34331
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-refund-multiple-users.ts
@@ -0,0 +1,104 @@
+/*
+ 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, delayMs } from "../harness/harness.js";
+import {
+ SandboxUserBundle,
+ NexusUserBundle,
+ launchLibeufinServices,
+ LibeufinSandboxApi,
+ LibeufinNexusApi,
+} from "../harness/libeufin.js";
+
+/**
+ * User 01 expects a refund from user 02, and expectedly user 03
+ * should not be involved in the process.
+ */
+export async function runLibeufinRefundMultipleUsersTest(t: GlobalTestState) {
+ /**
+ * User saltetd "01"
+ */
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user01sandbox = new SandboxUserBundle("01");
+
+ /**
+ * User saltetd "02"
+ */
+ const user02nexus = new NexusUserBundle(
+ "02",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user02sandbox = new SandboxUserBundle("02");
+
+ /**
+ * User saltetd "03"
+ */
+ const user03nexus = new NexusUserBundle(
+ "03",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user03sandbox = new SandboxUserBundle("03");
+
+ /**
+ * Launch Sandbox and Nexus.
+ */
+ const libeufinServices = await launchLibeufinServices(
+ t,
+ [user01nexus, user02nexus],
+ [user01sandbox, user02sandbox],
+ ["twg"],
+ );
+
+ // user 01 gets the payment
+ await libeufinServices.libeufinSandbox.makeTransaction(
+ user02sandbox.ebicsBankAccount.label, // debit
+ user01sandbox.ebicsBankAccount.label, // credit
+ "EUR:1",
+ "not a public key",
+ );
+
+ // user 01 fetches the payments
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+
+ // user 01 tries to submit the reimbursement, as
+ // the payment didn't have a valid public key in
+ // the subject.
+ await LibeufinNexusApi.submitInitiatedPayment(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ "1", // so far the only one that can exist.
+ );
+
+ // user 02 checks whether a reimbursement arrived.
+ let history = await LibeufinSandboxApi.getAccountTransactions(
+ libeufinServices.libeufinSandbox,
+ user02sandbox.ebicsBankAccount["label"],
+ );
+ // reimbursement arrived IFF the total payments are 2:
+ // 1 the original (faulty) transaction + 1 the reimbursement.
+ t.assertTrue(history["payments"].length == 2);
+}
+
+runLibeufinRefundMultipleUsersTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-refund.ts b/packages/taler-harness/src/integrationtests/test-libeufin-refund.ts
new file mode 100644
index 000000000..9d90121a0
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-refund.ts
@@ -0,0 +1,101 @@
+/*
+ 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, delayMs } from "../harness/harness.js";
+import {
+ SandboxUserBundle,
+ NexusUserBundle,
+ launchLibeufinServices,
+ LibeufinSandboxApi,
+ LibeufinNexusApi,
+} from "../harness/libeufin.js";
+
+/**
+ * Run basic test with LibEuFin.
+ */
+export async function runLibeufinRefundTest(t: GlobalTestState) {
+ /**
+ * User saltetd "01"
+ */
+ const user01nexus = new NexusUserBundle(
+ "01",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user01sandbox = new SandboxUserBundle("01");
+
+ /**
+ * User saltetd "02"
+ */
+ const user02nexus = new NexusUserBundle(
+ "02",
+ "http://localhost:5010/ebicsweb",
+ );
+ const user02sandbox = new SandboxUserBundle("02");
+
+ /**
+ * Launch Sandbox and Nexus.
+ */
+ const libeufinServices = await launchLibeufinServices(
+ t,
+ [user01nexus, user02nexus],
+ [user01sandbox, user02sandbox],
+ ["twg"],
+ );
+
+ // user 02 pays user 01 with a faulty (non Taler) subject.
+ await libeufinServices.libeufinSandbox.makeTransaction(
+ user02sandbox.ebicsBankAccount.label, // debit
+ user01sandbox.ebicsBankAccount.label, // credit
+ "EUR:1",
+ "not a public key",
+ );
+
+ // The bad payment should be now ingested and prepared as
+ // a reimbursement.
+ await LibeufinNexusApi.fetchTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+ // Check that the payment arrived at the Nexus.
+ const nexusTxs = await LibeufinNexusApi.getAccountTransactions(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ );
+ t.assertTrue(nexusTxs.data["transactions"].length == 1);
+
+ // Submit the reimbursement
+ await LibeufinNexusApi.submitInitiatedPayment(
+ libeufinServices.libeufinNexus,
+ user01nexus.localAccountName,
+ // The initiated payment (= the reimbursement) ID below
+ // got set by the Taler facade; at this point only one must
+ // exist. If "1" is not found, a 404 will make this test fail.
+ "1",
+ );
+
+ // user 02 checks whether the reimbursement arrived.
+ let history = await LibeufinSandboxApi.getAccountTransactions(
+ libeufinServices.libeufinSandbox,
+ user02sandbox.ebicsBankAccount["label"],
+ );
+ // 2 payments must exist: 1 the original (faulty) payment +
+ // 1 the reimbursement.
+ t.assertTrue(history["payments"].length == 2);
+}
+runLibeufinRefundTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts b/packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts
new file mode 100644
index 000000000..e56cb3d68
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts
@@ -0,0 +1,85 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ LibeufinSandboxApi,
+ LibeufinSandboxService,
+} from "../harness/libeufin.js";
+
+export async function runLibeufinSandboxWireTransferCliTest(
+ t: GlobalTestState,
+) {
+ const sandbox = await LibeufinSandboxService.create(t, {
+ httpPort: 5012,
+ databaseJdbcUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
+ });
+ await sandbox.start();
+ await sandbox.pingUntilAvailable();
+ await LibeufinSandboxApi.createDemobankAccount(
+ "mock-account",
+ "password-unused",
+ { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
+ "DE71500105179674997361"
+ );
+ await LibeufinSandboxApi.createDemobankAccount(
+ "mock-account-2",
+ "password-unused",
+ { baseUrl: sandbox.baseUrl + "/demobanks/default/access-api/" },
+ "DE71500105179674997364"
+ );
+
+ await sandbox.makeTransaction(
+ "mock-account",
+ "mock-account-2",
+ "EUR:1",
+ "one!",
+ );
+ await sandbox.makeTransaction(
+ "mock-account",
+ "mock-account-2",
+ "EUR:1",
+ "two!",
+ );
+ await sandbox.makeTransaction(
+ "mock-account",
+ "mock-account-2",
+ "EUR:1",
+ "three!",
+ );
+ await sandbox.makeTransaction(
+ "mock-account-2",
+ "mock-account",
+ "EUR:1",
+ "Give one back.",
+ );
+ await sandbox.makeTransaction(
+ "mock-account-2",
+ "mock-account",
+ "EUR:0.11",
+ "Give fraction back.",
+ );
+ let ret = await LibeufinSandboxApi.getAccountInfoWithBalance(
+ sandbox,
+ "mock-account-2",
+ );
+ console.log(ret.data.balance);
+ t.assertTrue(ret.data.balance == "EUR:1.89");
+}
+runLibeufinSandboxWireTransferCliTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-tutorial.ts b/packages/taler-harness/src/integrationtests/test-libeufin-tutorial.ts
new file mode 100644
index 000000000..7bc067cfe
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-libeufin-tutorial.ts
@@ -0,0 +1,128 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ LibeufinNexusService,
+ LibeufinSandboxService,
+ LibeufinCli,
+} from "../harness/libeufin.js";
+
+/**
+ * Run basic test with LibEuFin.
+ */
+export async function runLibeufinTutorialTest(t: GlobalTestState) {
+ // Set up test environment
+
+ 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`,
+ });
+
+ const nexusUser = { username: "foo", password: "secret" };
+ const libeufinCli = new LibeufinCli(t, {
+ sandboxUrl: libeufinSandbox.baseUrl,
+ nexusUrl: libeufinNexus.baseUrl,
+ sandboxDatabaseUri: `jdbc:sqlite:${t.testDir}/libeufin-sandbox.sqlite3`,
+ nexusDatabaseUri: `jdbc:sqlite:${t.testDir}/libeufin-nexus.sqlite3`,
+ nexusUser: nexusUser,
+ });
+
+ const ebicsDetails = {
+ hostId: "testhost",
+ partnerId: "partner01",
+ userId: "user01",
+ };
+ const bankAccountDetails = {
+ currency: "EUR",
+ iban: "DE18500105172929531888",
+ bic: "INGDDEFFXXX",
+ personName: "Jane Normal",
+ accountName: "testacct01",
+ };
+
+ await libeufinCli.checkSandbox();
+ await libeufinCli.createEbicsHost("testhost");
+ await libeufinCli.createEbicsSubscriber(ebicsDetails);
+ await libeufinCli.createEbicsBankAccount(ebicsDetails, bankAccountDetails);
+ await libeufinCli.generateTransactions(bankAccountDetails.accountName);
+
+ await libeufinNexus.start();
+ await libeufinNexus.pingUntilAvailable();
+
+ await libeufinNexus.createNexusSuperuser(nexusUser);
+ const connectionDetails = {
+ subscriberDetails: ebicsDetails,
+ ebicsUrl: `${libeufinSandbox.baseUrl}ebicsweb`, // FIXME: need appropriate URL concatenation
+ connectionName: "my-ebics-conn",
+ };
+ await libeufinCli.createEbicsConnection(connectionDetails);
+ await libeufinCli.createBackupFile({
+ passphrase: "secret",
+ outputFile: `${t.testDir}/connection-backup.json`,
+ connectionName: connectionDetails.connectionName,
+ });
+ await libeufinCli.createKeyLetter({
+ outputFile: `${t.testDir}/letter.pdf`,
+ connectionName: connectionDetails.connectionName,
+ });
+ await libeufinCli.connect(connectionDetails.connectionName);
+ await libeufinCli.downloadBankAccounts(connectionDetails.connectionName);
+ await libeufinCli.listOfferedBankAccounts(connectionDetails.connectionName);
+
+ const bankAccountImportDetails = {
+ offeredBankAccountName: bankAccountDetails.accountName,
+ nexusBankAccountName: "at-nexus-testacct01",
+ connectionName: connectionDetails.connectionName,
+ };
+
+ await libeufinCli.importBankAccount(bankAccountImportDetails);
+ await libeufinSandbox.c53tick()
+ await libeufinCli.fetchTransactions(bankAccountImportDetails.nexusBankAccountName);
+ await libeufinCli.transactions(bankAccountImportDetails.nexusBankAccountName);
+
+ const paymentDetails = {
+ creditorIban: "DE42500105171245624648",
+ creditorBic: "BELADEBEXXX",
+ creditorName: "Mina Musterfrau",
+ subject: "Purchase 01234",
+ amount: "1.0",
+ currency: "EUR",
+ nexusBankAccountName: bankAccountImportDetails.nexusBankAccountName,
+ };
+ await libeufinCli.preparePayment(paymentDetails);
+ await libeufinCli.submitPayment(paymentDetails, "1");
+
+ await libeufinCli.newTalerWireGatewayFacade({
+ accountName: bankAccountImportDetails.nexusBankAccountName,
+ connectionName: "my-ebics-conn",
+ currency: "EUR",
+ facadeName: "my-twg",
+ });
+ await libeufinCli.listFacades();
+}
+runLibeufinTutorialTest.suites = ["libeufin"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
new file mode 100644
index 000000000..30ab1cd4b
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts
@@ -0,0 +1,243 @@
+/*
+ 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 {
+ codecForMerchantOrderStatusUnpaid,
+ ConfirmPayResultType,
+ PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import axiosImp from "axios";
+const axios = axiosImp.default;
+import { URL } from "url";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ FaultInjectedExchangeService,
+ FaultInjectedMerchantService,
+} from "../harness/faultInjection.js";
+import {
+ BankService,
+ ExchangeService,
+ getPayto,
+ GlobalTestState,
+ MerchantPrivateApi,
+ MerchantService,
+ setupDb,
+ WalletCli,
+} from "../harness/harness.js";
+import {
+ FaultyMerchantTestEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+
+/**
+ * Run a test case with a simple TESTKUDOS Taler environment, consisting
+ * of one exchange, one bank and one merchant.
+ */
+export async function createConfusedMerchantTestkudosEnvironment(
+ t: GlobalTestState,
+): Promise<FaultyMerchantTestEnvironment> {
+ 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 faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083);
+ const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081);
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(
+ faultyExchange,
+ exchangeBankAccount.accountPaytoUri,
+ );
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ exchange.addOfferedCoins(defaultCoinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ // Confuse the merchant by adding the non-proxied exchange.
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ });
+
+ await merchant.addInstance({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ wallet,
+ bank,
+ exchangeBankAccount,
+ faultyMerchant,
+ faultyExchange,
+ };
+}
+
+/**
+ * Confuse the merchant by having one URL for the same exchange in the config,
+ * but sending coins from the same exchange with a different URL.
+ */
+export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, faultyExchange, faultyMerchant } =
+ await createConfusedMerchantTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange: faultyExchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ /**
+ * =========================================================================
+ * Create an order and let the wallet pay under a session ID
+ *
+ * We check along the way that the JSON response to /orders/{order_id}
+ * returns the right thing.
+ * =========================================================================
+ */
+
+ const merchant = faultyMerchant;
+
+ let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ },
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ sessionId: "mysession-one",
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ t.assertTrue(orderStatus.already_paid_order_id === undefined);
+ let publicOrderStatusUrl = orderStatus.order_status_url;
+
+ let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ console.log(pubUnpaidStatus);
+
+ let preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: pubUnpaidStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
+
+ const proposalId = preparePayResp.proposalId;
+
+ const orderUrlWithHash = new URL(publicOrderStatusUrl);
+ orderUrlWithHash.searchParams.set(
+ "h_contract",
+ preparePayResp.contractTermsHash,
+ );
+
+ console.log("requesting", orderUrlWithHash.href);
+
+ publicOrderStatusResp = await axios.get(orderUrlWithHash.href, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ const confirmPayRes = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId: proposalId,
+ },
+ );
+
+ t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
+}
+
+runMerchantExchangeConfusionTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
new file mode 100644
index 000000000..09231cdd8
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts
@@ -0,0 +1,129 @@
+/*
+ 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 { URL } from "@gnu-taler/taler-util";
+import axiosImp from "axios";
+const axios = axiosImp.default;
+import {
+ ExchangeService,
+ GlobalTestState,
+ MerchantApiClient,
+ MerchantService,
+ setupDb,
+ getPayto,
+} from "../harness/harness.js";
+
+/**
+ * Test instance deletion and authentication for it
+ */
+export async function runMerchantInstancesDeleteTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ 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,
+ });
+
+ // We add the exchange to the config, but note that the exchange won't be started.
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ // Base URL for the default instance.
+ const baseUrl = merchant.makeInstanceBaseUrl();
+
+ {
+ const r = await axios.get(new URL("config", baseUrl).href);
+ console.log(r.data);
+ t.assertDeepEqual(r.data.currency, "TESTKUDOS");
+ }
+
+ // Instances should initially be empty
+ {
+ const r = await axios.get(new URL("management/instances", baseUrl).href);
+ t.assertDeepEqual(r.data.instances, []);
+ }
+
+ // Add an instance, no auth!
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ auth: {
+ method: "external",
+ },
+ });
+
+ // Add an instance, no auth!
+ await merchant.addInstance({
+ id: "myinst",
+ name: "Second Instance",
+ paytoUris: [getPayto("merchant-default")],
+ auth: {
+ method: "external",
+ },
+ });
+
+ let merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), {
+ method: "external",
+ });
+
+ await merchantClient.changeAuth({
+ method: "token",
+ token: "secret-token:foobar",
+ });
+
+ merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), {
+ method: "token",
+ token: "secret-token:foobar",
+ });
+
+ // Check that deleting an instance checks the auth
+ // of the default instance.
+ {
+ const unauthMerchantClient = new MerchantApiClient(
+ merchant.makeInstanceBaseUrl(),
+ {
+ method: "token",
+ token: "secret-token:invalid",
+ },
+ );
+
+ const exc = await t.assertThrowsAsync(async () => {
+ await unauthMerchantClient.deleteInstance("myinst");
+ });
+ console.log("Got expected exception", exc);
+ t.assertAxiosError(exc);
+ t.assertDeepEqual(exc.response?.status, 401);
+ }
+}
+
+runMerchantInstancesDeleteTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
new file mode 100644
index 000000000..a4e44c7f3
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts
@@ -0,0 +1,189 @@
+/*
+ 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 { Duration } from "@gnu-taler/taler-util";
+import axiosImp from "axios";
+const axios = axiosImp.default;
+import {
+ ExchangeService,
+ GlobalTestState,
+ MerchantApiClient,
+ MerchantService,
+ setupDb,
+ getPayto,
+} from "../harness/harness.js";
+
+/**
+ * Do basic checks on instance management and authentication.
+ */
+export async function runMerchantInstancesUrlsTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ 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,
+ });
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ const clientForDefault = new MerchantApiClient(
+ merchant.makeInstanceBaseUrl(),
+ {
+ method: "token",
+ token: "secret-token:i-am-default",
+ },
+ );
+
+ await clientForDefault.createInstance({
+ id: "default",
+ address: {},
+ default_max_deposit_fee: "TESTKUDOS:1",
+ default_max_wire_fee: "TESTKUDOS:1",
+ default_pay_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ seconds: 60 }),
+ ),
+ default_wire_fee_amortization: 1,
+ default_wire_transfer_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ seconds: 60 }),
+ ),
+ jurisdiction: {},
+ name: "My Default Instance",
+ payto_uris: [getPayto("bar")],
+ auth: {
+ method: "token",
+ token: "secret-token:i-am-default",
+ },
+ });
+
+ await clientForDefault.createInstance({
+ id: "myinst",
+ address: {},
+ default_max_deposit_fee: "TESTKUDOS:1",
+ default_max_wire_fee: "TESTKUDOS:1",
+ default_pay_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ seconds: 60 }),
+ ),
+ default_wire_fee_amortization: 1,
+ default_wire_transfer_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ seconds: 60 }),
+ ),
+ jurisdiction: {},
+ name: "My Second Instance",
+ payto_uris: [getPayto("bar")],
+ auth: {
+ method: "token",
+ token: "secret-token:i-am-myinst",
+ },
+ });
+
+ async function check(url: string, token: string, expectedStatus: number) {
+ const resp = await axios.get(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ validateStatus: () => true,
+ });
+ console.log(
+ `checking ${url}, expected ${expectedStatus}, got ${resp.status}`,
+ );
+ t.assertDeepEqual(resp.status, expectedStatus);
+ }
+
+ const tokDefault = "secret-token:i-am-default";
+
+ const defaultBaseUrl = merchant.makeInstanceBaseUrl();
+
+ await check(
+ `${defaultBaseUrl}private/instances/default/instances/default/config`,
+ tokDefault,
+ 404,
+ );
+
+ // Instance management is only available when accessing the default instance
+ // directly.
+ await check(
+ `${defaultBaseUrl}instances/default/private/instances`,
+ "foo",
+ 404,
+ );
+
+ // Non-default instances don't allow instance management.
+ await check(`${defaultBaseUrl}instances/foo/private/instances`, "foo", 404);
+ await check(
+ `${defaultBaseUrl}instances/myinst/private/instances`,
+ "foo",
+ 404,
+ );
+
+ await check(`${defaultBaseUrl}config`, "foo", 200);
+ await check(`${defaultBaseUrl}instances/default/config`, "foo", 200);
+ await check(`${defaultBaseUrl}instances/myinst/config`, "foo", 200);
+ await check(`${defaultBaseUrl}instances/foo/config`, "foo", 404);
+ await check(
+ `${defaultBaseUrl}instances/default/instances/config`,
+ "foo",
+ 404,
+ );
+
+ await check(
+ `${defaultBaseUrl}private/instances/myinst/config`,
+ tokDefault,
+ 404,
+ );
+
+ await check(
+ `${defaultBaseUrl}instances/myinst/private/orders`,
+ tokDefault,
+ 401,
+ );
+
+ await check(
+ `${defaultBaseUrl}instances/myinst/private/orders`,
+ tokDefault,
+ 401,
+ );
+
+ await check(
+ `${defaultBaseUrl}instances/myinst/private/orders`,
+ "secret-token:i-am-myinst",
+ 200,
+ );
+
+ await check(
+ `${defaultBaseUrl}private/instances/myinst/orders`,
+ tokDefault,
+ 404,
+ );
+}
+
+runMerchantInstancesUrlsTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
new file mode 100644
index 000000000..3efe83241
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts
@@ -0,0 +1,184 @@
+/*
+ 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 { URL } from "@gnu-taler/taler-util";
+import axiosImp from "axios";
+const axios = axiosImp.default;
+import {
+ ExchangeService,
+ GlobalTestState,
+ MerchantApiClient,
+ MerchantService,
+ setupDb,
+ getPayto
+} from "../harness/harness.js";
+
+/**
+ * Do basic checks on instance management and authentication.
+ */
+export async function runMerchantInstancesTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ 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,
+ });
+
+ // We add the exchange to the config, but note that the exchange won't be started.
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ // Base URL for the default instance.
+ const baseUrl = merchant.makeInstanceBaseUrl();
+
+ {
+ const r = await axios.get(new URL("config", baseUrl).href);
+ console.log(r.data);
+ t.assertDeepEqual(r.data.currency, "TESTKUDOS");
+ }
+
+ // Instances should initially be empty
+ {
+ const r = await axios.get(new URL("management/instances", baseUrl).href);
+ t.assertDeepEqual(r.data.instances, []);
+ }
+
+ // Add an instance, no auth!
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ auth: {
+ method: "external",
+ },
+ });
+
+ // Add an instance, no auth!
+ await merchant.addInstance({
+ id: "myinst",
+ name: "Second Instance",
+ paytoUris: [getPayto("merchant-default")],
+ auth: {
+ method: "external",
+ },
+ });
+
+ let merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), {
+ method: "external",
+ });
+
+ {
+ const r = await merchantClient.getInstances();
+ t.assertDeepEqual(r.instances.length, 2);
+ }
+
+ // Check that a "malformed" bearer Authorization header gets ignored
+ {
+ const url = merchant.makeInstanceBaseUrl();
+ const resp = await axios.get(new URL("management/instances", url).href, {
+ headers: {
+ Authorization: "foo bar-baz",
+ },
+ });
+ t.assertDeepEqual(resp.status, 200);
+ }
+
+ {
+ const fullDetails = await merchantClient.getInstanceFullDetails("default");
+ t.assertDeepEqual(fullDetails.auth.method, "external");
+ }
+
+ await merchantClient.changeAuth({
+ method: "token",
+ token: "secret-token:foobar",
+ });
+
+ // Now this should fail, as we didn't change the auth of the client yet.
+ const exc = await t.assertThrowsAsync(async () => {
+ console.log("requesting instances with auth", merchantClient.auth);
+ const resp = await merchantClient.getInstances();
+ console.log("instances result:", resp);
+ });
+
+ console.log(exc);
+
+ t.assertAxiosError(exc);
+ t.assertTrue(exc.response?.status === 401);
+
+ merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), {
+ method: "token",
+ token: "secret-token:foobar",
+ });
+
+ // With the new client auth settings, request should work again.
+ await merchantClient.getInstances();
+
+ // Now, try some variations.
+ {
+ const url = merchant.makeInstanceBaseUrl();
+ const resp = await axios.get(new URL("management/instances", url).href, {
+ headers: {
+ // Note the spaces
+ Authorization: "Bearer secret-token:foobar",
+ },
+ });
+ t.assertDeepEqual(resp.status, 200);
+ }
+
+ // Check that auth is reported properly
+ {
+ const fullDetails = await merchantClient.getInstanceFullDetails("default");
+ t.assertDeepEqual(fullDetails.auth.method, "token");
+ // Token should *not* be reported back.
+ t.assertDeepEqual(fullDetails.auth.token, undefined);
+ }
+
+ // Check that deleting an instance checks the auth
+ // of the default instance.
+ {
+ const unauthMerchantClient = new MerchantApiClient(
+ merchant.makeInstanceBaseUrl(),
+ {
+ method: "external",
+ },
+ );
+
+ const exc = await t.assertThrowsAsync(async () => {
+ await unauthMerchantClient.deleteInstance("myinst");
+ });
+ console.log(exc);
+ t.assertAxiosError(exc);
+ t.assertDeepEqual(exc.response?.status, 401);
+ }
+}
+
+runMerchantInstancesTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts b/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts
new file mode 100644
index 000000000..4b9f53f05
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts
@@ -0,0 +1,162 @@
+/*
+ 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, MerchantPrivateApi } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
+import {
+ PreparePayResultType,
+ codecForMerchantOrderStatusUnpaid,
+ ConfirmPayResultType,
+ URL,
+} from "@gnu-taler/taler-util";
+import axiosImp from "axios";
+const axios = axiosImp.default;
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runMerchantLongpollingTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ /**
+ * =========================================================================
+ * Create an order and let the wallet pay under a session ID
+ *
+ * We check along the way that the JSON response to /orders/{order_id}
+ * returns the right thing.
+ * =========================================================================
+ */
+
+ let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ },
+ create_token: false,
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ sessionId: "mysession-one",
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ t.assertTrue(orderStatus.already_paid_order_id === undefined);
+ let publicOrderStatusUrl = new URL(orderStatus.order_status_url);
+
+ // First, request order status without longpolling
+ {
+ console.log("requesting", publicOrderStatusUrl.href);
+ let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (before claiming, no long polling), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+ }
+
+ // Now do long-polling for half a second!
+ publicOrderStatusUrl.searchParams.set("timeout_ms", "500");
+
+ console.log("requesting", publicOrderStatusUrl.href);
+ let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (before claiming, with long-polling), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ console.log(pubUnpaidStatus);
+
+ /**
+ * =========================================================================
+ * Now actually pay, but WHILE a long poll is active!
+ * =========================================================================
+ */
+
+ let preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: pubUnpaidStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
+
+ publicOrderStatusUrl.searchParams.set("timeout_ms", "5000");
+ publicOrderStatusUrl.searchParams.set(
+ "h_contract",
+ preparePayResp.contractTermsHash,
+ );
+
+ let publicOrderStatusPromise = axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
+
+ const proposalId = preparePayResp.proposalId;
+
+ publicOrderStatusResp = await publicOrderStatusPromise;
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ const confirmPayRes = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId: proposalId,
+ },
+ );
+
+ t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
+}
+
+runMerchantLongpollingTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts
new file mode 100644
index 000000000..5d9b23fa7
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts
@@ -0,0 +1,303 @@
+/*
+ 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,
+ MerchantPrivateApi,
+ MerchantServiceInterface,
+ WalletCli,
+ ExchangeServiceInterface,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+import {
+ URL,
+ durationFromSpec,
+ PreparePayResultType,
+ Duration,
+} from "@gnu-taler/taler-util";
+import axiosImp from "axios";
+const axios = axiosImp.default;
+import {
+ WalletApiOperation,
+ BankServiceHandle,
+} from "@gnu-taler/taler-wallet-core";
+
+async function testRefundApiWithFulfillmentUrl(
+ t: GlobalTestState,
+ env: {
+ merchant: MerchantServiceInterface;
+ bank: BankServiceHandle;
+ wallet: WalletCli;
+ exchange: ExchangeServiceInterface;
+ },
+): Promise<void> {
+ const { wallet, bank, exchange, merchant } = env;
+
+ // Set up order.
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/fulfillment",
+ },
+ refund_delay: Duration.toTalerProtocolDuration(
+ durationFromSpec({ minutes: 5 }),
+ ),
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ const talerPayUri = orderStatus.taler_pay_uri;
+ const orderId = orderResp.order_id;
+
+ // Make wallet pay for the order
+
+ let preparePayResult = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: preparePayResult.proposalId,
+ });
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ preparePayResult = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.AlreadyConfirmed,
+ );
+
+ await MerchantPrivateApi.giveRefund(merchant, {
+ amount: "TESTKUDOS:5",
+ instance: "default",
+ justification: "foo",
+ orderId: orderResp.order_id,
+ });
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5");
+
+ // Now test what the merchant gives as a response for various requests to the
+ // public order status URL!
+
+ let publicOrderStatusUrl = new URL(
+ `orders/${orderId}`,
+ merchant.makeInstanceBaseUrl(),
+ );
+ publicOrderStatusUrl.searchParams.set(
+ "h_contract",
+ preparePayResult.contractTermsHash,
+ );
+
+ let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+ console.log(publicOrderStatusResp.data);
+ t.assertTrue(publicOrderStatusResp.status === 200);
+ t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5");
+
+ publicOrderStatusUrl = new URL(
+ `orders/${orderId}`,
+ merchant.makeInstanceBaseUrl(),
+ );
+ console.log(`requesting order status via '${publicOrderStatusUrl.href}'`);
+ publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+ console.log(publicOrderStatusResp.status);
+ console.log(publicOrderStatusResp.data);
+ // We didn't give any authentication, so we should get a fulfillment URL back
+ t.assertTrue(publicOrderStatusResp.status === 403);
+}
+
+async function testRefundApiWithFulfillmentMessage(
+ t: GlobalTestState,
+ env: {
+ merchant: MerchantServiceInterface;
+ bank: BankServiceHandle;
+ wallet: WalletCli;
+ exchange: ExchangeServiceInterface;
+ },
+): Promise<void> {
+ const { wallet, bank, exchange, merchant } = env;
+
+ // Set up order.
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_message: "Thank you for buying foobar",
+ },
+ refund_delay: Duration.toTalerProtocolDuration(
+ durationFromSpec({ minutes: 5 }),
+ ),
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ const talerPayUri = orderStatus.taler_pay_uri;
+ const orderId = orderResp.order_id;
+
+ // Make wallet pay for the order
+
+ let preparePayResult = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: preparePayResult.proposalId,
+ });
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ preparePayResult = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.AlreadyConfirmed,
+ );
+
+ await MerchantPrivateApi.giveRefund(merchant, {
+ amount: "TESTKUDOS:5",
+ instance: "default",
+ justification: "foo",
+ orderId: orderResp.order_id,
+ });
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:5");
+
+ // Now test what the merchant gives as a response for various requests to the
+ // public order status URL!
+
+ let publicOrderStatusUrl = new URL(
+ `orders/${orderId}`,
+ merchant.makeInstanceBaseUrl(),
+ );
+ publicOrderStatusUrl.searchParams.set(
+ "h_contract",
+ preparePayResult.contractTermsHash,
+ );
+
+ let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+ console.log(publicOrderStatusResp.data);
+ t.assertTrue(publicOrderStatusResp.status === 200);
+ t.assertAmountEquals(publicOrderStatusResp.data.refund_amount, "TESTKUDOS:5");
+
+ publicOrderStatusUrl = new URL(
+ `orders/${orderId}`,
+ merchant.makeInstanceBaseUrl(),
+ );
+
+ publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+ console.log(publicOrderStatusResp.data);
+ // We didn't give any authentication, so we should get a fulfillment URL back
+ t.assertTrue(publicOrderStatusResp.status === 403);
+}
+
+/**
+ * Test case for the refund API of the merchant backend.
+ */
+export async function runMerchantRefundApiTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ await testRefundApiWithFulfillmentUrl(t, {
+ wallet,
+ bank,
+ exchange,
+ merchant,
+ });
+
+ await testRefundApiWithFulfillmentMessage(t, {
+ wallet,
+ bank,
+ exchange,
+ merchant,
+ });
+}
+
+runMerchantRefundApiTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts
new file mode 100644
index 000000000..70edaaf0c
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts
@@ -0,0 +1,620 @@
+/*
+ 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 {
+ ConfirmPayResultType,
+ PreparePayResultType,
+ URL,
+ encodeCrock,
+ getRandomBytes,
+} from "@gnu-taler/taler-util";
+import { NodeHttpLib, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantPrivateApi,
+ MerchantService,
+ WalletCli,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+
+const httpLib = new NodeHttpLib();
+
+interface Context {
+ merchant: MerchantService;
+ merchantBaseUrl: string;
+ bank: BankService;
+ exchange: ExchangeService;
+}
+
+async function testWithClaimToken(
+ t: GlobalTestState,
+ c: Context,
+): Promise<void> {
+ const wallet = new WalletCli(t, "withclaimtoken");
+ const { bank, exchange } = c;
+ const { merchant, merchantBaseUrl } = c;
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const sessionId = "mysession";
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ });
+
+ const claimToken = orderResp.token;
+ const orderId = orderResp.order_id;
+ t.assertTrue(!!claimToken);
+ let talerPayUri: string;
+
+ {
+ const httpResp = await httpLib.get(
+ new URL(`orders/${orderId}`, merchantBaseUrl).href,
+ );
+ const r = await httpResp.json();
+ t.assertDeepEqual(httpResp.status, 202);
+ console.log(r);
+ }
+
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ url.searchParams.set("token", claimToken);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ t.assertDeepEqual(httpResp.status, 402);
+ console.log(r);
+ talerPayUri = r.taler_pay_uri;
+ t.assertTrue(!!talerPayUri);
+ }
+
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ url.searchParams.set("token", claimToken);
+ const httpResp = await httpLib.get(url.href, {
+ headers: {
+ Accept: "text/html",
+ },
+ });
+ const r = await httpResp.text();
+ t.assertDeepEqual(httpResp.status, 402);
+ console.log(r);
+ }
+
+ const preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
+ const contractTermsHash = preparePayResp.contractTermsHash;
+ const proposalId = preparePayResp.proposalId;
+
+ // claimed, unpaid, access with wrong h_contract
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const hcWrong = encodeCrock(getRandomBytes(64));
+ url.searchParams.set("h_contract", hcWrong);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 403);
+ }
+
+ // claimed, unpaid, access with wrong claim token
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const ctWrong = encodeCrock(getRandomBytes(16));
+ url.searchParams.set("token", ctWrong);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 403);
+ }
+
+ // claimed, unpaid, access with correct claim token
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ url.searchParams.set("token", claimToken);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 402);
+ }
+
+ // claimed, unpaid, access with correct contract terms hash
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ url.searchParams.set("h_contract", contractTermsHash);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 402);
+ }
+
+ // claimed, unpaid, access without credentials
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 202);
+ }
+
+ const confirmPayRes = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId: proposalId,
+ },
+ );
+
+ t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
+
+ // paid, access without credentials
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 202);
+ }
+
+ // paid, access with wrong h_contract
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const hcWrong = encodeCrock(getRandomBytes(64));
+ url.searchParams.set("h_contract", hcWrong);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 403);
+ }
+
+ // paid, access with wrong claim token
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const ctWrong = encodeCrock(getRandomBytes(16));
+ url.searchParams.set("token", ctWrong);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 403);
+ }
+
+ // paid, access with correct h_contract
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ url.searchParams.set("h_contract", contractTermsHash);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 200);
+ }
+
+ // paid, access with correct claim token, JSON
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ url.searchParams.set("token", claimToken);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 200);
+ const respFulfillmentUrl = r.fulfillment_url;
+ t.assertDeepEqual(respFulfillmentUrl, "https://example.com/article42");
+ }
+
+ // paid, access with correct claim token, HTML
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ url.searchParams.set("token", claimToken);
+ const httpResp = await httpLib.get(url.href, {
+ headers: { Accept: "text/html" },
+ });
+ t.assertDeepEqual(httpResp.status, 200);
+ }
+
+ const confirmPayRes2 = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId: proposalId,
+ sessionId: sessionId,
+ },
+ );
+
+ t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done);
+
+ // Create another order with identical fulfillment URL to test the "already paid" flow
+ const alreadyPaidOrderResp = await MerchantPrivateApi.createOrder(
+ merchant,
+ "default",
+ {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ },
+ );
+
+ const apOrderId = alreadyPaidOrderResp.order_id;
+ const apToken = alreadyPaidOrderResp.token;
+ t.assertTrue(!!apToken);
+
+ {
+ const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
+ url.searchParams.set("token", apToken);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 402);
+ }
+
+ // Check for already paid session ID, JSON
+ {
+ const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
+ url.searchParams.set("token", apToken);
+ url.searchParams.set("session_id", sessionId);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 402);
+ const alreadyPaidOrderId = r.already_paid_order_id;
+ t.assertDeepEqual(alreadyPaidOrderId, orderId);
+ }
+
+ // Check for already paid session ID, HTML
+ {
+ const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
+ url.searchParams.set("token", apToken);
+ url.searchParams.set("session_id", sessionId);
+ const httpResp = await httpLib.get(url.href, {
+ headers: { Accept: "text/html" },
+ });
+ t.assertDeepEqual(httpResp.status, 302);
+ const location = httpResp.headers.get("Location");
+ console.log("location header:", location);
+ t.assertDeepEqual(location, "https://example.com/article42");
+ }
+}
+
+async function testWithoutClaimToken(
+ t: GlobalTestState,
+ c: Context,
+): Promise<void> {
+ const wallet = new WalletCli(t, "withoutct");
+ const sessionId = "mysession2";
+ const { bank, exchange } = c;
+ const { merchant, merchantBaseUrl } = c;
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ create_token: false,
+ });
+
+ const orderId = orderResp.order_id;
+ let talerPayUri: string;
+
+ {
+ const httpResp = await httpLib.get(
+ new URL(`orders/${orderId}`, merchantBaseUrl).href,
+ );
+ const r = await httpResp.json();
+ t.assertDeepEqual(httpResp.status, 402);
+ console.log(r);
+ }
+
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ t.assertDeepEqual(httpResp.status, 402);
+ console.log(r);
+ talerPayUri = r.taler_pay_uri;
+ t.assertTrue(!!talerPayUri);
+ }
+
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const httpResp = await httpLib.get(url.href, {
+ headers: {
+ Accept: "text/html",
+ },
+ });
+ const r = await httpResp.text();
+ t.assertDeepEqual(httpResp.status, 402);
+ console.log(r);
+ }
+
+ const preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri,
+ },
+ );
+
+ console.log(preparePayResp);
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
+ const contractTermsHash = preparePayResp.contractTermsHash;
+ const proposalId = preparePayResp.proposalId;
+
+ // claimed, unpaid, access with wrong h_contract
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const hcWrong = encodeCrock(getRandomBytes(64));
+ url.searchParams.set("h_contract", hcWrong);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 403);
+ }
+
+ // claimed, unpaid, access with wrong claim token
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const ctWrong = encodeCrock(getRandomBytes(16));
+ url.searchParams.set("token", ctWrong);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 403);
+ }
+
+ // claimed, unpaid, no claim token
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 402);
+ }
+
+ // claimed, unpaid, access with correct contract terms hash
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ url.searchParams.set("h_contract", contractTermsHash);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 402);
+ }
+
+ // claimed, unpaid, access without credentials
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ // No credentials, but the order doesn't require a claim token.
+ // This effectively means that the order ID is already considered
+ // enough authentication, at least to check for the basic order status
+ t.assertDeepEqual(httpResp.status, 402);
+ }
+
+ const confirmPayRes = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId: proposalId,
+ },
+ );
+
+ t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
+
+ // paid, access without credentials
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 200);
+ }
+
+ // paid, access with wrong h_contract
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const hcWrong = encodeCrock(getRandomBytes(64));
+ url.searchParams.set("h_contract", hcWrong);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 403);
+ }
+
+ // paid, access with wrong claim token
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const ctWrong = encodeCrock(getRandomBytes(16));
+ url.searchParams.set("token", ctWrong);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 403);
+ }
+
+ // paid, access with correct h_contract
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ url.searchParams.set("h_contract", contractTermsHash);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 200);
+ }
+
+ // paid, JSON
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 200);
+ const respFulfillmentUrl = r.fulfillment_url;
+ t.assertDeepEqual(respFulfillmentUrl, "https://example.com/article42");
+ }
+
+ // paid, HTML
+ {
+ const url = new URL(`orders/${orderId}`, merchantBaseUrl);
+ const httpResp = await httpLib.get(url.href, {
+ headers: { Accept: "text/html" },
+ });
+ t.assertDeepEqual(httpResp.status, 200);
+ }
+
+ const confirmPayRes2 = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId: proposalId,
+ sessionId: sessionId,
+ },
+ );
+
+ t.assertTrue(confirmPayRes2.type === ConfirmPayResultType.Done);
+
+ // Create another order with identical fulfillment URL to test the "already paid" flow
+ const alreadyPaidOrderResp = await MerchantPrivateApi.createOrder(
+ merchant,
+ "default",
+ {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ },
+ );
+
+ const apOrderId = alreadyPaidOrderResp.order_id;
+ const apToken = alreadyPaidOrderResp.token;
+ t.assertTrue(!!apToken);
+
+ {
+ const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
+ url.searchParams.set("token", apToken);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 402);
+ }
+
+ // Check for already paid session ID, JSON
+ {
+ const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
+ url.searchParams.set("token", apToken);
+ url.searchParams.set("session_id", sessionId);
+ const httpResp = await httpLib.get(url.href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 402);
+ const alreadyPaidOrderId = r.already_paid_order_id;
+ t.assertDeepEqual(alreadyPaidOrderId, orderId);
+ }
+
+ // Check for already paid session ID, HTML
+ {
+ const url = new URL(`orders/${apOrderId}`, merchantBaseUrl);
+ url.searchParams.set("token", apToken);
+ url.searchParams.set("session_id", sessionId);
+ const httpResp = await httpLib.get(url.href, {
+ headers: { Accept: "text/html" },
+ });
+ t.assertDeepEqual(httpResp.status, 302);
+ const location = httpResp.headers.get("Location");
+ console.log("location header:", location);
+ t.assertDeepEqual(location, "https://example.com/article42");
+ }
+}
+
+/**
+ * Checks for the /orders/{id} endpoint of the merchant.
+ *
+ * The tests here should exercise all code paths in the executable
+ * specification of the endpoint.
+ */
+export async function runMerchantSpecPublicOrdersTest(t: GlobalTestState) {
+ const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment(
+ t,
+ );
+
+ // Base URL for the default instance.
+ const merchantBaseUrl = merchant.makeInstanceBaseUrl();
+
+ {
+ const httpResp = await httpLib.get(new URL("config", merchantBaseUrl).href);
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(r.currency, "TESTKUDOS");
+ }
+
+ {
+ const httpResp = await httpLib.get(
+ new URL("orders/foo", merchantBaseUrl).href,
+ );
+ const r = await httpResp.json();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 404);
+ // FIXME: also check Taler error code
+ }
+
+ {
+ const httpResp = await httpLib.get(
+ new URL("orders/foo", merchantBaseUrl).href,
+ {
+ headers: {
+ Accept: "text/html",
+ },
+ },
+ );
+ const r = await httpResp.text();
+ console.log(r);
+ t.assertDeepEqual(httpResp.status, 404);
+ // FIXME: also check Taler error code
+ }
+
+ await testWithClaimToken(t, {
+ merchant,
+ merchantBaseUrl,
+ exchange,
+ bank,
+ });
+
+ await testWithoutClaimToken(t, {
+ merchant,
+ merchantBaseUrl,
+ exchange,
+ bank,
+ });
+}
+
+runMerchantSpecPublicOrdersTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-pay-paid.ts b/packages/taler-harness/src/integrationtests/test-pay-paid.ts
new file mode 100644
index 000000000..2ef91e4a8
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-pay-paid.ts
@@ -0,0 +1,222 @@
+/*
+ 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, MerchantPrivateApi } from "../harness/harness.js";
+import {
+ withdrawViaBank,
+ createFaultInjectedMerchantTestkudosEnvironment,
+} from "../harness/helpers.js";
+import {
+ PreparePayResultType,
+ codecForMerchantOrderStatusUnpaid,
+ ConfirmPayResultType,
+ URL,
+} from "@gnu-taler/taler-util";
+import axiosImp from "axios";
+const axios = axiosImp.default;
+import { FaultInjectionRequestContext } from "../harness/faultInjection.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for the wallets repurchase detection mechanism
+ * based on the fulfillment URL.
+ *
+ * FIXME: This test is now almost the same as test-paywall-flow,
+ * since we can't initiate payment via a "claimed" private order status
+ * response.
+ */
+export async function runPayPaidTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, faultyExchange, faultyMerchant } =
+ await createFaultInjectedMerchantTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange: faultyExchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ /**
+ * =========================================================================
+ * Create an order and let the wallet pay under a session ID
+ *
+ * We check along the way that the JSON response to /orders/{order_id}
+ * returns the right thing.
+ * =========================================================================
+ */
+
+ const merchant = faultyMerchant;
+
+ let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ sessionId: "mysession-one",
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ t.assertTrue(orderStatus.already_paid_order_id === undefined);
+ let publicOrderStatusUrl = orderStatus.order_status_url;
+
+ let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ console.log(pubUnpaidStatus);
+
+ let preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: pubUnpaidStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
+
+ const proposalId = preparePayResp.proposalId;
+
+ publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ const confirmPayRes = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId: proposalId,
+ },
+ );
+
+ t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
+
+ publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
+ validateStatus: () => true,
+ });
+
+ console.log(publicOrderStatusResp.data);
+
+ if (publicOrderStatusResp.status != 200) {
+ console.log(publicOrderStatusResp.data);
+ throw Error(
+ `expected status 200 (after paying), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ /**
+ * =========================================================================
+ * Now change up the session ID and do payment re-play!
+ * =========================================================================
+ */
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ sessionId: "mysession-two",
+ });
+
+ console.log(
+ "order status under mysession-two:",
+ JSON.stringify(orderStatus, undefined, 2),
+ );
+
+ // Should be claimed (not paid!) because of a new session ID
+ t.assertTrue(orderStatus.order_status === "claimed");
+
+ let numPayRequested = 0;
+ let numPaidRequested = 0;
+
+ faultyMerchant.faultProxy.addFault({
+ async modifyRequest(ctx: FaultInjectionRequestContext) {
+ const url = new URL(ctx.requestUrl);
+ if (url.pathname.endsWith("/pay")) {
+ numPayRequested++;
+ } else if (url.pathname.endsWith("/paid")) {
+ numPaidRequested++;
+ }
+ },
+ });
+
+ let orderRespTwo = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ });
+
+ let orderStatusTwo = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderRespTwo.order_id,
+ sessionId: "mysession-two",
+ },
+ );
+
+ t.assertTrue(orderStatusTwo.order_status === "unpaid");
+
+ // Pay with new taler://pay URI, which should
+ // have the new session ID!
+ // Wallet should now automatically re-play payment.
+ preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatusTwo.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed);
+ t.assertTrue(preparePayResp.paid);
+
+ // Make sure the wallet is actually doing the replay properly.
+ t.assertTrue(numPaidRequested == 1);
+ t.assertTrue(numPayRequested == 0);
+}
+
+runPayPaidTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-payment-claim.ts b/packages/taler-harness/src/integrationtests/test-payment-claim.ts
new file mode 100644
index 000000000..e93d2c44c
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-claim.ts
@@ -0,0 +1,110 @@
+/*
+ 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,
+ MerchantPrivateApi,
+ WalletCli,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+import { PreparePayResultType } from "@gnu-taler/taler-util";
+import { TalerErrorCode } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runPaymentClaimTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ const walletTwo = new WalletCli(t, "two");
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ // Set up order.
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ },
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ const talerPayUri = orderStatus.taler_pay_uri;
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ t.assertThrowsTalerErrorAsync(async () => {
+ await walletTwo.client.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri,
+ });
+ });
+
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: preparePayResult.proposalId,
+ });
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ walletTwo.deleteDatabase();
+
+ const err = await t.assertThrowsTalerErrorAsync(async () => {
+ await walletTwo.client.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri,
+ });
+ });
+
+ t.assertTrue(err.hasErrorCode(TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED));
+
+ await t.shutdown();
+}
+
+runPaymentClaimTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-payment-fault.ts b/packages/taler-harness/src/integrationtests/test-payment-fault.ts
new file mode 100644
index 000000000..dea538e35
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-fault.ts
@@ -0,0 +1,222 @@
+/*
+ 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/>
+ */
+
+/**
+ * Sample fault injection test.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ GlobalTestState,
+ MerchantService,
+ ExchangeService,
+ setupDb,
+ BankService,
+ WalletCli,
+ MerchantPrivateApi,
+ getPayto,
+} from "../harness/harness.js";
+import {
+ FaultInjectedExchangeService,
+ FaultInjectionRequestContext,
+ FaultInjectionResponseContext,
+} from "../harness/faultInjection.js";
+import { CoreApiResponse } from "@gnu-taler/taler-util";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ WalletApiOperation,
+ BankApi,
+ BankAccessApi,
+} from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runPaymentFaultTest(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 exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ await exchange.addBankAccount("1", exchangeBankAccount);
+ exchange.addOfferedCoins(defaultCoinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ const faultyExchange = new FaultInjectedExchangeService(t, exchange, 8091);
+
+ // Print all requests to the exchange
+ faultyExchange.faultProxy.addFault({
+ async modifyRequest(ctx: FaultInjectionRequestContext) {
+ console.log("got request", ctx);
+ },
+ async modifyResponse(ctx: FaultInjectionResponseContext) {
+ console.log("got response", ctx);
+ },
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ merchant.addExchange(faultyExchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ });
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ // Create withdrawal operation
+
+ const user = await BankApi.createRandomBankUser(bank);
+ const wop = await BankAccessApi.createWithdrawalOperation(
+ bank,
+ user,
+ "TESTKUDOS:20",
+ );
+
+ // Hand it to the wallet
+
+ await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ });
+
+ await wallet.runPending();
+
+ // Withdraw
+
+ await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
+ exchangeBaseUrl: faultyExchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ });
+ await wallet.runPending();
+
+ // Confirm it
+
+ await BankApi.confirmWithdrawalOperation(bank, user, wop);
+
+ await wallet.runUntilDone();
+
+ // Check balance
+
+ await wallet.client.call(WalletApiOperation.GetBalances, {});
+
+ // Set up order.
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ },
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ let apiResp: CoreApiResponse;
+
+ const prepResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ const proposalId = prepResp.proposalId;
+
+ await wallet.runPending();
+
+ // Drop 3 responses from the exchange.
+ let faultCount = 0;
+ faultyExchange.faultProxy.addFault({
+ async modifyResponse(ctx: FaultInjectionResponseContext) {
+ if (!ctx.request.requestUrl.endsWith("/deposit")) {
+ return;
+ }
+ if (faultCount < 3) {
+ console.log(`blocking /deposit request #${faultCount}`);
+ faultCount++;
+ ctx.dropResponse = true;
+ } else {
+ console.log(`letting through /deposit request #${faultCount}`);
+ }
+ },
+ });
+
+ // confirmPay won't work, as the exchange is unreachable
+
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ // FIXME: should be validated, don't cast!
+ proposalId: proposalId,
+ });
+
+ await wallet.runUntilDone();
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+}
+
+runPaymentFaultTest.suites = ["wallet"];
+runPaymentFaultTest.timeoutMs = 120000;
diff --git a/packages/taler-harness/src/integrationtests/test-payment-forgettable.ts b/packages/taler-harness/src/integrationtests/test-payment-forgettable.ts
new file mode 100644
index 000000000..3bdd6bef3
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-forgettable.ts
@@ -0,0 +1,81 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+ makeTestPayment,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for payment with a contract that has forgettable fields.
+ */
+export async function runPaymentForgettableTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ {
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ extra: {
+ foo: { bar: "baz" },
+ $forgettable: {
+ foo: "gnu",
+ },
+ },
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ }
+
+ console.log("testing with forgettable field without hash");
+
+ {
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ extra: {
+ foo: { bar: "baz" },
+ $forgettable: {
+ foo: true,
+ },
+ },
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ }
+
+ await wallet.runUntilDone();
+}
+
+runPaymentForgettableTest.suites = ["wallet", "merchant"];
diff --git a/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts b/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts
new file mode 100644
index 000000000..1099a8188
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-idempotency.ts
@@ -0,0 +1,121 @@
+/*
+ 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, MerchantPrivateApi } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+import { PreparePayResultType } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Test the wallet-core payment API, especially that repeated operations
+ * return the expected result.
+ */
+export async function runPaymentIdempotencyTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ // Set up order.
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ },
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ const talerPayUri = orderStatus.taler_pay_uri;
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ const preparePayResultRep = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+ t.assertTrue(
+ preparePayResultRep.status === PreparePayResultType.PaymentPossible,
+ );
+
+ const proposalId = preparePayResult.proposalId;
+
+ const confirmPayResult = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId: proposalId,
+ },
+ );
+
+ console.log("confirm pay result", confirmPayResult);
+
+ await wallet.runUntilDone();
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ const preparePayResultAfter = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri,
+ },
+ );
+
+ console.log("result after:", preparePayResultAfter);
+
+ t.assertTrue(
+ preparePayResultAfter.status === PreparePayResultType.AlreadyConfirmed,
+ );
+ t.assertTrue(preparePayResultAfter.paid === true);
+
+ await t.shutdown();
+}
+
+runPaymentIdempotencyTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
new file mode 100644
index 000000000..46325c05f
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts
@@ -0,0 +1,163 @@
+/*
+ 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,
+ setupDb,
+ BankService,
+ ExchangeService,
+ MerchantService,
+ WalletCli,
+ MerchantPrivateApi,
+ getPayto
+} from "../harness/harness.js";
+import { withdrawViaBank } from "../harness/helpers.js";
+import { coin_ct10, coin_u1 } from "../harness/denomStructures.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+async function setupTest(
+ t: GlobalTestState,
+): Promise<{
+ merchant: MerchantService;
+ exchange: ExchangeService;
+ bank: BankService;
+}> {
+ 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 exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+
+ exchange.addOfferedCoins([coin_ct10, coin_u1]);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ await exchange.addBankAccount("1", exchangeBankAccount);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ });
+
+ await merchant.addInstance({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ return {
+ merchant,
+ bank,
+ exchange,
+ };
+}
+
+/**
+ * Run test.
+ *
+ * This test uses a very sub-optimal denomination structure.
+ */
+export async function runPaymentMultipleTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { merchant, bank, exchange } = await setupTest(t);
+
+ 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 MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:80",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ },
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
+
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ // FIXME: should be validated, don't cast!
+ proposalId: r1.proposalId,
+ });
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ await t.shutdown();
+}
+
+runPaymentMultipleTest.suites = ["wallet"];
+runPaymentMultipleTest.timeoutMs = 120000;
diff --git a/packages/taler-harness/src/integrationtests/test-payment-on-demo.ts b/packages/taler-harness/src/integrationtests/test-payment-on-demo.ts
new file mode 100644
index 000000000..737620ce7
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-on-demo.ts
@@ -0,0 +1,114 @@
+/*
+ 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, WalletCli } from "../harness/harness.js";
+import { makeTestPayment } from "../harness/helpers.js";
+import {
+ WalletApiOperation,
+ BankApi,
+ BankAccessApi,
+ BankServiceHandle,
+ NodeHttpLib,
+} from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runPaymentDemoTest(t: GlobalTestState) {
+ // Withdraw digital cash into the wallet.
+ let bankInterface: BankServiceHandle = {
+ baseUrl: "https://bank.demo.taler.net/",
+ bankAccessApiBaseUrl: "https://bank.demo.taler.net/",
+ http: new NodeHttpLib(),
+ };
+ let user = await BankApi.createRandomBankUser(bankInterface);
+ let wop = await BankAccessApi.createWithdrawalOperation(
+ bankInterface,
+ user,
+ "KUDOS:20",
+ );
+
+ let wallet = new WalletCli(t);
+ await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ });
+
+ await wallet.runPending();
+
+ // Confirm it
+
+ await BankApi.confirmWithdrawalOperation(bankInterface, user, wop);
+
+ // Withdraw
+
+ await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ });
+ await wallet.runUntilDone();
+
+ let balanceBefore = await wallet.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ t.assertTrue(balanceBefore["balances"].length == 1);
+
+ const order = {
+ summary: "Buy me!",
+ amount: "KUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ let merchant = {
+ makeInstanceBaseUrl: function (instanceName?: string) {
+ return "https://backend.demo.taler.net/instances/donations/";
+ },
+ port: 0,
+ name: "donations",
+ };
+
+ t.assertTrue("TALER_ENV_FRONTENDS_APITOKEN" in process.env);
+
+ await makeTestPayment(
+ t,
+ {
+ merchant,
+ wallet,
+ order,
+ },
+ {
+ Authorization: `Bearer ${process.env["TALER_ENV_FRONTENDS_APITOKEN"]}`,
+ },
+ );
+
+ await wallet.runUntilDone();
+
+ let balanceAfter = await wallet.client.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+ t.assertTrue(balanceAfter["balances"].length == 1);
+ t.assertTrue(
+ balanceBefore["balances"][0]["available"] >
+ balanceAfter["balances"][0]["available"],
+ );
+}
+
+runPaymentDemoTest.excludeByDefault = true;
+runPaymentDemoTest.suites = ["buildbot"];
diff --git a/packages/taler-harness/src/integrationtests/test-payment-transient.ts b/packages/taler-harness/src/integrationtests/test-payment-transient.ts
new file mode 100644
index 000000000..b57b355c6
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-transient.ts
@@ -0,0 +1,185 @@
+/*
+ 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, MerchantPrivateApi } from "../harness/harness.js";
+import {
+ withdrawViaBank,
+ createFaultInjectedMerchantTestkudosEnvironment,
+} from "../harness/helpers.js";
+import {
+ FaultInjectionResponseContext,
+} from "../harness/faultInjection.js";
+import {
+ codecForMerchantOrderStatusUnpaid,
+ ConfirmPayResultType,
+ PreparePayResultType,
+ TalerErrorCode,
+ TalerErrorDetail,
+ URL,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import axiosImp from "axios";
+const axios = axiosImp.default;
+
+/**
+ * Run test for a payment where the merchant has a transient
+ * failure in /pay
+ */
+export async function runPaymentTransientTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet,
+ bank,
+ exchange,
+ faultyMerchant,
+ } = await createFaultInjectedMerchantTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ const merchant = faultyMerchant;
+
+ let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ sessionId: "mysession-one",
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ t.assertTrue(orderStatus.already_paid_order_id === undefined);
+ let publicOrderStatusUrl = orderStatus.order_status_url;
+
+ let publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+
+
+ throw Error(
+ `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ console.log(pubUnpaidStatus);
+
+ let preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: pubUnpaidStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
+
+ const proposalId = preparePayResp.proposalId;
+
+ publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ let faultInjected = false;
+
+ faultyMerchant.faultProxy.addFault({
+ async modifyResponse(ctx: FaultInjectionResponseContext) {
+ console.log("in modifyResponse");
+ const url = new URL(ctx.request.requestUrl);
+ console.log("pathname is", url.pathname);
+ if (!url.pathname.endsWith("/pay")) {
+ return;
+ }
+ if (faultInjected) {
+ console.log("not injecting pay fault");
+ return;
+ }
+ faultInjected = true;
+ console.log("injecting pay fault");
+ const err: TalerErrorDetail = {
+ code: TalerErrorCode.GENERIC_DB_COMMIT_FAILED,
+ hint: "something went wrong",
+ };
+ ctx.responseBody = Buffer.from(JSON.stringify(err));
+ ctx.statusCode = 500;
+ },
+ });
+
+ const confirmPayResp = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId,
+ },
+ );
+
+ console.log(confirmPayResp);
+
+ t.assertTrue(confirmPayResp.type === ConfirmPayResultType.Pending);
+ t.assertTrue(faultInjected);
+
+ const confirmPayRespTwo = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId,
+ },
+ );
+
+ t.assertTrue(confirmPayRespTwo.type === ConfirmPayResultType.Done);
+
+ // Now ask the merchant if paid
+
+ console.log("requesting", publicOrderStatusUrl);
+ publicOrderStatusResp = await axios.get(publicOrderStatusUrl, {
+ validateStatus: () => true,
+ });
+
+ console.log(publicOrderStatusResp.data);
+
+ if (publicOrderStatusResp.status != 200) {
+ console.log(publicOrderStatusResp.data);
+ throw Error(
+ `expected status 200 (after paying), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+}
+
+runPaymentTransientTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-payment-zero.ts b/packages/taler-harness/src/integrationtests/test-payment-zero.ts
new file mode 100644
index 000000000..c38b8b382
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment-zero.ts
@@ -0,0 +1,72 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+ makeTestPayment,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for a payment for a "free" order with
+ * an amount of zero.
+ */
+export async function runPaymentZeroTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironment(t);
+
+ // First, make a "free" payment when we don't even have
+ // any money in the
+
+ // Withdraw digital cash into the wallet.
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ await wallet.runUntilDone();
+
+ await makeTestPayment(t, {
+ wallet,
+ merchant,
+ order: {
+ summary: "I am free!",
+ amount: "TESTKUDOS:0",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ },
+ });
+
+ await wallet.runUntilDone();
+
+ const transactions = await wallet.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ for (const tr of transactions.transactions) {
+ t.assertDeepEqual(tr.pending, false);
+ }
+}
+
+runPaymentZeroTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-payment.ts b/packages/taler-harness/src/integrationtests/test-payment.ts
new file mode 100644
index 000000000..66d10f996
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-payment.ts
@@ -0,0 +1,77 @@
+/*
+ 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 } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+ makeTestPayment,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runPaymentTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ wallet,
+ bank,
+ exchange,
+ merchant,
+ } = await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+ await wallet.runUntilDone();
+
+ // Test JSON normalization of contract terms: Does the wallet
+ // agree with the merchant?
+ const order2 = {
+ summary: "Testing “unicode” characters",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order: order2 });
+ await wallet.runUntilDone();
+
+ // Test JSON normalization of contract terms: Does the wallet
+ // agree with the merchant?
+ const order3 = {
+ summary: "Testing\nNewlines\rAnd\tStuff\nHere\b",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order: order3 });
+
+ await wallet.runUntilDone();
+}
+
+runPaymentTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-paywall-flow.ts b/packages/taler-harness/src/integrationtests/test-paywall-flow.ts
new file mode 100644
index 000000000..a9601c625
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-paywall-flow.ts
@@ -0,0 +1,252 @@
+/*
+ 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, MerchantPrivateApi } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+import {
+ PreparePayResultType,
+ codecForMerchantOrderStatusUnpaid,
+ ConfirmPayResultType,
+ URL,
+} from "@gnu-taler/taler-util";
+import axiosImp from "axios";
+const axios = axiosImp.default;
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runPaywallFlowTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ /**
+ * =========================================================================
+ * Create an order and let the wallet pay under a session ID
+ *
+ * We check along the way that the JSON response to /orders/{order_id}
+ * returns the right thing.
+ * =========================================================================
+ */
+
+ let orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ });
+
+ const firstOrderId = orderResp.order_id;
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ sessionId: "mysession-one",
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ const talerPayUriOne = orderStatus.taler_pay_uri;
+
+ t.assertTrue(orderStatus.already_paid_order_id === undefined);
+ let publicOrderStatusUrl = new URL(orderStatus.order_status_url);
+
+ let publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ console.log(pubUnpaidStatus);
+
+ let preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: pubUnpaidStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
+
+ const proposalId = preparePayResp.proposalId;
+
+ console.log("requesting", publicOrderStatusUrl.href);
+ publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+ console.log("response body", publicOrderStatusResp.data);
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(
+ `expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ const confirmPayRes = await wallet.client.call(
+ WalletApiOperation.ConfirmPay,
+ {
+ proposalId: proposalId,
+ },
+ );
+
+ t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done);
+
+ publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+
+ console.log(publicOrderStatusResp.data);
+
+ if (publicOrderStatusResp.status != 200) {
+ console.log(publicOrderStatusResp.data);
+ throw Error(
+ `expected status 200 (after paying), but got ${publicOrderStatusResp.status}`,
+ );
+ }
+
+ /**
+ * =========================================================================
+ * Now change up the session ID!
+ * =========================================================================
+ */
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ sessionId: "mysession-two",
+ });
+
+ // Should be claimed (not paid!) because of a new session ID
+ t.assertTrue(orderStatus.order_status === "claimed");
+
+ // Pay with new taler://pay URI, which should
+ // have the new session ID!
+ // Wallet should now automatically re-play payment.
+ preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: talerPayUriOne,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed);
+ t.assertTrue(preparePayResp.paid);
+
+ /**
+ * =========================================================================
+ * Now we test re-purchase detection.
+ * =========================================================================
+ */
+
+ orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ // Same fulfillment URL as previously!
+ fulfillment_url: "https://example.com/article42",
+ public_reorder_url: "https://example.com/article42-share",
+ },
+ });
+
+ const secondOrderId = orderResp.order_id;
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: secondOrderId,
+ sessionId: "mysession-three",
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ t.assertTrue(orderStatus.already_paid_order_id === undefined);
+ publicOrderStatusUrl = new URL(orderStatus.order_status_url);
+
+ // Here the re-purchase detection should kick in,
+ // and the wallet should re-pay for the old order
+ // under the new session ID (mysession-three).
+ preparePayResp = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed);
+ t.assertTrue(preparePayResp.paid);
+
+ // The first order should now be paid under "mysession-three",
+ // as the wallet did re-purchase detection
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: firstOrderId,
+ sessionId: "mysession-three",
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ // Check that with a completely new session ID, the status would NOT
+ // be paid.
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: firstOrderId,
+ sessionId: "mysession-four",
+ });
+
+ t.assertTrue(orderStatus.order_status === "claimed");
+
+ // Now check if the public status of the new order is correct.
+
+ console.log("requesting public status", publicOrderStatusUrl);
+
+ // Ask the order status of the claimed-but-unpaid order
+ publicOrderStatusResp = await axios.get(publicOrderStatusUrl.href, {
+ validateStatus: () => true,
+ });
+
+ if (publicOrderStatusResp.status != 402) {
+ throw Error(`expected status 402, but got ${publicOrderStatusResp.status}`);
+ }
+
+ pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
+ publicOrderStatusResp.data,
+ );
+
+ console.log(publicOrderStatusResp.data);
+
+ t.assertTrue(pubUnpaidStatus.already_paid_order_id === firstOrderId);
+}
+
+runPaywallFlowTest.suites = ["merchant", "wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
new file mode 100644
index 000000000..211f20494
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts
@@ -0,0 +1,101 @@
+/*
+ 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 { AbsoluteTime, Duration, j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, WalletCli } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runPeerToPeerPullTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange, merchant } = await createSimpleTestkudosEnvironment(
+ t,
+ );
+
+ // Withdraw digital cash into the wallet.
+ const wallet1 = new WalletCli(t, "w1");
+ const wallet2 = new WalletCli(t, "w2");
+ await withdrawViaBank(t, {
+ wallet: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wallet1.runUntilDone();
+
+ const purse_expiration = AbsoluteTime.toTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ const resp = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPullPayment,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:5",
+ purse_expiration
+ },
+ },
+ );
+
+ const checkResp = await wallet2.client.call(
+ WalletApiOperation.CheckPeerPullPayment,
+ {
+ talerUri: resp.talerUri,
+ },
+ );
+
+ console.log(`checkResp: ${j2s(checkResp)}`);
+
+ const acceptResp = await wallet2.client.call(
+ WalletApiOperation.AcceptPeerPullPayment,
+ {
+ peerPullPaymentIncomingId: checkResp.peerPullPaymentIncomingId,
+ },
+ );
+
+ await wallet1.runUntilDone();
+ await wallet2.runUntilDone();
+
+ const txn1 = await wallet1.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ const txn2 = await wallet2.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
+}
+
+runPeerToPeerPullTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
new file mode 100644
index 000000000..4aaeca624
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts
@@ -0,0 +1,119 @@
+/*
+ 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 { AbsoluteTime, Duration, j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, WalletCli } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runPeerToPeerPushTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironment(t);
+
+ const wallet1 = new WalletCli(t, "w1");
+ const wallet2 = new WalletCli(t, "w2");
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, {
+ wallet: wallet1,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wallet1.runUntilDone();
+
+ const purse_expiration = AbsoluteTime.toTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ days: 2 }),
+ ),
+ );
+
+ {
+ const resp = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPushPayment,
+ {
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:5",
+ purse_expiration
+ },
+ },
+ );
+
+ console.log(resp);
+
+ }
+ const resp = await wallet1.client.call(
+ WalletApiOperation.InitiatePeerPushPayment,
+ {
+ partialContractTerms: {
+ summary: "Hello World",
+ amount: "TESTKUDOS:5",
+ purse_expiration
+ },
+ },
+ );
+
+ console.log(resp);
+
+ const checkResp = await wallet2.client.call(
+ WalletApiOperation.CheckPeerPushPayment,
+ {
+ talerUri: resp.talerUri,
+ },
+ );
+
+ console.log(checkResp);
+
+ const acceptResp = await wallet2.client.call(
+ WalletApiOperation.AcceptPeerPushPayment,
+ {
+ peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId,
+ },
+ );
+
+ console.log(acceptResp);
+
+ await wallet1.runUntilDone();
+ await wallet2.runUntilDone();
+
+ const txn1 = await wallet1.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ const txn2 = await wallet2.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log(`txn1: ${j2s(txn1)}`);
+ console.log(`txn2: ${j2s(txn2)}`);
+}
+
+runPeerToPeerPushTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-refund-auto.ts b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
new file mode 100644
index 000000000..4c2a2f94a
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-refund-auto.ts
@@ -0,0 +1,105 @@
+/*
+ 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, MerchantPrivateApi } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+import { Duration, durationFromSpec } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runRefundAutoTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ // Set up order.
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ auto_refund: {
+ d_us: 3000 * 1000,
+ },
+ },
+ refund_delay: Duration.toTalerProtocolDuration(
+ durationFromSpec({ minutes: 5 }),
+ ),
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
+
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ // FIXME: should be validated, don't cast!
+ proposalId: r1.proposalId,
+ });
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ const ref = await MerchantPrivateApi.giveRefund(merchant, {
+ amount: "TESTKUDOS:5",
+ instance: "default",
+ justification: "foo",
+ orderId: orderResp.order_id,
+ });
+
+ console.log(ref);
+
+ // The wallet should now automatically pick up the refund.
+ await wallet.runUntilDone();
+
+ const transactions = await wallet.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log(JSON.stringify(transactions, undefined, 2));
+
+ const transactionTypes = transactions.transactions.map((x) => x.type);
+ t.assertDeepEqual(transactionTypes, ["withdrawal", "payment", "refund"]);
+
+ await t.shutdown();
+}
+
+runRefundAutoTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-refund-gone.ts b/packages/taler-harness/src/integrationtests/test-refund-gone.ts
new file mode 100644
index 000000000..b6cefda86
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-refund-gone.ts
@@ -0,0 +1,124 @@
+/*
+ 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, MerchantPrivateApi } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+ applyTimeTravel,
+} from "../harness/helpers.js";
+import {
+ AbsoluteTime,
+ Duration,
+ durationFromSpec,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runRefundGoneTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ // Set up order.
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ pay_deadline: AbsoluteTime.toTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ durationFromSpec({
+ minutes: 10,
+ }),
+ ),
+ ),
+ },
+ refund_delay: Duration.toTalerProtocolDuration(
+ durationFromSpec({ minutes: 1 }),
+ ),
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
+
+ const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: r1.proposalId,
+ });
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ console.log(orderStatus);
+
+ await applyTimeTravel(durationFromSpec({ hours: 1 }), { exchange, wallet });
+
+ await exchange.runAggregatorOnce();
+
+ const ref = await MerchantPrivateApi.giveRefund(merchant, {
+ amount: "TESTKUDOS:5",
+ instance: "default",
+ justification: "foo",
+ orderId: orderResp.order_id,
+ });
+
+ console.log(ref);
+
+ let rr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
+ talerRefundUri: ref.talerRefundUri,
+ });
+
+ console.log("refund response:", rr);
+ t.assertAmountEquals(rr.amountRefundGone, "TESTKUDOS:5");
+
+ await wallet.runUntilDone();
+
+ let r = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ console.log(JSON.stringify(r, undefined, 2));
+
+ const r3 = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ console.log(JSON.stringify(r3, undefined, 2));
+
+ await t.shutdown();
+}
+
+runRefundGoneTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-refund-incremental.ts b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts
new file mode 100644
index 000000000..8d1f6e873
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-refund-incremental.ts
@@ -0,0 +1,202 @@
+/*
+ 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,
+ delayMs,
+ MerchantPrivateApi,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+import {
+ TransactionType,
+ Amounts,
+ durationFromSpec,
+ Duration,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runRefundIncrementalTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ // Set up order.
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:10",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ },
+ refund_delay: Duration.toTalerProtocolDuration(
+ durationFromSpec({ minutes: 5 }),
+ ),
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
+
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: r1.proposalId,
+ });
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ let ref = await MerchantPrivateApi.giveRefund(merchant, {
+ amount: "TESTKUDOS:2.5",
+ instance: "default",
+ justification: "foo",
+ orderId: orderResp.order_id,
+ });
+
+ console.log("first refund increase response", ref);
+
+ {
+ let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
+ talerRefundUri: ref.talerRefundUri,
+ });
+ console.log(wr);
+ const txs = await wallet.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log(
+ "transactions after applying first refund:",
+ JSON.stringify(txs, undefined, 2),
+ );
+ }
+
+ // Wait at least a second, because otherwise the increased
+ // refund will be grouped with the previous one.
+ await delayMs(1200);
+
+ ref = await MerchantPrivateApi.giveRefund(merchant, {
+ amount: "TESTKUDOS:5",
+ instance: "default",
+ justification: "bar",
+ orderId: orderResp.order_id,
+ });
+
+ console.log("second refund increase response", ref);
+
+ // Wait at least a second, because otherwise the increased
+ // refund will be grouped with the previous one.
+ await delayMs(1200);
+
+ ref = await MerchantPrivateApi.giveRefund(merchant, {
+ amount: "TESTKUDOS:10",
+ instance: "default",
+ justification: "bar",
+ orderId: orderResp.order_id,
+ });
+
+ console.log("third refund increase response", ref);
+
+ {
+ let wr = await wallet.client.call(WalletApiOperation.ApplyRefund, {
+ talerRefundUri: ref.talerRefundUri,
+ });
+ console.log(wr);
+ }
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ t.assertAmountEquals(orderStatus.refund_amount, "TESTKUDOS:10");
+
+ console.log(JSON.stringify(orderStatus, undefined, 2));
+
+ await wallet.runUntilDone();
+
+ const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ console.log(JSON.stringify(bal, undefined, 2));
+
+ {
+ const txs = await wallet.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log(JSON.stringify(txs, undefined, 2));
+
+ const txTypes = txs.transactions.map((x) => x.type);
+ t.assertDeepEqual(txTypes, [
+ "withdrawal",
+ "payment",
+ "refund",
+ "refund",
+ "refund",
+ ]);
+
+ for (const tx of txs.transactions) {
+ if (tx.type !== TransactionType.Refund) {
+ continue;
+ }
+ t.assertAmountLeq(tx.amountEffective, tx.amountRaw);
+ }
+
+ const raw = Amounts.sum(
+ txs.transactions
+ .filter((x) => x.type === TransactionType.Refund)
+ .map((x) => x.amountRaw),
+ ).amount;
+
+ t.assertAmountEquals("TESTKUDOS:10", raw);
+
+ const effective = Amounts.sum(
+ txs.transactions
+ .filter((x) => x.type === TransactionType.Refund)
+ .map((x) => x.amountEffective),
+ ).amount;
+
+ t.assertAmountEquals("TESTKUDOS:8.59", effective);
+ }
+
+ await t.shutdown();
+}
+
+runRefundIncrementalTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-refund.ts b/packages/taler-harness/src/integrationtests/test-refund.ts
new file mode 100644
index 000000000..b63dad590
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-refund.ts
@@ -0,0 +1,106 @@
+/*
+ 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 { Duration, durationFromSpec } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runRefundTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ // Set up order.
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ },
+ refund_delay: Duration.toTalerProtocolDuration(
+ durationFromSpec({ minutes: 5 }),
+ ),
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const r1 = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
+
+ await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: r1.proposalId,
+ });
+
+ // Check if payment was successful.
+
+ orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+
+ const ref = await MerchantPrivateApi.giveRefund(merchant, {
+ amount: "TESTKUDOS:5",
+ instance: "default",
+ justification: "foo",
+ orderId: orderResp.order_id,
+ });
+
+ console.log(ref);
+
+ let r = await wallet.client.call(WalletApiOperation.ApplyRefund, {
+ talerRefundUri: ref.talerRefundUri,
+ });
+ console.log(r);
+
+ await wallet.runUntilDone();
+
+ {
+ const r2 = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ console.log(JSON.stringify(r2, undefined, 2));
+ }
+ {
+ const r2 = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ console.log(JSON.stringify(r2, undefined, 2));
+ }
+
+ await t.shutdown();
+}
+
+runRefundTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts
new file mode 100644
index 000000000..0fbb4960e
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-revocation.ts
@@ -0,0 +1,215 @@
+/*
+ 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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig } from "../harness/denomStructures.js";
+import {
+ GlobalTestState,
+ ExchangeService,
+ MerchantService,
+ WalletCli,
+ setupDb,
+ BankService,
+ delayMs,
+ getPayto,
+} from "../harness/harness.js";
+import {
+ withdrawViaBank,
+ makeTestPayment,
+ SimpleTestEnvironment,
+} from "../harness/helpers.js";
+
+async function revokeAllWalletCoins(req: {
+ wallet: WalletCli;
+ exchange: ExchangeService;
+ merchant: MerchantService;
+}): Promise<void> {
+ const { wallet, exchange, merchant } = req;
+ const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+ console.log(coinDump);
+ const usedDenomHashes = new Set<string>();
+ for (const coin of coinDump.coins) {
+ usedDenomHashes.add(coin.denom_pub_hash);
+ }
+ for (const x of usedDenomHashes.values()) {
+ await exchange.revokeDenomination(x);
+ }
+ await delayMs(1000);
+ await exchange.keyup();
+ await delayMs(1000);
+ await merchant.stop();
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+}
+
+async function createTestEnvironment(
+ t: GlobalTestState,
+): Promise<SimpleTestEnvironment> {
+ 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);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coin_u1: CoinConfig = {
+ cipher: "RSA" as const,
+ durationLegal: "3 years",
+ durationSpend: "2 years",
+ durationWithdraw: "7 days",
+ rsaKeySize: 1024,
+ name: `TESTKUDOS_u1`,
+ value: `TESTKUDOS:1`,
+ feeDeposit: `TESTKUDOS:0`,
+ feeRefresh: `TESTKUDOS:0`,
+ feeRefund: `TESTKUDOS:0`,
+ feeWithdraw: `TESTKUDOS:0`,
+ };
+
+ exchange.addCoinConfigList([coin_u1]);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ });
+
+ await merchant.addInstance({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ wallet,
+ bank,
+ exchangeBankAccount,
+ };
+}
+
+/**
+ * Basic time travel test.
+ */
+export async function runRevocationTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } = await createTestEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
+
+ console.log("revoking first time");
+ await revokeAllWalletCoins({ wallet, exchange, merchant });
+
+ // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565
+ // is implemented.
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ forceUpdate: true,
+ });
+ await wallet.runUntilDone();
+ await wallet.runUntilDone();
+ const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ console.log("wallet balance", bal);
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:10",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ await makeTestPayment(t, { wallet, merchant, order });
+
+ wallet.deleteDatabase();
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
+
+ const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+ console.log(coinDump);
+ const coinPubList = coinDump.coins.map((x) => x.coin_pub);
+ await wallet.client.call(WalletApiOperation.ForceRefresh, {
+ coinPubList,
+ });
+ await wallet.runUntilDone();
+
+ console.log("revoking second time");
+ await revokeAllWalletCoins({ wallet, exchange, merchant });
+
+ // FIXME: this shouldn't be necessary once https://bugs.taler.net/n/6565
+ // is implemented.
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ forceUpdate: true,
+ });
+ await wallet.runUntilDone();
+ await wallet.runUntilDone();
+ {
+ const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ console.log("wallet balance", bal);
+ }
+
+ await makeTestPayment(t, { wallet, merchant, order });
+}
+
+runRevocationTest.timeoutMs = 120000;
+runRevocationTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
new file mode 100644
index 000000000..54b66e0b2
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts
@@ -0,0 +1,216 @@
+/*
+ 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 {
+ ConfirmPayResultType,
+ Duration,
+ durationFromSpec,
+ PreparePayResultType,
+} from "@gnu-taler/taler-util";
+import {
+ PendingOperationsResponse,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { makeNoFeeCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantPrivateApi,
+ MerchantService,
+ setupDb,
+ WalletCli,
+ getPayto
+} from "../harness/harness.js";
+import { startWithdrawViaBank, withdrawViaBank } from "../harness/helpers.js";
+
+async function applyTimeTravel(
+ timetravelDuration: Duration,
+ s: {
+ exchange?: ExchangeService;
+ merchant?: MerchantService;
+ wallet?: WalletCli;
+ },
+): Promise<void> {
+ if (s.exchange) {
+ await s.exchange.stop();
+ s.exchange.setTimetravel(timetravelDuration);
+ await s.exchange.start();
+ await s.exchange.pingUntilAvailable();
+ }
+
+ if (s.merchant) {
+ await s.merchant.stop();
+ s.merchant.setTimetravel(timetravelDuration);
+ await s.merchant.start();
+ await s.merchant.pingUntilAvailable();
+ }
+
+ if (s.wallet) {
+ console.log("setting wallet time travel to", timetravelDuration);
+ s.wallet.setTimetravel(timetravelDuration);
+ }
+}
+
+/**
+ * Basic time travel test.
+ */
+export async function runTimetravelAutorefreshTest(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);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ exchange.addCoinConfigList(makeNoFeeCoinConfig("TESTKUDOS"));
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [getPayto("merchant-default")],
+ });
+
+ await merchant.addInstance({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [getPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
+
+ // Travel into the future, the deposit expiration is two years
+ // into the future.
+ console.log("applying first time travel");
+ await applyTimeTravel(durationFromSpec({ days: 400 }), {
+ wallet,
+ exchange,
+ merchant,
+ });
+
+ await wallet.runUntilDone();
+
+ let p: PendingOperationsResponse;
+ p = await wallet.client.call(WalletApiOperation.GetPendingOperations, {});
+
+ console.log("pending operations after first time travel");
+ console.log(JSON.stringify(p, undefined, 2));
+
+ await startWithdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ await wallet.runUntilDone();
+
+ // Travel into the future, the deposit expiration is two years
+ // into the future.
+ console.log("applying second time travel");
+ await applyTimeTravel(durationFromSpec({ years: 2, months: 6 }), {
+ wallet,
+ exchange,
+ merchant,
+ });
+
+ // At this point, the original coins should've been refreshed.
+ // It would be too late to refresh them now, as we're past
+ // the two year deposit expiration.
+
+ await wallet.runUntilDone();
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order: {
+ fulfillment_url: "http://example.com",
+ summary: "foo",
+ amount: "TESTKUDOS:30",
+ },
+ });
+
+ const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderResp.order_id,
+ instance: "default",
+ },
+ );
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ const r = await wallet.client.call(WalletApiOperation.PreparePayForUri, {
+ talerPayUri: orderStatus.taler_pay_uri,
+ });
+
+ console.log(r);
+
+ t.assertTrue(r.status === PreparePayResultType.PaymentPossible);
+
+ const cpr = await wallet.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: r.proposalId,
+ });
+
+ t.assertTrue(cpr.type === ConfirmPayResultType.Done);
+}
+
+runTimetravelAutorefreshTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts
new file mode 100644
index 000000000..9335af9f5
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts
@@ -0,0 +1,98 @@
+/*
+ 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 { Duration, TransactionType } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ startWithdrawViaBank,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+
+/**
+ * Basic time travel test.
+ */
+export async function runTimetravelWithdrawTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:15" });
+
+ // Travel 400 days into the future,
+ // as the deposit expiration is two years
+ // into the future.
+ const timetravelDuration: Duration = {
+ d_ms: 1000 * 60 * 60 * 24 * 400,
+ };
+
+ await exchange.stop();
+ exchange.setTimetravel(timetravelDuration);
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+ await exchange.keyup();
+
+ await merchant.stop();
+ merchant.setTimetravel(timetravelDuration);
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ console.log("starting withdrawal via bank");
+
+ // This should fail, as the wallet didn't time travel yet.
+ await startWithdrawViaBank(t, {
+ wallet,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+
+ console.log("starting withdrawal done");
+
+ // Check that transactions are correct for the failed withdrawal
+ {
+ console.log("running until done (should run into maxRetries limit)");
+ await wallet.runUntilDone({ maxRetries: 5 });
+ console.log("wallet done running");
+ const transactions = await wallet.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log(transactions);
+ const types = transactions.transactions.map((x) => x.type);
+ t.assertDeepEqual(types, ["withdrawal", "withdrawal"]);
+ const wtrans = transactions.transactions[1];
+ t.assertTrue(wtrans.type === TransactionType.Withdrawal);
+ t.assertTrue(wtrans.pending);
+ }
+
+ // Now we also let the wallet time travel
+
+ wallet.setTimetravel(timetravelDuration);
+
+ // This doesn't work yet, see https://bugs.taler.net/n/6585
+
+ // await wallet.runUntilDone({ maxRetries: 5 });
+}
+
+runTimetravelWithdrawTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-tipping.ts b/packages/taler-harness/src/integrationtests/test-tipping.ts
new file mode 100644
index 000000000..d31e0c06b
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-tipping.ts
@@ -0,0 +1,129 @@
+/*
+ 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 { WalletApiOperation, BankApi } from "@gnu-taler/taler-wallet-core";
+import {
+ GlobalTestState,
+ MerchantPrivateApi,
+ getWireMethodForTest,
+} from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runTippingTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, merchant, exchangeBankAccount } =
+ await createSimpleTestkudosEnvironment(t);
+
+ const mbu = await BankApi.createRandomBankUser(bank);
+
+ const tipReserveResp = await MerchantPrivateApi.createTippingReserve(
+ merchant,
+ "default",
+ {
+ exchange_url: exchange.baseUrl,
+ initial_balance: "TESTKUDOS:10",
+ wire_method: getWireMethodForTest(),
+ },
+ );
+
+ console.log("tipReserveResp:", tipReserveResp);
+
+ t.assertDeepEqual(
+ tipReserveResp.payto_uri,
+ exchangeBankAccount.accountPaytoUri,
+ );
+
+ await BankApi.adminAddIncoming(bank, {
+ amount: "TESTKUDOS:10",
+ debitAccountPayto: mbu.accountPaytoUri,
+ exchangeBankAccount,
+ reservePub: tipReserveResp.reserve_pub,
+ });
+
+ await exchange.runWirewatchOnce();
+
+ await merchant.stop();
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ const r = await MerchantPrivateApi.queryTippingReserves(merchant, "default");
+ console.log("tipping reserves:", JSON.stringify(r, undefined, 2));
+
+ t.assertTrue(r.reserves.length === 1);
+ t.assertDeepEqual(
+ r.reserves[0].exchange_initial_amount,
+ r.reserves[0].merchant_initial_amount,
+ );
+
+ const tip = await MerchantPrivateApi.giveTip(merchant, "default", {
+ amount: "TESTKUDOS:5",
+ justification: "why not?",
+ next_url: "https://example.com/after-tip",
+ });
+
+ console.log("created tip", tip);
+
+ const doTip = async (): Promise<void> => {
+ const ptr = await wallet.client.call(WalletApiOperation.PrepareTip, {
+ talerTipUri: tip.taler_tip_uri,
+ });
+
+ console.log(ptr);
+
+ t.assertAmountEquals(ptr.tipAmountRaw, "TESTKUDOS:5");
+ t.assertAmountEquals(ptr.tipAmountEffective, "TESTKUDOS:4.85");
+
+ await wallet.client.call(WalletApiOperation.AcceptTip, {
+ walletTipId: ptr.walletTipId,
+ });
+
+ await wallet.runUntilDone();
+
+ const bal = await wallet.client.call(WalletApiOperation.GetBalances, {});
+
+ console.log(bal);
+
+ t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:4.85");
+
+ const txns = await wallet.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+
+ console.log("Transactions:", JSON.stringify(txns, undefined, 2));
+
+ t.assertDeepEqual(txns.transactions[0].type, "tip");
+ t.assertDeepEqual(txns.transactions[0].pending, false);
+ t.assertAmountEquals(
+ txns.transactions[0].amountEffective,
+ "TESTKUDOS:4.85",
+ );
+ t.assertAmountEquals(txns.transactions[0].amountRaw, "TESTKUDOS:5.0");
+ };
+
+ // Check twice so make sure tip handling is idempotent
+ await doTip();
+ await doTip();
+}
+
+runTippingTest.suites = ["wallet", "wallet-tipping"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts b/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts
new file mode 100644
index 000000000..fc2f3335d
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts
@@ -0,0 +1,168 @@
+/*
+ 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 { j2s } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, WalletCli } from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+import { SyncService } from "../harness/sync.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runWalletBackupBasicTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { commonDb, merchant, wallet, bank, exchange } =
+ await createSimpleTestkudosEnvironment(t);
+
+ const sync = await SyncService.create(t, {
+ currency: "TESTKUDOS",
+ annualFee: "TESTKUDOS:0.5",
+ database: commonDb.connStr,
+ fulfillmentUrl: "taler://fulfillment-success",
+ httpPort: 8089,
+ name: "sync1",
+ paymentBackendUrl: merchant.makeInstanceBaseUrl(),
+ uploadLimitMb: 10,
+ });
+
+ await sync.start();
+ await sync.pingUntilAvailable();
+
+ await wallet.client.call(WalletApiOperation.AddBackupProvider, {
+ backupProviderBaseUrl: sync.baseUrl,
+ activate: false,
+ name: sync.baseUrl,
+ });
+
+ {
+ const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ t.assertDeepEqual(bi.providers[0].active, false);
+ }
+
+ await wallet.client.call(WalletApiOperation.AddBackupProvider, {
+ backupProviderBaseUrl: sync.baseUrl,
+ activate: true,
+ name: sync.baseUrl,
+ });
+
+ {
+ const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ t.assertDeepEqual(bi.providers[0].active, true);
+ }
+
+ await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+
+ {
+ const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ console.log(bi);
+ t.assertDeepEqual(
+ bi.providers[0].paymentStatus.type,
+ "insufficient-balance",
+ );
+ }
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:10" });
+
+ await wallet.runUntilDone();
+
+ await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+
+ {
+ const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ console.log(bi);
+ }
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:5" });
+
+ await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+
+ {
+ const bi = await wallet.client.call(WalletApiOperation.GetBackupInfo, {});
+ console.log(bi);
+ }
+
+ const backupRecovery = await wallet.client.call(
+ WalletApiOperation.ExportBackupRecovery,
+ {},
+ );
+
+ const txs = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ console.log(`backed up transactions ${j2s(txs)}`);
+
+ const wallet2 = new WalletCli(t, "wallet2");
+
+ // Check that the second wallet is a fresh wallet.
+ {
+ const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+ t.assertTrue(bal.balances.length === 0);
+ }
+
+ await wallet2.client.call(WalletApiOperation.ImportBackupRecovery, {
+ recovery: backupRecovery,
+ });
+
+ await wallet2.client.call(WalletApiOperation.RunBackupCycle, {});
+
+ // Check that now the old balance is available!
+ {
+ const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+ t.assertTrue(bal.balances.length === 1);
+ console.log(bal);
+ }
+
+ // Now do some basic checks that the restored wallet is still functional
+ {
+ const txs = await wallet2.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log(`restored transactions ${j2s(txs)}`);
+ const bal1 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+
+ t.assertAmountEquals(bal1.balances[0].available, "TESTKUDOS:14.1");
+
+ await withdrawViaBank(t, {
+ wallet: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:10",
+ });
+
+ await exchange.runWirewatchOnce();
+
+ await wallet2.runUntilDone();
+
+ const txs2 = await wallet2.client.call(
+ WalletApiOperation.GetTransactions,
+ {},
+ );
+ console.log(`tx after withdraw after restore ${j2s(txs2)}`);
+
+ const bal2 = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+
+ t.assertAmountEquals(bal2.balances[0].available, "TESTKUDOS:23.82");
+ }
+}
+
+runWalletBackupBasicTest.suites = ["wallet", "wallet-backup"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts b/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts
new file mode 100644
index 000000000..8b52260e9
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts
@@ -0,0 +1,174 @@
+/*
+ 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 { PreparePayResultType } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ GlobalTestState,
+ WalletCli,
+ MerchantPrivateApi,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironment,
+ makeTestPayment,
+ withdrawViaBank,
+} from "../harness/helpers.js";
+import { SyncService } from "../harness/sync.js";
+
+export async function runWalletBackupDoublespendTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { commonDb, merchant, wallet, bank, exchange } =
+ await createSimpleTestkudosEnvironment(t);
+
+ const sync = await SyncService.create(t, {
+ currency: "TESTKUDOS",
+ annualFee: "TESTKUDOS:0.5",
+ database: commonDb.connStr,
+ fulfillmentUrl: "taler://fulfillment-success",
+ httpPort: 8089,
+ name: "sync1",
+ paymentBackendUrl: merchant.makeInstanceBaseUrl(),
+ uploadLimitMb: 10,
+ });
+
+ await sync.start();
+ await sync.pingUntilAvailable();
+
+ await wallet.client.call(WalletApiOperation.AddBackupProvider, {
+ backupProviderBaseUrl: sync.baseUrl,
+ activate: true,
+ name: sync.baseUrl,
+ });
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:10" });
+
+ await wallet.runUntilDone();
+
+ await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+ await wallet.runUntilDone();
+ await wallet.client.call(WalletApiOperation.RunBackupCycle, {});
+
+ const backupRecovery = await wallet.client.call(
+ WalletApiOperation.ExportBackupRecovery,
+ {},
+ );
+
+ const wallet2 = new WalletCli(t, "wallet2");
+
+ await wallet2.client.call(WalletApiOperation.ImportBackupRecovery, {
+ recovery: backupRecovery,
+ });
+
+ await wallet2.client.call(WalletApiOperation.RunBackupCycle, {});
+
+ console.log(
+ "wallet1 balance before spend:",
+ await wallet.client.call(WalletApiOperation.GetBalances, {}),
+ );
+
+ await makeTestPayment(t, {
+ merchant,
+ wallet,
+ order: {
+ summary: "foo",
+ amount: "TESTKUDOS:7",
+ },
+ });
+
+ await wallet.runUntilDone();
+
+ console.log(
+ "wallet1 balance after spend:",
+ await wallet.client.call(WalletApiOperation.GetBalances, {}),
+ );
+
+ {
+ console.log(
+ "wallet2 balance:",
+ await wallet2.client.call(WalletApiOperation.GetBalances, {}),
+ );
+ }
+
+ // Now we double-spend with the second wallet
+
+ {
+ const instance = "default";
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, instance, {
+ order: {
+ amount: "TESTKUDOS:8",
+ summary: "bla",
+ fulfillment_url: "taler://fulfillment-success",
+ },
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(
+ merchant,
+ {
+ orderId: orderResp.order_id,
+ },
+ );
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ {
+ console.log(
+ "wallet2 balance before preparePay:",
+ await wallet2.client.call(WalletApiOperation.GetBalances, {}),
+ );
+ }
+
+ const preparePayResult = await wallet2.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertDeepEqual(
+ preparePayResult.status,
+ PreparePayResultType.PaymentPossible,
+ );
+
+ const res = await wallet2.client.call(WalletApiOperation.ConfirmPay, {
+ proposalId: preparePayResult.proposalId,
+ });
+
+ console.log(res);
+
+ // FIXME: wait for a notification that indicates insufficient funds!
+
+ await withdrawViaBank(t, {
+ wallet: wallet2,
+ bank,
+ exchange,
+ amount: "TESTKUDOS:50",
+ });
+
+ const bal = await wallet2.client.call(WalletApiOperation.GetBalances, {});
+ console.log("bal", bal);
+
+ await wallet2.runUntilDone();
+ }
+}
+
+runWalletBackupDoublespendTest.suites = ["wallet", "wallet-backup"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
new file mode 100644
index 000000000..f5226c6c0
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts
@@ -0,0 +1,144 @@
+/*
+ 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 { Duration, PreparePayResultType } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ ExchangeService,
+ FakebankService,
+ getRandomIban,
+ GlobalTestState,
+ MerchantPrivateApi,
+ MerchantService,
+ setupDb,
+ WalletCli,
+} from "../harness/harness.js";
+import { withdrawViaBank } from "../harness/helpers.js";
+
+/**
+ * Test for wallet balance error messages / different types of insufficient balance.
+ *
+ * Related bugs:
+ * https://bugs.taler.net/n/7299
+ */
+export async function runWalletBalanceTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await FakebankService.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);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ // Fakebank uses x-taler-bank, but merchant is configured to only accept sepa!
+ const label = "mymerchant";
+ await merchant.addInstance({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [
+ `payto://iban/SANDBOXX/${getRandomIban(label)}?receiver-name=${label}`,
+ ],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ // Withdraw digital cash into the wallet.
+
+ await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+
+ const order = {
+ summary: "Buy me!",
+ amount: "TESTKUDOS:5",
+ fulfillment_url: "taler://fulfillment-success/thx",
+ };
+
+ const orderResp = await MerchantPrivateApi.createOrder(merchant, "default", {
+ order,
+ });
+
+ let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus(merchant, {
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await wallet.client.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertDeepEqual(
+ preparePayResult.status,
+ PreparePayResultType.InsufficientBalance,
+ );
+
+ await wallet.runUntilDone();
+}
+
+runWalletBalanceTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-cryptoworker.ts b/packages/taler-harness/src/integrationtests/test-wallet-cryptoworker.ts
new file mode 100644
index 000000000..a9f1c4d80
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-cryptoworker.ts
@@ -0,0 +1,55 @@
+/*
+ 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 { j2s } from "@gnu-taler/taler-util";
+import {
+ checkReserve,
+ CryptoDispatcher,
+ depositCoin,
+ downloadExchangeInfo,
+ findDenomOrThrow,
+ NodeHttpLib,
+ refreshCoin,
+ SynchronousCryptoWorkerFactoryNode,
+ TalerError,
+ topupReserveWithDemobank,
+ WalletApiOperation,
+ withdrawCoin,
+} from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState, WalletCli } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+
+/**
+ * Run test for the different crypto workers.
+ */
+export async function runWalletCryptoWorkerTest(t: GlobalTestState) {
+ const wallet1 = new WalletCli(t, "w1", {
+ cryptoWorkerType: "sync",
+ });
+
+ await wallet1.client.call(WalletApiOperation.TestCrypto, {});
+
+ const wallet2 = new WalletCli(t, "w2", {
+ cryptoWorkerType: "node-worker-thread",
+ });
+
+ await wallet2.client.call(WalletApiOperation.TestCrypto, {});
+}
+
+runWalletCryptoWorkerTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
new file mode 100644
index 000000000..269a8b240
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts
@@ -0,0 +1,112 @@
+/*
+ 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 { j2s } from "@gnu-taler/taler-util";
+import {
+ checkReserve,
+ CryptoDispatcher,
+ depositCoin,
+ downloadExchangeInfo,
+ findDenomOrThrow,
+ NodeHttpLib,
+ refreshCoin,
+ SynchronousCryptoWorkerFactoryNode,
+ TalerError,
+ topupReserveWithDemobank,
+ withdrawCoin,
+} from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal and payment.
+ */
+export async function runWalletDblessTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { bank, exchange } = await createSimpleTestkudosEnvironment(t);
+
+ const http = new NodeHttpLib();
+ const cryptiDisp = new CryptoDispatcher(new SynchronousCryptoWorkerFactoryNode());
+ const cryptoApi = cryptiDisp.cryptoApi;
+
+ try {
+ // Withdraw digital cash into the wallet.
+
+ const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http);
+
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+
+ await topupReserveWithDemobank(
+ http,
+ reserveKeyPair.pub,
+ bank.baseUrl,
+ bank.bankAccessApiBaseUrl,
+ exchangeInfo,
+ "TESTKUDOS:10",
+ );
+
+ await exchange.runWirewatchOnce();
+
+ await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub);
+
+ const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8");
+
+ const coin = await withdrawCoin({
+ http,
+ cryptoApi,
+ reserveKeyPair: {
+ reservePriv: reserveKeyPair.priv,
+ reservePub: reserveKeyPair.pub,
+ },
+ denom: d1,
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ await depositCoin({
+ amount: "TESTKUDOS:4",
+ coin: coin,
+ cryptoApi,
+ exchangeBaseUrl: exchange.baseUrl,
+ http,
+ });
+
+ const refreshDenoms = [
+ findDenomOrThrow(exchangeInfo, "TESTKUDOS:1"),
+ findDenomOrThrow(exchangeInfo, "TESTKUDOS:1"),
+ ];
+
+ await refreshCoin({
+ oldCoin: coin,
+ cryptoApi,
+ http,
+ newDenoms: refreshDenoms,
+ });
+ } catch (e) {
+ if (e instanceof TalerError) {
+ console.log(e);
+ console.log(j2s(e.errorDetail));
+ } else {
+ console.log(e);
+ }
+ throw e;
+ }
+}
+
+runWalletDblessTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-wallettesting.ts b/packages/taler-harness/src/integrationtests/test-wallettesting.ts
new file mode 100644
index 000000000..03c446db3
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-wallettesting.ts
@@ -0,0 +1,233 @@
+/*
+ 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/>
+ */
+
+/**
+ * Integration test for the wallet testing functionality used by the exchange
+ * test cases.
+ */
+
+/**
+ * Imports.
+ */
+import { Amounts, CoinStatus } from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ ExchangeService,
+ GlobalTestState,
+ MerchantService,
+ setupDb,
+ WalletCli,
+ getPayto,
+} from "../harness/harness.js";
+import { SimpleTestEnvironment } from "../harness/helpers.js";
+
+const merchantAuthToken = "secret-token:sandbox";
+
+/**
+ * Run a test case with a simple TESTKUDOS Taler environment, consisting
+ * of one exchange, one bank and one merchant.
+ */
+export async function createMyEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+): Promise<SimpleTestEnvironment> {
+ 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);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ 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: [getPayto("merchant-default")],
+ });
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ wallet,
+ bank,
+ exchangeBankAccount,
+ };
+}
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runWallettestingTest(t: GlobalTestState) {
+ const { wallet, bank, exchange, merchant } = await createMyEnvironment(t);
+
+ await wallet.client.call(WalletApiOperation.RunIntegrationTest, {
+ amountToSpend: "TESTKUDOS:5",
+ amountToWithdraw: "TESTKUDOS:10",
+ bankBaseUrl: bank.baseUrl,
+ bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
+ merchantAuthToken: merchantAuthToken,
+ merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+ });
+
+ let txns = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ console.log(JSON.stringify(txns, undefined, 2));
+ let txTypes = txns.transactions.map((x) => x.type);
+
+ t.assertDeepEqual(txTypes, [
+ "withdrawal",
+ "payment",
+ "withdrawal",
+ "payment",
+ "refund",
+ "payment",
+ ]);
+
+ wallet.deleteDatabase();
+
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ amount: "TESTKUDOS:10",
+ bankBaseUrl: bank.baseUrl,
+ bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ await wallet.runUntilDone();
+
+ await wallet.client.call(WalletApiOperation.TestPay, {
+ amount: "TESTKUDOS:5",
+ merchantAuthToken: merchantAuthToken,
+ merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+ summary: "foo",
+ });
+
+ await wallet.runUntilDone();
+
+ txns = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ console.log(JSON.stringify(txns, undefined, 2));
+ txTypes = txns.transactions.map((x) => x.type);
+
+ t.assertDeepEqual(txTypes, ["withdrawal", "payment"]);
+
+ wallet.deleteDatabase();
+
+ await wallet.client.call(WalletApiOperation.WithdrawTestBalance, {
+ amount: "TESTKUDOS:10",
+ bankBaseUrl: bank.baseUrl,
+ bankAccessApiBaseUrl: bank.bankAccessApiBaseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ await wallet.runUntilDone();
+
+ const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {});
+
+ console.log("coin dump:", JSON.stringify(coinDump, undefined, 2));
+
+ let susp: string | undefined;
+ {
+ for (const c of coinDump.coins) {
+ if (
+ c.coin_status === CoinStatus.Fresh &&
+ 0 === Amounts.cmp(c.denom_value, "TESTKUDOS:8")
+ ) {
+ susp = c.coin_pub;
+ }
+ }
+ }
+
+ t.assertTrue(susp !== undefined);
+
+ console.log("suspending coin");
+
+ await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
+ coinPub: susp,
+ suspended: true,
+ });
+
+ // This should fail, as we've suspended a coin that we need
+ // to pay.
+ await t.assertThrowsAsync(async () => {
+ await wallet.client.call(WalletApiOperation.TestPay, {
+ amount: "TESTKUDOS:5",
+ merchantAuthToken: merchantAuthToken,
+ merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+ summary: "foo",
+ });
+ });
+
+ console.log("unsuspending coin");
+
+ await wallet.client.call(WalletApiOperation.SetCoinSuspended, {
+ coinPub: susp,
+ suspended: false,
+ });
+
+ await wallet.client.call(WalletApiOperation.TestPay, {
+ amount: "TESTKUDOS:5",
+ merchantAuthToken: merchantAuthToken,
+ merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+ summary: "foo",
+ });
+
+ await t.shutdown();
+}
+
+runWallettestingTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts
new file mode 100644
index 000000000..bf2dc0133
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts
@@ -0,0 +1,84 @@
+/*
+ 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 { TalerErrorCode } from "@gnu-taler/taler-util";
+import {
+ WalletApiOperation,
+ BankApi,
+ BankAccessApi,
+} from "@gnu-taler/taler-wallet-core";
+import { GlobalTestState } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runWithdrawalAbortBankTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t);
+
+ // Create a withdrawal operation
+
+ const user = await BankApi.createRandomBankUser(bank);
+ const wop = await BankAccessApi.createWithdrawalOperation(
+ bank,
+ user,
+ "TESTKUDOS:10",
+ );
+
+ // Hand it to the wallet
+
+ await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ });
+
+ await wallet.runPending();
+
+ // Abort it
+
+ await BankApi.abortWithdrawalOperation(bank, user, wop);
+ //await BankApi.confirmWithdrawalOperation(bank, user, wop);
+
+ // Withdraw
+
+ // Difference:
+ // -> with euFin, the wallet selects
+ // -> with PyBank, the wallet stops _before_
+ //
+ // WHY ?!
+ //
+ const e = await t.assertThrowsTalerErrorAsync(async () => {
+ await wallet.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+ });
+ t.assertDeepEqual(
+ e.errorDetail.code,
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ );
+
+ await t.shutdown();
+}
+
+runWithdrawalAbortBankTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
new file mode 100644
index 000000000..dc7298e5d
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
@@ -0,0 +1,91 @@
+/*
+ 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 } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+import {
+ WalletApiOperation,
+ BankApi,
+ BankAccessApi,
+} from "@gnu-taler/taler-wallet-core";
+import { j2s } from "@gnu-taler/taler-util";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange } = await createSimpleTestkudosEnvironment(t);
+
+ // Create a withdrawal operation
+
+ const user = await BankApi.createRandomBankUser(bank);
+ const wop = await BankAccessApi.createWithdrawalOperation(
+ bank,
+ user,
+ "TESTKUDOS:10",
+ );
+
+ // Hand it to the wallet
+
+ const r1 = await wallet.client.call(
+ WalletApiOperation.GetWithdrawalDetailsForUri,
+ {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+
+ await wallet.runPending();
+
+ // Withdraw
+
+ const r2 = await wallet.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+ // Do it twice to check idempotency
+ const r3 = await wallet.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ },
+ );
+ await wallet.runPending();
+
+ // Confirm it
+
+ await BankApi.confirmWithdrawalOperation(bank, user, wop);
+
+ await wallet.runUntilDone();
+
+ // Check balance
+
+ const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
+
+ const txn = await wallet.client.call(WalletApiOperation.GetTransactions, {});
+ console.log(`transactions: ${j2s(txn)}`);
+}
+
+runWithdrawalBankIntegratedTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts
new file mode 100644
index 000000000..ec6e54e6c
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts
@@ -0,0 +1,97 @@
+/*
+ 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,
+ WalletCli,
+ setupDb,
+ ExchangeService,
+ FakebankService,
+} from "../harness/harness.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import { URL } from "@gnu-taler/taler-util";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runWithdrawalFakebankTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await FakebankService.create(t, {
+ currency: "TESTKUDOS",
+ httpPort: 8082,
+ allowRegistrations: true,
+ // Not used by fakebank
+ database: db.connStr,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ exchange.addBankAccount("1", {
+ accountName: "exchange",
+ accountPassword: "x",
+ wireGatewayApiBaseUrl: new URL("/exchange/", bank.baseUrl).href,
+ accountPaytoUri: "payto://x-taler-bank/localhost/exchange",
+ });
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
+ exchange: exchange.baseUrl,
+ amount: "TESTKUDOS:10",
+ bank: bank.baseUrl,
+ });
+
+ await exchange.runWirewatchOnce();
+
+ await wallet.runUntilDone();
+
+ // Check balance
+
+ const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
+
+ await t.shutdown();
+}
+
+runWithdrawalFakebankTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-high.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-high.ts
new file mode 100644
index 000000000..deb0e6dde
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-high.ts
@@ -0,0 +1,99 @@
+/*
+ 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,
+ WalletCli,
+ setupDb,
+ ExchangeService,
+ FakebankService,
+} from "../harness/harness.js";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import { URL } from "@gnu-taler/taler-util";
+
+/**
+ * Withdraw a high amount. Mostly intended
+ * as a perf test.
+ */
+export async function runWithdrawalHighTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const bank = await FakebankService.create(t, {
+ currency: "TESTKUDOS",
+ httpPort: 8082,
+ allowRegistrations: true,
+ // Not used by fakebank
+ database: db.connStr,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ exchange.addBankAccount("1", {
+ accountName: "exchange",
+ accountPassword: "x",
+ wireGatewayApiBaseUrl: new URL("/exchange/", bank.baseUrl).href,
+ accountPaytoUri: "payto://x-taler-bank/localhost/exchange",
+ });
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ console.log("setup done!");
+
+ const wallet = new WalletCli(t);
+
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ await wallet.client.call(WalletApiOperation.WithdrawFakebank, {
+ exchange: exchange.baseUrl,
+ amount: "TESTKUDOS:5000",
+ bank: bank.baseUrl,
+ });
+
+ await exchange.runWirewatchOnce();
+
+ await wallet.runUntilDone();
+
+ // Check balance
+
+ const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ console.log(balResp);
+
+ await t.shutdown();
+}
+
+runWithdrawalHighTest.suites = ["wallet-perf"];
+runWithdrawalHighTest.excludeByDefault = true;
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts
new file mode 100644
index 000000000..b691ae508
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts
@@ -0,0 +1,84 @@
+/*
+ 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 } from "../harness/harness.js";
+import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
+import { WalletApiOperation, BankApi } from "@gnu-taler/taler-wallet-core";
+import {
+ AbsoluteTime,
+ Duration,
+ TalerProtocolTimestamp,
+} from "@gnu-taler/taler-util";
+
+/**
+ * Run test for basic, bank-integrated withdrawal.
+ */
+export async function runTestWithdrawalManualTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { wallet, bank, exchange, exchangeBankAccount } =
+ await createSimpleTestkudosEnvironment(t);
+
+ // Create a withdrawal operation
+
+ const user = await BankApi.createRandomBankUser(bank);
+
+ await wallet.client.call(WalletApiOperation.AddExchange, {
+ exchangeBaseUrl: exchange.baseUrl,
+ });
+
+ const tStart = AbsoluteTime.now();
+
+ // We expect this to return immediately.
+ const wres = await wallet.client.call(
+ WalletApiOperation.AcceptManualWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ amount: "TESTKUDOS:10",
+ },
+ );
+
+ // Check that the request did not go into long-polling.
+ const duration = AbsoluteTime.difference(tStart, AbsoluteTime.now());
+ if (duration.d_ms > 5 * 1000) {
+ throw Error("withdrawal took too long (longpolling issue)");
+ }
+
+ const reservePub: string = wres.reservePub;
+
+ await BankApi.adminAddIncoming(bank, {
+ exchangeBankAccount,
+ amount: "TESTKUDOS:10",
+ debitAccountPayto: user.accountPaytoUri,
+ reservePub: reservePub,
+ });
+
+ await exchange.runWirewatchOnce();
+
+ await wallet.runUntilDone();
+
+ // Check balance
+
+ const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
+ t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
+
+ await t.shutdown();
+}
+
+runTestWithdrawalManualTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
new file mode 100644
index 000000000..4b1c28bde
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -0,0 +1,496 @@
+/*
+ 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/>
+ */
+
+import { CancellationToken, minimatch } from "@gnu-taler/taler-util";
+import * as child_process from "child_process";
+import * as fs from "fs";
+import * as os from "os";
+import * as path from "path";
+import url from "url";
+import {
+ GlobalTestState,
+ runTestWithState,
+ shouldLingerInTest,
+ TestRunResult,
+} from "../harness/harness.js";
+import { runAgeRestrictionsMerchantTest } from "./test-age-restrictions-merchant.js";
+import { runBankApiTest } from "./test-bank-api.js";
+import { runClaimLoopTest } from "./test-claim-loop.js";
+import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
+import { runDenomUnofferedTest } from "./test-denom-unoffered.js";
+import { runDepositTest } from "./test-deposit.js";
+import { runExchangeManagementTest } from "./test-exchange-management.js";
+import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
+import { runFeeRegressionTest } from "./test-fee-regression.js";
+import { runForcedSelectionTest } from "./test-forced-selection.js";
+import { runLibeufinApiBankaccountTest } from "./test-libeufin-api-bankaccount.js";
+import { runLibeufinApiBankconnectionTest } from "./test-libeufin-api-bankconnection.js";
+import { runLibeufinApiFacadeTest } from "./test-libeufin-api-facade.js";
+import { runLibeufinApiFacadeBadRequestTest } from "./test-libeufin-api-facade-bad-request.js";
+import { runLibeufinApiPermissionsTest } from "./test-libeufin-api-permissions.js";
+import { runLibeufinApiSandboxCamtTest } from "./test-libeufin-api-sandbox-camt.js";
+import { runLibeufinApiSandboxTransactionsTest } from "./test-libeufin-api-sandbox-transactions.js";
+import { runLibeufinApiSchedulingTest } from "./test-libeufin-api-scheduling.js";
+import { runLibeufinApiUsersTest } from "./test-libeufin-api-users.js";
+import { runLibeufinBadGatewayTest } from "./test-libeufin-bad-gateway.js";
+import { runLibeufinBasicTest } from "./test-libeufin-basic.js";
+import { runLibeufinC5xTest } from "./test-libeufin-c5x.js";
+import { runLibeufinAnastasisFacadeTest } from "./test-libeufin-facade-anastasis.js";
+import { runLibeufinKeyrotationTest } from "./test-libeufin-keyrotation.js";
+import { runLibeufinNexusBalanceTest } from "./test-libeufin-nexus-balance.js";
+import { runLibeufinRefundTest } from "./test-libeufin-refund.js";
+import { runLibeufinRefundMultipleUsersTest } from "./test-libeufin-refund-multiple-users.js";
+import { runLibeufinSandboxWireTransferCliTest } from "./test-libeufin-sandbox-wire-transfer-cli.js";
+import { runLibeufinTutorialTest } from "./test-libeufin-tutorial.js";
+import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion.js";
+import { runMerchantInstancesTest } from "./test-merchant-instances.js";
+import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete.js";
+import { runMerchantInstancesUrlsTest } from "./test-merchant-instances-urls.js";
+import { runMerchantLongpollingTest } from "./test-merchant-longpolling.js";
+import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js";
+import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js";
+import { runPayPaidTest } from "./test-pay-paid.js";
+import { runPaymentTest } from "./test-payment.js";
+import { runPaymentClaimTest } from "./test-payment-claim.js";
+import { runPaymentFaultTest } from "./test-payment-fault.js";
+import { runPaymentForgettableTest } from "./test-payment-forgettable.js";
+import { runPaymentIdempotencyTest } from "./test-payment-idempotency.js";
+import { runPaymentMultipleTest } from "./test-payment-multiple.js";
+import { runPaymentDemoTest } from "./test-payment-on-demo.js";
+import { runPaymentTransientTest } from "./test-payment-transient.js";
+import { runPaymentZeroTest } from "./test-payment-zero.js";
+import { runPaywallFlowTest } from "./test-paywall-flow.js";
+import { runPeerToPeerPullTest } from "./test-peer-to-peer-pull.js";
+import { runPeerToPeerPushTest } from "./test-peer-to-peer-push.js";
+import { runRefundTest } from "./test-refund.js";
+import { runRefundAutoTest } from "./test-refund-auto.js";
+import { runRefundGoneTest } from "./test-refund-gone.js";
+import { runRefundIncrementalTest } from "./test-refund-incremental.js";
+import { runRevocationTest } from "./test-revocation.js";
+import { runTimetravelAutorefreshTest } from "./test-timetravel-autorefresh.js";
+import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw.js";
+import { runTippingTest } from "./test-tipping.js";
+import { runWalletBackupBasicTest } from "./test-wallet-backup-basic.js";
+import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend.js";
+import { runWalletDblessTest } from "./test-wallet-dbless.js";
+import { runWallettestingTest } from "./test-wallettesting.js";
+import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js";
+import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js";
+import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
+import { runTestWithdrawalManualTest } from "./test-withdrawal-manual.js";
+import { runAgeRestrictionsPeerTest } from "./test-age-restrictions-peer.js";
+import { runWalletBalanceTest } from "./test-wallet-balance.js";
+import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js";
+import { runWalletCryptoWorkerTest } from "./test-wallet-cryptoworker.js";
+import { runWithdrawalHighTest } from "./test-withdrawal-high.js";
+
+/**
+ * Test runner.
+ */
+
+/**
+ * Spec for one test.
+ */
+interface TestMainFunction {
+ (t: GlobalTestState): Promise<void>;
+ timeoutMs?: number;
+ excludeByDefault?: boolean;
+ suites?: string[];
+}
+
+const allTests: TestMainFunction[] = [
+ runAgeRestrictionsMerchantTest,
+ runAgeRestrictionsPeerTest,
+ runAgeRestrictionsMixedMerchantTest,
+ runBankApiTest,
+ runClaimLoopTest,
+ runClauseSchnorrTest,
+ runWalletCryptoWorkerTest,
+ runDepositTest,
+ runDenomUnofferedTest,
+ runExchangeManagementTest,
+ runExchangeTimetravelTest,
+ runFeeRegressionTest,
+ runForcedSelectionTest,
+ runLibeufinBasicTest,
+ runLibeufinKeyrotationTest,
+ runLibeufinTutorialTest,
+ runLibeufinRefundTest,
+ runLibeufinC5xTest,
+ runLibeufinNexusBalanceTest,
+ runLibeufinBadGatewayTest,
+ runLibeufinRefundMultipleUsersTest,
+ runLibeufinApiPermissionsTest,
+ runLibeufinApiFacadeTest,
+ runLibeufinApiFacadeBadRequestTest,
+ runLibeufinAnastasisFacadeTest,
+ runLibeufinApiSchedulingTest,
+ runLibeufinApiUsersTest,
+ runLibeufinApiBankaccountTest,
+ runLibeufinApiBankconnectionTest,
+ runLibeufinApiSandboxTransactionsTest,
+ runLibeufinApiSandboxCamtTest,
+ runLibeufinSandboxWireTransferCliTest,
+ runMerchantExchangeConfusionTest,
+ runMerchantInstancesTest,
+ runMerchantInstancesDeleteTest,
+ runMerchantInstancesUrlsTest,
+ runMerchantLongpollingTest,
+ runMerchantSpecPublicOrdersTest,
+ runMerchantRefundApiTest,
+ runPaymentClaimTest,
+ runPaymentFaultTest,
+ runPaymentForgettableTest,
+ runPaymentIdempotencyTest,
+ runPaymentMultipleTest,
+ runPaymentTest,
+ runPaymentDemoTest,
+ runPaymentTransientTest,
+ runPaymentZeroTest,
+ runPayPaidTest,
+ runPaywallFlowTest,
+ runPeerToPeerPushTest,
+ runPeerToPeerPullTest,
+ runRefundAutoTest,
+ runRefundGoneTest,
+ runRefundIncrementalTest,
+ runRefundTest,
+ runRevocationTest,
+ runTestWithdrawalManualTest,
+ runWithdrawalFakebankTest,
+ runTimetravelAutorefreshTest,
+ runTimetravelWithdrawTest,
+ runTippingTest,
+ runWalletBackupBasicTest,
+ runWalletBackupDoublespendTest,
+ runWalletBalanceTest,
+ runWithdrawalHighTest,
+ runWallettestingTest,
+ runWalletDblessTest,
+ runWithdrawalAbortBankTest,
+ runWithdrawalBankIntegratedTest,
+];
+
+export interface TestRunSpec {
+ includePattern?: string;
+ suiteSpec?: string;
+ dryRun?: boolean;
+ verbosity: number;
+}
+
+export interface TestInfo {
+ name: string;
+ suites: string[];
+ excludeByDefault: boolean;
+}
+
+function updateCurrentSymlink(testDir: string): void {
+ const currLink = path.join(
+ os.tmpdir(),
+ `taler-integrationtests-${os.userInfo().username}-current`,
+ );
+ try {
+ fs.unlinkSync(currLink);
+ } catch (e) {
+ // Ignore
+ }
+ try {
+ fs.symlinkSync(testDir, currLink);
+ } catch (e) {
+ console.log(e);
+ // Ignore
+ }
+}
+
+export function getTestName(tf: TestMainFunction): string {
+ const res = tf.name.match(/run([a-zA-Z0-9]*)Test/);
+ if (!res) {
+ throw Error("invalid test name, must be 'run${NAME}Test'");
+ }
+ return res[1]
+ .replace(/[a-z0-9][A-Z]/g, (x) => {
+ return x[0] + "-" + x[1];
+ })
+ .toLowerCase();
+}
+
+interface RunTestChildInstruction {
+ testName: string;
+ testRootDir: string;
+}
+
+export async function runTests(spec: TestRunSpec) {
+ const testRootDir = fs.mkdtempSync(
+ path.join(os.tmpdir(), "taler-integrationtests-"),
+ );
+ updateCurrentSymlink(testRootDir);
+ console.log(`testsuite root directory: ${testRootDir}`);
+
+ const testResults: TestRunResult[] = [];
+
+ let currentChild: child_process.ChildProcess | undefined;
+
+ const handleSignal = (s: NodeJS.Signals) => {
+ console.log(`received signal ${s} in test parent`);
+ if (currentChild) {
+ currentChild.kill("SIGTERM");
+ }
+ reportAndQuit(testRootDir, testResults, true);
+ };
+
+ process.on("SIGINT", (s) => handleSignal(s));
+ process.on("SIGTERM", (s) => handleSignal(s));
+ //process.on("unhandledRejection", handleSignal);
+ //process.on("uncaughtException", handleSignal);
+
+ let suites: Set<string> | undefined;
+
+ if (spec.suiteSpec) {
+ suites = new Set(spec.suiteSpec.split(",").map((x) => x.trim()));
+ }
+
+ for (const [n, testCase] of allTests.entries()) {
+ const testName = getTestName(testCase);
+ if (spec.includePattern && !minimatch(testName, spec.includePattern)) {
+ continue;
+ }
+
+ if (suites) {
+ const ts = new Set(testCase.suites ?? []);
+ const intersection = new Set([...suites].filter((x) => ts.has(x)));
+ if (intersection.size === 0) {
+ continue;
+ }
+ } else {
+ if (testCase.excludeByDefault) {
+ continue;
+ }
+ }
+
+ if (spec.dryRun) {
+ console.log(`dry run: would run test ${testName}`);
+ continue;
+ }
+
+ const testInstr: RunTestChildInstruction = {
+ testName,
+ testRootDir,
+ };
+
+ const myFilename = url.fileURLToPath(import.meta.url);
+
+ currentChild = child_process.fork(myFilename, ["__TWCLI_TESTWORKER"], {
+ env: {
+ TWCLI_RUN_TEST_INSTRUCTION: JSON.stringify(testInstr),
+ ...process.env,
+ },
+ stdio: ["pipe", "pipe", "pipe", "ipc"],
+ });
+
+ const testDir = path.join(testRootDir, testName);
+ fs.mkdirSync(testDir, { recursive: true });
+
+ const harnessLogFilename = path.join(testRootDir, testName, "harness.log");
+ const harnessLogStream = fs.createWriteStream(harnessLogFilename);
+
+ if (spec.verbosity > 0) {
+ currentChild.stderr?.pipe(process.stderr);
+ currentChild.stdout?.pipe(process.stdout);
+ }
+
+ currentChild.stdout?.pipe(harnessLogStream);
+ currentChild.stderr?.pipe(harnessLogStream);
+
+ const defaultTimeout = 60000;
+ const testTimeoutMs = testCase.timeoutMs ?? defaultTimeout;
+
+ console.log(`running ${testName} with timeout ${testTimeoutMs}ms`);
+
+ const { token } = CancellationToken.timeout(testTimeoutMs);
+
+ const resultPromise: Promise<TestRunResult> = new Promise(
+ (resolve, reject) => {
+ let msg: TestRunResult | undefined;
+ currentChild!.on("message", (m) => {
+ if (token.isCancelled) {
+ return;
+ }
+ msg = m as TestRunResult;
+ });
+ currentChild!.on("exit", (code, signal) => {
+ if (token.isCancelled) {
+ return;
+ }
+ console.log(`process exited code=${code} signal=${signal}`);
+ if (signal) {
+ reject(new Error(`test worker exited with signal ${signal}`));
+ } else if (code != 0) {
+ reject(new Error(`test worker exited with code ${code}`));
+ } else if (!msg) {
+ reject(
+ new Error(
+ `test worker exited without giving back the test results`,
+ ),
+ );
+ } else {
+ resolve(msg);
+ }
+ });
+ currentChild!.on("error", (err) => {
+ if (token.isCancelled) {
+ return;
+ }
+ reject(err);
+ });
+ },
+ );
+
+ let result: TestRunResult;
+
+ try {
+ result = await token.racePromise(resultPromise);
+ } catch (e: any) {
+ console.error(`test ${testName} timed out`);
+ if (token.isCancelled) {
+ result = {
+ status: "fail",
+ reason: "timeout",
+ timeSec: testTimeoutMs / 1000,
+ name: testName,
+ };
+ currentChild.kill("SIGTERM");
+ } else {
+ throw Error(e);
+ }
+ }
+
+ harnessLogStream.close();
+
+ console.log(`parent: got result ${JSON.stringify(result)}`);
+
+ testResults.push(result);
+ }
+
+ reportAndQuit(testRootDir, testResults);
+}
+
+export function reportAndQuit(
+ testRootDir: string,
+ testResults: TestRunResult[],
+ interrupted: boolean = false,
+): never {
+ let numTotal = 0;
+ let numFail = 0;
+ let numSkip = 0;
+ let numPass = 0;
+
+ for (const result of testResults) {
+ numTotal++;
+ if (result.status === "fail") {
+ numFail++;
+ } else if (result.status === "skip") {
+ numSkip++;
+ } else if (result.status === "pass") {
+ numPass++;
+ }
+ }
+
+ const resultsFile = path.join(testRootDir, "results.json");
+ fs.writeFileSync(
+ path.join(testRootDir, "results.json"),
+ JSON.stringify({ testResults, interrupted }, undefined, 2),
+ );
+ if (interrupted) {
+ console.log("test suite was interrupted");
+ }
+ console.log(`See ${resultsFile} for details`);
+ console.log(`Skipped: ${numSkip}/${numTotal}`);
+ console.log(`Failed: ${numFail}/${numTotal}`);
+ console.log(`Passed: ${numPass}/${numTotal}`);
+
+ if (interrupted) {
+ process.exit(3);
+ } else if (numPass < numTotal - numSkip) {
+ process.exit(1);
+ } else {
+ process.exit(0);
+ }
+}
+
+export function getTestInfo(): TestInfo[] {
+ return allTests.map((x) => ({
+ name: getTestName(x),
+ suites: x.suites ?? [],
+ excludeByDefault: x.excludeByDefault ?? false,
+ }));
+}
+
+const runTestInstrStr = process.env["TWCLI_RUN_TEST_INSTRUCTION"];
+if (runTestInstrStr && process.argv.includes("__TWCLI_TESTWORKER")) {
+ // Test will call taler-wallet-cli, so we must not propagate this variable.
+ delete process.env["TWCLI_RUN_TEST_INSTRUCTION"];
+ const { testRootDir, testName } = JSON.parse(
+ runTestInstrStr,
+ ) as RunTestChildInstruction;
+ console.log(`running test ${testName} in worker process`);
+
+ process.on("disconnect", () => {
+ console.log("got disconnect from parent");
+ process.exit(3);
+ });
+
+ const runTest = async () => {
+ let testMain: TestMainFunction | undefined;
+ for (const t of allTests) {
+ if (getTestName(t) === testName) {
+ testMain = t;
+ break;
+ }
+ }
+
+ if (!process.send) {
+ console.error("can't communicate with parent");
+ process.exit(2);
+ }
+
+ if (!testMain) {
+ console.log(`test ${testName} not found`);
+ process.exit(2);
+ }
+
+ const testDir = path.join(testRootDir, testName);
+ console.log(`running test ${testName}`);
+ const gc = new GlobalTestState({
+ testDir,
+ });
+ const testResult = await runTestWithState(gc, testMain, testName);
+ process.send(testResult);
+ };
+
+ runTest()
+ .then(() => {
+ console.log(`test ${testName} finished in worker`);
+ if (shouldLingerInTest()) {
+ console.log("lingering ...");
+ return;
+ }
+ process.exit(0);
+ })
+ .catch((e) => {
+ console.log(e);
+ process.exit(1);
+ });
+}