diff options
author | Florian Dold <florian@dold.me> | 2022-12-23 12:59:29 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2022-12-23 13:19:41 +0100 |
commit | 083c4cf5d96314c44dd716cf3cc931e95b651bbd (patch) | |
tree | 7f15a46224d5dfe495e26dc6ec66996c889498ff /packages/taler-harness/src/integrationtests | |
parent | d98711cb51d13bb2da3682014c7c6e75d7fbb4f0 (diff) | |
download | wallet-core-083c4cf5d96314c44dd716cf3cc931e95b651bbd.tar.xz |
spill extra functionality from wallet-cli into taler-harness
We want to keep taler-wallet-cli smaller and have fewer dependencies.
Diffstat (limited to 'packages/taler-harness/src/integrationtests')
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); + }); +} |