From 083c4cf5d96314c44dd716cf3cc931e95b651bbd Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 23 Dec 2022 12:59:29 +0100 Subject: spill extra functionality from wallet-cli into taler-harness We want to keep taler-wallet-cli smaller and have fewer dependencies. --- packages/demobank-ui/package.json | 2 +- packages/idb-bridge/package.json | 2 +- packages/idb-bridge/src/util/structuredClone.ts | 13 +- packages/merchant-backoffice-ui/package.json | 2 +- packages/pogen/package.json | 2 +- packages/taler-harness/Makefile | 30 + packages/taler-harness/bin/taler-harness.mjs | 19 + packages/taler-harness/build.mjs | 70 + packages/taler-harness/package.json | 44 + packages/taler-harness/src/bench1.ts | 189 ++ packages/taler-harness/src/bench2.ts | 170 ++ packages/taler-harness/src/bench3.ts | 205 ++ .../taler-harness/src/benchMerchantIDGenerator.ts | 83 + packages/taler-harness/src/env-full.ts | 101 + packages/taler-harness/src/env1.ts | 70 + .../taler-harness/src/harness/denomStructures.ts | 157 ++ .../taler-harness/src/harness/faultInjection.ts | 256 +++ packages/taler-harness/src/harness/harness.ts | 2024 ++++++++++++++++++++ packages/taler-harness/src/harness/helpers.ts | 444 +++++ .../taler-harness/src/harness/libeufin-apis.ts | 872 +++++++++ packages/taler-harness/src/harness/libeufin.ts | 910 +++++++++ .../taler-harness/src/harness/merchantApiTypes.ts | 337 ++++ packages/taler-harness/src/harness/sync.ts | 119 ++ packages/taler-harness/src/index.ts | 338 ++++ .../integrationtests/scenario-prompt-payment.ts | 60 + .../test-age-restrictions-merchant.ts | 201 ++ .../test-age-restrictions-mixed-merchant.ts | 116 ++ .../integrationtests/test-age-restrictions-peer.ts | 92 + .../src/integrationtests/test-bank-api.ts | 136 ++ .../src/integrationtests/test-claim-loop.ts | 79 + .../src/integrationtests/test-clause-schnorr.ts | 97 + .../src/integrationtests/test-denom-unoffered.ts | 126 ++ .../src/integrationtests/test-deposit.ts | 71 + .../integrationtests/test-exchange-management.ts | 285 +++ .../integrationtests/test-exchange-timetravel.ts | 240 +++ .../src/integrationtests/test-fee-regression.ts | 200 ++ .../src/integrationtests/test-forced-selection.ts | 87 + .../test-libeufin-api-bankaccount.ts | 109 ++ .../test-libeufin-api-bankconnection.ts | 56 + .../test-libeufin-api-facade-bad-request.ts | 71 + .../integrationtests/test-libeufin-api-facade.ts | 70 + .../test-libeufin-api-permissions.ts | 64 + .../test-libeufin-api-sandbox-camt.ts | 76 + .../test-libeufin-api-sandbox-transactions.ts | 69 + .../test-libeufin-api-scheduling.ts | 106 + .../integrationtests/test-libeufin-api-users.ts | 63 + .../integrationtests/test-libeufin-bad-gateway.ts | 74 + .../src/integrationtests/test-libeufin-basic.ts | 308 +++ .../src/integrationtests/test-libeufin-c5x.ts | 147 ++ .../test-libeufin-facade-anastasis.ts | 169 ++ .../integrationtests/test-libeufin-keyrotation.ts | 79 + .../test-libeufin-nexus-balance.ts | 118 ++ .../test-libeufin-refund-multiple-users.ts | 104 + .../src/integrationtests/test-libeufin-refund.ts | 101 + .../test-libeufin-sandbox-wire-transfer-cli.ts | 85 + .../src/integrationtests/test-libeufin-tutorial.ts | 128 ++ .../test-merchant-exchange-confusion.ts | 243 +++ .../test-merchant-instances-delete.ts | 129 ++ .../test-merchant-instances-urls.ts | 189 ++ .../integrationtests/test-merchant-instances.ts | 184 ++ .../integrationtests/test-merchant-longpolling.ts | 162 ++ .../integrationtests/test-merchant-refund-api.ts | 303 +++ .../test-merchant-spec-public-orders.ts | 620 ++++++ .../src/integrationtests/test-pay-paid.ts | 222 +++ .../src/integrationtests/test-payment-claim.ts | 110 ++ .../src/integrationtests/test-payment-fault.ts | 222 +++ .../integrationtests/test-payment-forgettable.ts | 81 + .../integrationtests/test-payment-idempotency.ts | 121 ++ .../src/integrationtests/test-payment-multiple.ts | 163 ++ .../src/integrationtests/test-payment-on-demo.ts | 114 ++ .../src/integrationtests/test-payment-transient.ts | 185 ++ .../src/integrationtests/test-payment-zero.ts | 72 + .../src/integrationtests/test-payment.ts | 77 + .../src/integrationtests/test-paywall-flow.ts | 252 +++ .../src/integrationtests/test-peer-to-peer-pull.ts | 101 + .../src/integrationtests/test-peer-to-peer-push.ts | 119 ++ .../src/integrationtests/test-refund-auto.ts | 105 + .../src/integrationtests/test-refund-gone.ts | 124 ++ .../integrationtests/test-refund-incremental.ts | 202 ++ .../src/integrationtests/test-refund.ts | 106 + .../src/integrationtests/test-revocation.ts | 215 +++ .../test-timetravel-autorefresh.ts | 216 +++ .../integrationtests/test-timetravel-withdraw.ts | 98 + .../src/integrationtests/test-tipping.ts | 129 ++ .../integrationtests/test-wallet-backup-basic.ts | 168 ++ .../test-wallet-backup-doublespend.ts | 174 ++ .../src/integrationtests/test-wallet-balance.ts | 144 ++ .../integrationtests/test-wallet-cryptoworker.ts | 55 + .../src/integrationtests/test-wallet-dbless.ts | 112 ++ .../src/integrationtests/test-wallettesting.ts | 233 +++ .../integrationtests/test-withdrawal-abort-bank.ts | 84 + .../test-withdrawal-bank-integrated.ts | 91 + .../integrationtests/test-withdrawal-fakebank.ts | 97 + .../src/integrationtests/test-withdrawal-high.ts | 99 + .../src/integrationtests/test-withdrawal-manual.ts | 84 + .../src/integrationtests/testrunner.ts | 496 +++++ packages/taler-harness/src/lint.ts | 534 ++++++ packages/taler-harness/tsconfig.json | 33 + packages/taler-wallet-cli/assets/.gitkeep | 0 packages/taler-wallet-cli/build.mjs | 11 +- packages/taler-wallet-cli/package.json | 2 +- packages/taler-wallet-cli/src/assets.ts | 52 - packages/taler-wallet-cli/src/bench1.ts | 189 -- packages/taler-wallet-cli/src/bench2.ts | 170 -- packages/taler-wallet-cli/src/bench3.ts | 205 -- .../src/benchMerchantIDGenerator.ts | 83 - packages/taler-wallet-cli/src/env-full.ts | 101 - packages/taler-wallet-cli/src/env1.ts | 70 - .../src/harness/denomStructures.ts | 157 -- .../taler-wallet-cli/src/harness/faultInjection.ts | 256 --- packages/taler-wallet-cli/src/harness/harness.ts | 2024 -------------------- packages/taler-wallet-cli/src/harness/helpers.ts | 444 ----- .../taler-wallet-cli/src/harness/libeufin-apis.ts | 872 --------- packages/taler-wallet-cli/src/harness/libeufin.ts | 910 --------- .../src/harness/merchantApiTypes.ts | 337 ---- packages/taler-wallet-cli/src/harness/sync.ts | 119 -- packages/taler-wallet-cli/src/index.ts | 273 +-- .../integrationtests/scenario-prompt-payment.ts | 60 - .../test-age-restrictions-merchant.ts | 201 -- .../test-age-restrictions-mixed-merchant.ts | 116 -- .../integrationtests/test-age-restrictions-peer.ts | 92 - .../src/integrationtests/test-bank-api.ts | 136 -- .../src/integrationtests/test-claim-loop.ts | 79 - .../src/integrationtests/test-clause-schnorr.ts | 97 - .../src/integrationtests/test-denom-unoffered.ts | 126 -- .../src/integrationtests/test-deposit.ts | 71 - .../integrationtests/test-exchange-management.ts | 285 --- .../integrationtests/test-exchange-timetravel.ts | 240 --- .../src/integrationtests/test-fee-regression.ts | 200 -- .../src/integrationtests/test-forced-selection.ts | 87 - .../test-libeufin-api-bankaccount.ts | 109 -- .../test-libeufin-api-bankconnection.ts | 56 - .../test-libeufin-api-facade-bad-request.ts | 71 - .../integrationtests/test-libeufin-api-facade.ts | 70 - .../test-libeufin-api-permissions.ts | 64 - .../test-libeufin-api-sandbox-camt.ts | 76 - .../test-libeufin-api-sandbox-transactions.ts | 69 - .../test-libeufin-api-scheduling.ts | 106 - .../integrationtests/test-libeufin-api-users.ts | 63 - .../integrationtests/test-libeufin-bad-gateway.ts | 74 - .../src/integrationtests/test-libeufin-basic.ts | 308 --- .../src/integrationtests/test-libeufin-c5x.ts | 147 -- .../test-libeufin-facade-anastasis.ts | 169 -- .../integrationtests/test-libeufin-keyrotation.ts | 79 - .../test-libeufin-nexus-balance.ts | 118 -- .../test-libeufin-refund-multiple-users.ts | 104 - .../src/integrationtests/test-libeufin-refund.ts | 101 - .../test-libeufin-sandbox-wire-transfer-cli.ts | 85 - .../src/integrationtests/test-libeufin-tutorial.ts | 128 -- .../test-merchant-exchange-confusion.ts | 243 --- .../test-merchant-instances-delete.ts | 129 -- .../test-merchant-instances-urls.ts | 189 -- .../integrationtests/test-merchant-instances.ts | 184 -- .../integrationtests/test-merchant-longpolling.ts | 162 -- .../integrationtests/test-merchant-refund-api.ts | 303 --- .../test-merchant-spec-public-orders.ts | 620 ------ .../src/integrationtests/test-pay-paid.ts | 222 --- .../src/integrationtests/test-payment-claim.ts | 110 -- .../src/integrationtests/test-payment-fault.ts | 222 --- .../integrationtests/test-payment-forgettable.ts | 81 - .../integrationtests/test-payment-idempotency.ts | 121 -- .../src/integrationtests/test-payment-multiple.ts | 163 -- .../src/integrationtests/test-payment-on-demo.ts | 114 -- .../src/integrationtests/test-payment-transient.ts | 185 -- .../src/integrationtests/test-payment-zero.ts | 72 - .../src/integrationtests/test-payment.ts | 77 - .../src/integrationtests/test-paywall-flow.ts | 252 --- .../src/integrationtests/test-peer-to-peer-pull.ts | 101 - .../src/integrationtests/test-peer-to-peer-push.ts | 119 -- .../src/integrationtests/test-refund-auto.ts | 105 - .../src/integrationtests/test-refund-gone.ts | 124 -- .../integrationtests/test-refund-incremental.ts | 202 -- .../src/integrationtests/test-refund.ts | 106 - .../src/integrationtests/test-revocation.ts | 215 --- .../test-timetravel-autorefresh.ts | 216 --- .../integrationtests/test-timetravel-withdraw.ts | 98 - .../src/integrationtests/test-tipping.ts | 129 -- .../integrationtests/test-wallet-backup-basic.ts | 168 -- .../test-wallet-backup-doublespend.ts | 174 -- .../src/integrationtests/test-wallet-balance.ts | 144 -- .../integrationtests/test-wallet-cryptoworker.ts | 55 - .../src/integrationtests/test-wallet-dbless.ts | 112 -- .../src/integrationtests/test-wallettesting.ts | 233 --- .../integrationtests/test-withdrawal-abort-bank.ts | 84 - .../test-withdrawal-bank-integrated.ts | 91 - .../integrationtests/test-withdrawal-fakebank.ts | 97 - .../src/integrationtests/test-withdrawal-high.ts | 99 - .../src/integrationtests/test-withdrawal-manual.ts | 84 - .../src/integrationtests/testrunner.ts | 496 ----- packages/taler-wallet-cli/src/lint.ts | 534 ------ packages/taler-wallet-embedded/package.json | 6 +- packages/taler-wallet-webextension/package.json | 4 +- packages/web-util/package.json | 2 +- 193 files changed, 17524 insertions(+), 17299 deletions(-) create mode 100644 packages/taler-harness/Makefile create mode 100755 packages/taler-harness/bin/taler-harness.mjs create mode 100755 packages/taler-harness/build.mjs create mode 100644 packages/taler-harness/package.json create mode 100644 packages/taler-harness/src/bench1.ts create mode 100644 packages/taler-harness/src/bench2.ts create mode 100644 packages/taler-harness/src/bench3.ts create mode 100644 packages/taler-harness/src/benchMerchantIDGenerator.ts create mode 100644 packages/taler-harness/src/env-full.ts create mode 100644 packages/taler-harness/src/env1.ts create mode 100644 packages/taler-harness/src/harness/denomStructures.ts create mode 100644 packages/taler-harness/src/harness/faultInjection.ts create mode 100644 packages/taler-harness/src/harness/harness.ts create mode 100644 packages/taler-harness/src/harness/helpers.ts create mode 100644 packages/taler-harness/src/harness/libeufin-apis.ts create mode 100644 packages/taler-harness/src/harness/libeufin.ts create mode 100644 packages/taler-harness/src/harness/merchantApiTypes.ts create mode 100644 packages/taler-harness/src/harness/sync.ts create mode 100644 packages/taler-harness/src/index.ts create mode 100644 packages/taler-harness/src/integrationtests/scenario-prompt-payment.ts create mode 100644 packages/taler-harness/src/integrationtests/test-age-restrictions-merchant.ts create mode 100644 packages/taler-harness/src/integrationtests/test-age-restrictions-mixed-merchant.ts create mode 100644 packages/taler-harness/src/integrationtests/test-age-restrictions-peer.ts create mode 100644 packages/taler-harness/src/integrationtests/test-bank-api.ts create mode 100644 packages/taler-harness/src/integrationtests/test-claim-loop.ts create mode 100644 packages/taler-harness/src/integrationtests/test-clause-schnorr.ts create mode 100644 packages/taler-harness/src/integrationtests/test-denom-unoffered.ts create mode 100644 packages/taler-harness/src/integrationtests/test-deposit.ts create mode 100644 packages/taler-harness/src/integrationtests/test-exchange-management.ts create mode 100644 packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts create mode 100644 packages/taler-harness/src/integrationtests/test-fee-regression.ts create mode 100644 packages/taler-harness/src/integrationtests/test-forced-selection.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-api-bankaccount.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-api-bankconnection.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-api-facade-bad-request.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-api-facade.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-api-permissions.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-camt.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-api-sandbox-transactions.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-api-scheduling.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-api-users.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-bad-gateway.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-basic.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-c5x.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-facade-anastasis.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-keyrotation.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-nexus-balance.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-refund-multiple-users.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-refund.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts create mode 100644 packages/taler-harness/src/integrationtests/test-libeufin-tutorial.ts create mode 100644 packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts create mode 100644 packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts create mode 100644 packages/taler-harness/src/integrationtests/test-merchant-instances-urls.ts create mode 100644 packages/taler-harness/src/integrationtests/test-merchant-instances.ts create mode 100644 packages/taler-harness/src/integrationtests/test-merchant-longpolling.ts create mode 100644 packages/taler-harness/src/integrationtests/test-merchant-refund-api.ts create mode 100644 packages/taler-harness/src/integrationtests/test-merchant-spec-public-orders.ts create mode 100644 packages/taler-harness/src/integrationtests/test-pay-paid.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-claim.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-fault.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-forgettable.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-idempotency.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-multiple.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-on-demo.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-transient.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-zero.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment.ts create mode 100644 packages/taler-harness/src/integrationtests/test-paywall-flow.ts create mode 100644 packages/taler-harness/src/integrationtests/test-peer-to-peer-pull.ts create mode 100644 packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts create mode 100644 packages/taler-harness/src/integrationtests/test-refund-auto.ts create mode 100644 packages/taler-harness/src/integrationtests/test-refund-gone.ts create mode 100644 packages/taler-harness/src/integrationtests/test-refund-incremental.ts create mode 100644 packages/taler-harness/src/integrationtests/test-refund.ts create mode 100644 packages/taler-harness/src/integrationtests/test-revocation.ts create mode 100644 packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts create mode 100644 packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts create mode 100644 packages/taler-harness/src/integrationtests/test-tipping.ts create mode 100644 packages/taler-harness/src/integrationtests/test-wallet-backup-basic.ts create mode 100644 packages/taler-harness/src/integrationtests/test-wallet-backup-doublespend.ts create mode 100644 packages/taler-harness/src/integrationtests/test-wallet-balance.ts create mode 100644 packages/taler-harness/src/integrationtests/test-wallet-cryptoworker.ts create mode 100644 packages/taler-harness/src/integrationtests/test-wallet-dbless.ts create mode 100644 packages/taler-harness/src/integrationtests/test-wallettesting.ts create mode 100644 packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts create mode 100644 packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts create mode 100644 packages/taler-harness/src/integrationtests/test-withdrawal-fakebank.ts create mode 100644 packages/taler-harness/src/integrationtests/test-withdrawal-high.ts create mode 100644 packages/taler-harness/src/integrationtests/test-withdrawal-manual.ts create mode 100644 packages/taler-harness/src/integrationtests/testrunner.ts create mode 100644 packages/taler-harness/src/lint.ts create mode 100644 packages/taler-harness/tsconfig.json delete mode 100644 packages/taler-wallet-cli/assets/.gitkeep delete mode 100644 packages/taler-wallet-cli/src/assets.ts delete mode 100644 packages/taler-wallet-cli/src/bench1.ts delete mode 100644 packages/taler-wallet-cli/src/bench2.ts delete mode 100644 packages/taler-wallet-cli/src/bench3.ts delete mode 100644 packages/taler-wallet-cli/src/benchMerchantIDGenerator.ts delete mode 100644 packages/taler-wallet-cli/src/env-full.ts delete mode 100644 packages/taler-wallet-cli/src/env1.ts delete mode 100644 packages/taler-wallet-cli/src/harness/denomStructures.ts delete mode 100644 packages/taler-wallet-cli/src/harness/faultInjection.ts delete mode 100644 packages/taler-wallet-cli/src/harness/harness.ts delete mode 100644 packages/taler-wallet-cli/src/harness/helpers.ts delete mode 100644 packages/taler-wallet-cli/src/harness/libeufin-apis.ts delete mode 100644 packages/taler-wallet-cli/src/harness/libeufin.ts delete mode 100644 packages/taler-wallet-cli/src/harness/merchantApiTypes.ts delete mode 100644 packages/taler-wallet-cli/src/harness/sync.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-mixed-merchant.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-peer.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-clause-schnorr.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-deposit.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-forced-selection.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-payment.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-refund.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-revocation.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-tipping.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-wallet-balance.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-wallet-cryptoworker.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-withdrawal-high.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts delete mode 100644 packages/taler-wallet-cli/src/integrationtests/testrunner.ts delete mode 100644 packages/taler-wallet-cli/src/lint.ts (limited to 'packages') diff --git a/packages/demobank-ui/package.json b/packages/demobank-ui/package.json index 8ba3448a4..d915ee31b 100644 --- a/packages/demobank-ui/package.json +++ b/packages/demobank-ui/package.json @@ -47,7 +47,7 @@ "@types/chai": "^4.3.0", "@types/history": "^4.7.8", "@types/mocha": "^10.0.1", - "@types/node": "^18.11.14", + "@types/node": "^18.11.17", "@typescript-eslint/eslint-plugin": "^5.41.0", "@typescript-eslint/parser": "^5.41.0", "async_hooks": "^1.0.0", diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json index b93ca9f0d..9a58b8695 100644 --- a/packages/idb-bridge/package.json +++ b/packages/idb-bridge/package.json @@ -22,7 +22,7 @@ } }, "devDependencies": { - "@types/node": "^18.8.5", + "@types/node": "^18.11.17", "ava": "^4.3.3", "esm": "^3.2.25", "prettier": "^2.5.1", diff --git a/packages/idb-bridge/src/util/structuredClone.ts b/packages/idb-bridge/src/util/structuredClone.ts index c33dc5e36..5ed269db3 100644 --- a/packages/idb-bridge/src/util/structuredClone.ts +++ b/packages/idb-bridge/src/util/structuredClone.ts @@ -425,6 +425,11 @@ export function structuredRevive(val: any): any { * Structured clone for IndexedDB. */ export function structuredClone(val: any): any { + // @ts-ignore + if (globalThis._tart?.structuredClone) { + // @ts-ignore + return globalThis._tart?.structuredClone(val); + } return mkDeepClone()(val); } @@ -432,5 +437,11 @@ export function structuredClone(val: any): any { * Structured clone for IndexedDB. */ export function checkStructuredCloneOrThrow(val: any): void { - return mkDeepCloneCheckOnly()(val); + // @ts-ignore + if (globalThis._tart?.structuredClone) { + // @ts-ignore + globalThis._tart?.structuredClone(val); + return; + } + mkDeepCloneCheckOnly()(val); } diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json index ca8923e05..beacd42f6 100644 --- a/packages/merchant-backoffice-ui/package.json +++ b/packages/merchant-backoffice-ui/package.json @@ -53,7 +53,7 @@ "@types/history": "^4.7.8", "@types/jest": "^26.0.23", "@types/mocha": "^8.2.3", - "@types/node": "^18.8.5", + "@types/node": "^18.11.17", "@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/parser": "^4.22.0", "base64-inline-loader": "^1.1.1", diff --git a/packages/pogen/package.json b/packages/pogen/package.json index 9b6daad13..11033c5de 100644 --- a/packages/pogen/package.json +++ b/packages/pogen/package.json @@ -15,7 +15,7 @@ "typescript": "^4.8.4" }, "dependencies": { - "@types/node": "^18.8.5", + "@types/node": "^18.11.17", "glob": "^7.2.0" } } diff --git a/packages/taler-harness/Makefile b/packages/taler-harness/Makefile new file mode 100644 index 000000000..85c146641 --- /dev/null +++ b/packages/taler-harness/Makefile @@ -0,0 +1,30 @@ +# This Makefile has been placed in the public domain. + +-include ../../.config.mk +include .config.mk + +$(info prefix is $(prefix)) + +all: + @echo use 'make install' to build and install taler-harness + +ifndef prefix +.PHONY: warn-noprefix install +warn-noprefix: + @echo "no prefix configured, did you run ./configure?" +install: warn-noprefix +else +install_target = $(prefix)/lib/taler-harness +.PHONY: install install-nodeps +install: + pnpm install --frozen-lockfile --filter @gnu-taler/taler-harness... + install -d $(prefix)/bin + install -d $(install_target)/bin + install -d $(install_target)/node_modules/taler-harness + install -d $(install_target)/node_modules/taler-harness/bin + install -d $(install_target)/node_modules/taler-harness/dist + install ./dist/taler-harness-bundled.cjs $(install_target)/node_modules/taler-harness/dist/ + install ./dist/taler-harness-bundled.cjs.map $(install_target)/node_modules/taler-harness/dist/ + install ./bin/taler-harness.mjs $(install_target)/node_modules/taler-harness/bin/ + ln -sf $(install_target)/node_modules/taler-harness/bin/taler-harness.mjs $(prefix)/bin/taler-harness +endif diff --git a/packages/taler-harness/bin/taler-harness.mjs b/packages/taler-harness/bin/taler-harness.mjs new file mode 100755 index 000000000..f8deebedb --- /dev/null +++ b/packages/taler-harness/bin/taler-harness.mjs @@ -0,0 +1,19 @@ +#!/usr/bin/env node +/* + This file is part of GNU Taler + (C) 2022 Taler Systems SA + + 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. + + 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 + TALER; see the file COPYING. If not, see + */ + +import { main } from '../dist/taler-harness-bundled.cjs'; +main(); diff --git a/packages/taler-harness/build.mjs b/packages/taler-harness/build.mjs new file mode 100755 index 000000000..b02d159e1 --- /dev/null +++ b/packages/taler-harness/build.mjs @@ -0,0 +1,70 @@ +#!/usr/bin/env node +/* + 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 + */ + +import esbuild from 'esbuild' +import path from "path" +import fs from "fs" + +const BASE = process.cwd() + +let GIT_ROOT = BASE +while (!fs.existsSync(path.join(GIT_ROOT, '.git')) && GIT_ROOT !== '/') { + GIT_ROOT = path.join(GIT_ROOT, '../') +} +if (GIT_ROOT === '/') { + console.log("not found") + process.exit(1); +} +const GIT_HASH = GIT_ROOT === '/' ? undefined : git_hash() + + +let _package = JSON.parse(fs.readFileSync(path.join(BASE, 'package.json'))); + +function git_hash() { + const rev = fs.readFileSync(path.join(GIT_ROOT, '.git', 'HEAD')).toString().trim().split(/.*[: ]/).slice(-1)[0]; + if (rev.indexOf('/') === -1) { + return rev; + } else { + return fs.readFileSync(path.join(GIT_ROOT, '.git', rev)).toString().trim(); + } +} + +// Still commonjs, because axios doesn't work properly under mjs +export const buildConfig = { + entryPoints: ["src/index.ts"], + outfile: "dist/taler-harness-bundled.cjs", + bundle: true, + minify: false, + target: [ + 'es2020' + ], + format: 'cjs', + platform: 'node', + sourcemap: true, + define: { + '__VERSION__': `"${_package.version}"`, + '__GIT_HASH__': `"${GIT_HASH}"`, + }, +} + +esbuild + .build(buildConfig) + .catch((e) => { + console.log(e) + process.exit(1) + }); + diff --git a/packages/taler-harness/package.json b/packages/taler-harness/package.json new file mode 100644 index 000000000..f521924c4 --- /dev/null +++ b/packages/taler-harness/package.json @@ -0,0 +1,44 @@ +{ + "name": "@gnu-taler/taler-harness", + "version": "0.9.0-dev.2", + "description": "", + "engines": { + "node": ">=0.12.0" + }, + "repository": { + "type": "git", + "url": "git://git.taler.net/wallet-core.git" + }, + "author": "Florian Dold", + "license": "GPL-3.0", + "bin": { + "taler-harness": "./bin/taler-harness.mjs" + }, + "type": "module", + "scripts": { + "compile": "./build.mjs", + "check": "tsc", + "clean": "rimraf lib dist tsconfig.tsbuildinfo", + "pretty": "prettier --write src" + }, + "files": [ + "AUTHORS", + "README", + "COPYING", + "bin/", + "dist/node", + "src/" + ], + "devDependencies": { + "@types/node": "^18.11.17", + "prettier": "^2.5.1", + "rimraf": "^3.0.2", + "typescript": "^4.8.4" + }, + "dependencies": { + "@gnu-taler/taler-util": "workspace:*", + "@gnu-taler/taler-wallet-core": "workspace:*", + "axios": "^0.27.2", + "tslib": "^2.4.0" + } +} diff --git a/packages/taler-harness/src/bench1.ts b/packages/taler-harness/src/bench1.ts new file mode 100644 index 000000000..84786d25a --- /dev/null +++ b/packages/taler-harness/src/bench1.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 + */ + +/** + * Imports. + */ +import { + buildCodecForObject, + codecForNumber, + codecForString, + codecForBoolean, + codecOptional, + j2s, + Logger, +} from "@gnu-taler/taler-util"; +import { + getDefaultNodeWallet2, + NodeHttpLib, + WalletApiOperation, + Wallet, + AccessStats, +} from "@gnu-taler/taler-wallet-core"; + +/** + * Entry point for the benchmark. + * + * The benchmark runs against an existing Taler deployment and does not + * set up its own services. + */ +export async function runBench1(configJson: any): Promise { + const logger = new Logger("Bench1"); + + // Validate the configuration file for this benchmark. + const b1conf = codecForBench1Config().decode(configJson); + + const myHttpLib = new NodeHttpLib(); + myHttpLib.setThrottling(false); + + const numIter = b1conf.iterations ?? 1; + const numDeposits = b1conf.deposits ?? 5; + const restartWallet = b1conf.restartAfter ?? 20; + + const withdrawOnly = b1conf.withdrawOnly ?? false; + const withdrawAmount = (numDeposits + 1) * 10; + + logger.info( + `Starting Benchmark iterations=${numIter} deposits=${numDeposits}`, + ); + + const trustExchange = !!process.env["TALER_WALLET_INSECURE_TRUST_EXCHANGE"]; + if (trustExchange) { + logger.info("trusting exchange (not validating signatures)"); + } else { + logger.info("not trusting exchange (validating signatures)"); + } + const batchWithdrawal = !!process.env["TALER_WALLET_BATCH_WITHDRAWAL"]; + + let wallet = {} as Wallet; + let getDbStats: () => AccessStats; + + for (let i = 0; i < numIter; i++) { + // Create a new wallet in each iteration + // otherwise the TPS go down + // my assumption is that the in-memory db file gets too large + if (i % restartWallet == 0) { + if (Object.keys(wallet).length !== 0) { + wallet.stop(); + console.log("wallet DB stats", j2s(getDbStats!())); + } + + const res = await getDefaultNodeWallet2({ + // No persistent DB storage. + persistentStoragePath: undefined, + httpLib: myHttpLib, + }); + wallet = res.wallet; + getDbStats = res.getDbStats; + if (trustExchange) { + wallet.setInsecureTrustExchange(); + } + wallet.setBatchWithdrawal(batchWithdrawal); + await wallet.client.call(WalletApiOperation.InitWallet, {}); + } + + logger.trace(`Starting withdrawal amount=${withdrawAmount}`); + let start = Date.now(); + + await wallet.client.call(WalletApiOperation.WithdrawFakebank, { + amount: b1conf.currency + ":" + withdrawAmount, + bank: b1conf.bank, + exchange: b1conf.exchange, + }); + + await wallet.runTaskLoop({ + stopWhenDone: true, + }); + + logger.info( + `Finished withdrawal amount=${withdrawAmount} time=${Date.now() - start}`, + ); + + if (!withdrawOnly) { + for (let i = 0; i < numDeposits; i++) { + logger.trace(`Starting deposit amount=10`); + start = Date.now(); + + await wallet.client.call(WalletApiOperation.CreateDepositGroup, { + amount: b1conf.currency + ":10", + depositPaytoUri: b1conf.payto, + }); + + await wallet.runTaskLoop({ + stopWhenDone: true, + }); + + logger.info(`Finished deposit amount=10 time=${Date.now() - start}`); + } + } + } + + wallet.stop(); + console.log("wallet DB stats", j2s(getDbStats!())); +} + +/** + * Format of the configuration file passed to the benchmark + */ +interface Bench1Config { + /** + * Base URL of the bank. + */ + bank: string; + + /** + * Payto url for deposits. + */ + payto: string; + + /** + * Base URL of the exchange. + */ + exchange: string; + + /** + * How many withdraw/deposit iterations should be made? + * Defaults to 1. + */ + iterations?: number; + + currency: string; + + deposits?: number; + + /** + * How any iterations run until the wallet db gets purged + * Defaults to 20. + */ + restartAfter?: number; + + withdrawOnly?: boolean; +} + +/** + * Schema validation codec for Bench1Config. + */ +const codecForBench1Config = () => + buildCodecForObject() + .property("bank", codecForString()) + .property("payto", codecForString()) + .property("exchange", codecForString()) + .property("iterations", codecOptional(codecForNumber())) + .property("deposits", codecOptional(codecForNumber())) + .property("currency", codecForString()) + .property("restartAfter", codecOptional(codecForNumber())) + .property("withdrawOnly", codecOptional(codecForBoolean())) + .build("Bench1Config"); diff --git a/packages/taler-harness/src/bench2.ts b/packages/taler-harness/src/bench2.ts new file mode 100644 index 000000000..196737436 --- /dev/null +++ b/packages/taler-harness/src/bench2.ts @@ -0,0 +1,170 @@ +/* + 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 + */ + +/** + * Imports. + */ +import { + buildCodecForObject, + codecForNumber, + codecForString, + codecOptional, + Logger, +} from "@gnu-taler/taler-util"; +import { + checkReserve, + createFakebankReserve, + CryptoDispatcher, + depositCoin, + downloadExchangeInfo, + findDenomOrThrow, + NodeHttpLib, + refreshCoin, + SynchronousCryptoWorkerFactoryNode, + withdrawCoin, +} from "@gnu-taler/taler-wallet-core"; + +/** + * Entry point for the benchmark. + * + * The benchmark runs against an existing Taler deployment and does not + * set up its own services. + */ +export async function runBench2(configJson: any): Promise { + const logger = new Logger("Bench2"); + + // Validate the configuration file for this benchmark. + const benchConf = codecForBench2Config().decode(configJson); + const curr = benchConf.currency; + const cryptoDisp = new CryptoDispatcher(new SynchronousCryptoWorkerFactoryNode()); + const cryptoApi = cryptoDisp.cryptoApi; + + const http = new NodeHttpLib(); + http.setThrottling(false); + + const numIter = benchConf.iterations ?? 1; + const numDeposits = benchConf.deposits ?? 5; + + const reserveAmount = (numDeposits + 1) * 10; + + for (let i = 0; i < numIter; i++) { + const exchangeInfo = await downloadExchangeInfo(benchConf.exchange, http); + + const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); + + console.log("creating fakebank reserve"); + + await createFakebankReserve({ + amount: `${curr}:${reserveAmount}`, + exchangeInfo, + fakebankBaseUrl: benchConf.bank, + http, + reservePub: reserveKeyPair.pub, + }); + + console.log("waiting for reserve"); + + await checkReserve(http, benchConf.exchange, reserveKeyPair.pub); + + console.log("reserve found"); + + const d1 = findDenomOrThrow(exchangeInfo, `${curr}:8`); + + for (let j = 0; j < numDeposits; j++) { + console.log("withdrawing coin"); + const coin = await withdrawCoin({ + http, + cryptoApi, + reserveKeyPair: { + reservePriv: reserveKeyPair.priv, + reservePub: reserveKeyPair.pub, + }, + denom: d1, + exchangeBaseUrl: benchConf.exchange, + }); + + console.log("depositing coin"); + + await depositCoin({ + amount: `${curr}:4`, + coin: coin, + cryptoApi, + exchangeBaseUrl: benchConf.exchange, + http, + depositPayto: benchConf.payto, + }); + + const refreshDenoms = [ + findDenomOrThrow(exchangeInfo, `${curr}:1`), + findDenomOrThrow(exchangeInfo, `${curr}:1`), + ]; + + console.log("refreshing coin"); + + await refreshCoin({ + oldCoin: coin, + cryptoApi, + http, + newDenoms: refreshDenoms, + }); + + console.log("refresh done"); + } + } +} + +/** + * Format of the configuration file passed to the benchmark + */ +interface Bench2Config { + /** + * Base URL of the bank. + */ + bank: string; + + /** + * Payto url for deposits. + */ + payto: string; + + /** + * Base URL of the exchange. + */ + exchange: string; + + /** + * How many withdraw/deposit iterations should be made? + * Defaults to 1. + */ + iterations?: number; + + currency: string; + + deposits?: number; +} + +/** + * Schema validation codec for Bench1Config. + */ +const codecForBench2Config = () => + buildCodecForObject() + .property("bank", codecForString()) + .property("payto", codecForString()) + .property("exchange", codecForString()) + .property("iterations", codecOptional(codecForNumber())) + .property("deposits", codecOptional(codecForNumber())) + .property("currency", codecForString()) + .build("Bench2Config"); diff --git a/packages/taler-harness/src/bench3.ts b/packages/taler-harness/src/bench3.ts new file mode 100644 index 000000000..6041c525c --- /dev/null +++ b/packages/taler-harness/src/bench3.ts @@ -0,0 +1,205 @@ +/* + 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 + */ + +/** + * Imports. + */ +import { + buildCodecForObject, + codecForNumber, + codecForString, + codecOptional, + j2s, + Logger, +} from "@gnu-taler/taler-util"; +import { + getDefaultNodeWallet2, + NodeHttpLib, + WalletApiOperation, + Wallet, + AccessStats, +} from "@gnu-taler/taler-wallet-core"; +import benchMerchantIDGenerator from "./benchMerchantIDGenerator.js"; + +/** + * Entry point for the benchmark. + * + * The benchmark runs against an existing Taler deployment and does not + * set up its own services. + */ +export async function runBench3(configJson: any): Promise { + const logger = new Logger("Bench3"); + + // Validate the configuration file for this benchmark. + const b3conf = codecForBench3Config().decode(configJson); + + if (!b3conf.paytoTemplate.includes("${id")) { + throw new Error("Payto template url must contain '${id}' placeholder"); + } + + const myHttpLib = new NodeHttpLib(); + myHttpLib.setThrottling(false); + + const numIter = b3conf.iterations ?? 1; + const numDeposits = b3conf.deposits ?? 5; + const restartWallet = b3conf.restartAfter ?? 20; + + const withdrawAmount = (numDeposits + 1) * 10; + + const IDGenerator = benchMerchantIDGenerator(b3conf.randomAlg, b3conf.numMerchants ?? 100); + + logger.info( + `Starting Benchmark iterations=${numIter} deposits=${numDeposits} with ${b3conf.randomAlg} merchant selection`, + ); + + const trustExchange = !!process.env["TALER_WALLET_INSECURE_TRUST_EXCHANGE"]; + if (trustExchange) { + logger.info("trusting exchange (not validating signatures)"); + } else { + logger.info("not trusting exchange (validating signatures)"); + } + const batchWithdrawal = !!process.env["TALER_WALLET_BATCH_WITHDRAWAL"]; + + let wallet = {} as Wallet; + let getDbStats: () => AccessStats; + + for (let i = 0; i < numIter; i++) { + // Create a new wallet in each iteration + // otherwise the TPS go down + // my assumption is that the in-memory db file gets too large + if (i % restartWallet == 0) { + if (Object.keys(wallet).length !== 0) { + wallet.stop(); + console.log("wallet DB stats", j2s(getDbStats!())); + } + + const res = await getDefaultNodeWallet2({ + // No persistent DB storage. + persistentStoragePath: undefined, + httpLib: myHttpLib, + }); + wallet = res.wallet; + getDbStats = res.getDbStats; + if (trustExchange) { + wallet.setInsecureTrustExchange(); + } + wallet.setBatchWithdrawal(batchWithdrawal); + await wallet.client.call(WalletApiOperation.InitWallet, {}); + } + + logger.trace(`Starting withdrawal amount=${withdrawAmount}`); + let start = Date.now(); + + await wallet.client.call(WalletApiOperation.WithdrawFakebank, { + amount: b3conf.currency + ":" + withdrawAmount, + bank: b3conf.bank, + exchange: b3conf.exchange, + }); + + await wallet.runTaskLoop({ + stopWhenDone: true, + }); + + logger.info( + `Finished withdrawal amount=${withdrawAmount} time=${Date.now() - start}`, + ); + + for (let i = 0; i < numDeposits; i++) { + logger.trace(`Starting deposit amount=10`); + start = Date.now(); + + let merchID = IDGenerator.getRandomMerchantID(); + let payto = b3conf.paytoTemplate.replace("${id}", merchID.toString()); + + await wallet.client.call(WalletApiOperation.CreateDepositGroup, { + amount: b3conf.currency + ":10", + depositPaytoUri: payto, + }); + + await wallet.runTaskLoop({ + stopWhenDone: true, + }); + + logger.info(`Finished deposit amount=10 time=${Date.now() - start}`); + } + } + + wallet.stop(); + console.log("wallet DB stats", j2s(getDbStats!())); +} + +/** + * Format of the configuration file passed to the benchmark + */ +interface Bench3Config { + /** + * Base URL of the bank. + */ + bank: string; + + /** + * Payto url template for deposits, must contain '${id}' for replacements. + */ + paytoTemplate: string; + + /** + * Base URL of the exchange. + */ + exchange: string; + + /** + * How many withdraw/deposit iterations should be made? + * Defaults to 1. + */ + iterations?: number; + + currency: string; + + deposits?: number; + + /** + * How any iterations run until the wallet db gets purged + * Defaults to 20. + */ + restartAfter?: number; + + /** + * Number of merchants to select from randomly + */ + numMerchants?: number; + + /** + * Which random generator to use. + * Possible values: 'zipf', 'rand' + */ + randomAlg: string; +} + +/** + * Schema validation codec for Bench1Config. + */ +const codecForBench3Config = () => + buildCodecForObject() + .property("bank", codecForString()) + .property("paytoTemplate", codecForString()) + .property("numMerchants", codecOptional(codecForNumber())) + .property("randomAlg", codecForString()) + .property("exchange", codecForString()) + .property("iterations", codecOptional(codecForNumber())) + .property("deposits", codecOptional(codecForNumber())) + .property("currency", codecForString()) + .property("restartAfter", codecOptional(codecForNumber())) + .build("Bench1Config"); diff --git a/packages/taler-harness/src/benchMerchantIDGenerator.ts b/packages/taler-harness/src/benchMerchantIDGenerator.ts new file mode 100644 index 000000000..b83c12bb8 --- /dev/null +++ b/packages/taler-harness/src/benchMerchantIDGenerator.ts @@ -0,0 +1,83 @@ +/* + 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 + + @author: Boss Marco + */ + +const getRandomInt = function(max: number) { + return Math.floor(Math.random() * max); +} + +abstract class BenchMerchantIDGenerator { + abstract getRandomMerchantID(): number +} + +class ZipfGenerator extends BenchMerchantIDGenerator { + + weights: number[]; + total_weight: number; + + constructor(numMerchants: number) { + super(); + this.weights = new Array(numMerchants); + for (var i = 0; i < this.weights.length; i++) { + /* we use integers (floor), make sure we have big enough values + * by multiplying with + * numMerchants again */ + this.weights[i] = Math.floor((numMerchants/(i+1)) * numMerchants); + } + this.total_weight = this.weights.reduce((p, n) => p + n); + } + + getRandomMerchantID(): number { + let random = getRandomInt(this.total_weight); + let current = 0; + + for (var i = 0; i < this.weights.length; i++) { + current += this.weights[i]; + if (random <= current) { + return i+1; + } + } + + /* should never come here */ + return getRandomInt(this.weights.length); + } +} + +class RandomGenerator extends BenchMerchantIDGenerator { + + max: number + + constructor(numMerchants: number) { + super(); + this.max = numMerchants + } + + getRandomMerchantID() { + return getRandomInt(this.max); + } +} + +export default function(type: string, maxID: number): BenchMerchantIDGenerator { + switch (type) { + case "zipf": + return new ZipfGenerator(maxID); + case "rand": + return new RandomGenerator(maxID); + default: + throw new Error("Valid types are 'zipf' and 'rand'"); + } +} diff --git a/packages/taler-harness/src/env-full.ts b/packages/taler-harness/src/env-full.ts new file mode 100644 index 000000000..3a684db0b --- /dev/null +++ b/packages/taler-harness/src/env-full.ts @@ -0,0 +1,101 @@ +/* + 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 + */ + +/** + * Imports. + */ +import { Duration, j2s, URL } from "@gnu-taler/taler-util"; +import { CoinConfig, defaultCoinConfig } from "./harness/denomStructures.js"; +import { + GlobalTestState, + setupDb, + ExchangeService, + FakebankService, + MerchantService, + getPayto, +} from "./harness/harness.js"; + +/** + * Entry point for the full Taler test environment. + */ +export async function runEnvFull(t: GlobalTestState): Promise { + 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", + ); + console.log("exchange bank account", j2s(exchangeBankAccount)); + 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(); + + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [getPayto("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: [getPayto("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + console.log("setup done!"); +} diff --git a/packages/taler-harness/src/env1.ts b/packages/taler-harness/src/env1.ts new file mode 100644 index 000000000..aec0b7b8f --- /dev/null +++ b/packages/taler-harness/src/env1.ts @@ -0,0 +1,70 @@ +/* + 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 + */ + +/** + * Imports. + */ +import { URL } from "@gnu-taler/taler-util"; +import { CoinConfig, defaultCoinConfig } from "./harness/denomStructures.js"; +import { + GlobalTestState, + setupDb, + ExchangeService, + FakebankService, +} from "./harness/harness.js"; + +/** + * Entry point for the benchmark. + * + * The benchmark runs against an existing Taler deployment and does not + * set up its own services. + */ +export async function runEnv1(t: GlobalTestState): Promise { + const db = await setupDb(t); + + const bank = await FakebankService.create(t, { + currency: "TESTKUDOS", + httpPort: 8082, + allowRegistrations: true, + 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!"); +} diff --git a/packages/taler-harness/src/harness/denomStructures.ts b/packages/taler-harness/src/harness/denomStructures.ts new file mode 100644 index 000000000..b12857c7e --- /dev/null +++ b/packages/taler-harness/src/harness/denomStructures.ts @@ -0,0 +1,157 @@ +/* + 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 + */ + +export interface CoinCoinfigCommon { + name: string; + value: string; + durationWithdraw: string; + durationSpend: string; + durationLegal: string; + feeWithdraw: string; + feeDeposit: string; + feeRefresh: string; + feeRefund: string; + ageRestricted?: boolean; +} + +export interface CoinConfigRsa extends CoinCoinfigCommon { + cipher: "RSA"; + rsaKeySize: number; +} + +/** + * Clause Schnorr coin config. + */ +export interface CoinConfigCs extends CoinCoinfigCommon { + cipher: "CS"; +} + +export type CoinConfig = CoinConfigRsa | CoinConfigCs; + +const coinRsaCommon = { + cipher: "RSA" as const, + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + rsaKeySize: 1024, +}; + +export const coin_ct1 = (curr: string): CoinConfig => ({ + ...coinRsaCommon, + name: `${curr}_ct1`, + value: `${curr}:0.01`, + feeDeposit: `${curr}:0.00`, + feeRefresh: `${curr}:0.01`, + feeRefund: `${curr}:0.00`, + feeWithdraw: `${curr}:0.01`, +}); + +export const coin_ct10 = (curr: string): CoinConfig => ({ + ...coinRsaCommon, + name: `${curr}_ct10`, + value: `${curr}:0.10`, + feeDeposit: `${curr}:0.01`, + feeRefresh: `${curr}:0.01`, + feeRefund: `${curr}:0.00`, + feeWithdraw: `${curr}:0.01`, +}); + +export const coin_u1 = (curr: string): CoinConfig => ({ + ...coinRsaCommon, + name: `${curr}_u1`, + value: `${curr}:1`, + feeDeposit: `${curr}:0.02`, + feeRefresh: `${curr}:0.02`, + feeRefund: `${curr}:0.02`, + feeWithdraw: `${curr}:0.02`, +}); + +export const coin_u2 = (curr: string): CoinConfig => ({ + ...coinRsaCommon, + name: `${curr}_u2`, + value: `${curr}:2`, + feeDeposit: `${curr}:0.02`, + feeRefresh: `${curr}:0.02`, + feeRefund: `${curr}:0.02`, + feeWithdraw: `${curr}:0.02`, +}); + +export const coin_u4 = (curr: string): CoinConfig => ({ + ...coinRsaCommon, + name: `${curr}_u4`, + value: `${curr}:4`, + feeDeposit: `${curr}:0.02`, + feeRefresh: `${curr}:0.02`, + feeRefund: `${curr}:0.02`, + feeWithdraw: `${curr}:0.02`, +}); + +export const coin_u8 = (curr: string): CoinConfig => ({ + ...coinRsaCommon, + name: `${curr}_u8`, + value: `${curr}:8`, + feeDeposit: `${curr}:0.16`, + feeRefresh: `${curr}:0.16`, + feeRefund: `${curr}:0.16`, + feeWithdraw: `${curr}:0.16`, +}); + +const coin_u10 = (curr: string): CoinConfig => ({ + ...coinRsaCommon, + name: `${curr}_u10`, + value: `${curr}:10`, + feeDeposit: `${curr}:0.2`, + feeRefresh: `${curr}:0.2`, + feeRefund: `${curr}:0.2`, + feeWithdraw: `${curr}:0.2`, +}); + +export const defaultCoinConfig = [ + coin_ct1, + coin_ct10, + coin_u1, + coin_u2, + coin_u4, + coin_u8, + coin_u10, +]; + +export function makeNoFeeCoinConfig(curr: string): CoinConfig[] { + const cc: CoinConfig[] = []; + + for (let i = 0; i < 16; i++) { + const ct = 2 ** i; + + const unit = Math.floor(ct / 100); + const cent = ct % 100; + + cc.push({ + cipher: "RSA", + durationLegal: "3 years", + durationSpend: "2 years", + durationWithdraw: "7 days", + rsaKeySize: 1024, + name: `${curr}-u${i}`, + feeDeposit: `${curr}:0`, + feeRefresh: `${curr}:0`, + feeRefund: `${curr}:0`, + feeWithdraw: `${curr}:0`, + value: `${curr}:${unit}.${cent}`, + }); + } + + return cc; +} diff --git a/packages/taler-harness/src/harness/faultInjection.ts b/packages/taler-harness/src/harness/faultInjection.ts new file mode 100644 index 000000000..4c3d0c123 --- /dev/null +++ b/packages/taler-harness/src/harness/faultInjection.ts @@ -0,0 +1,256 @@ +/* + 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 + */ + +/** + * Fault injection proxy. + * + * @author Florian Dold + */ + +/** + * Imports + */ +import * as http from "http"; +import { URL } from "url"; +import { + GlobalTestState, + ExchangeService, + ExchangeServiceInterface, + MerchantServiceInterface, + MerchantService, +} from "../harness/harness.js"; + +export interface FaultProxyConfig { + inboundPort: number; + targetPort: number; +} + +/** + * Fault injection context. Modified by fault injection functions. + */ +export interface FaultInjectionRequestContext { + requestUrl: string; + method: string; + requestHeaders: Record; + requestBody?: Buffer; + dropRequest: boolean; +} + +export interface FaultInjectionResponseContext { + request: FaultInjectionRequestContext; + statusCode: number; + responseHeaders: Record; + responseBody: Buffer | undefined; + dropResponse: boolean; +} + +export interface FaultSpec { + modifyRequest?: (ctx: FaultInjectionRequestContext) => Promise; + modifyResponse?: (ctx: FaultInjectionResponseContext) => Promise; +} + +export class FaultProxy { + constructor( + private globalTestState: GlobalTestState, + private faultProxyConfig: FaultProxyConfig, + ) {} + + private currentFaultSpecs: FaultSpec[] = []; + + start() { + const server = http.createServer((req, res) => { + const requestChunks: Buffer[] = []; + const requestUrl = `http://localhost:${this.faultProxyConfig.inboundPort}${req.url}`; + console.log("request for", new URL(requestUrl)); + req.on("data", (chunk) => { + requestChunks.push(chunk); + }); + req.on("end", async () => { + console.log("end of data"); + let requestBuffer: Buffer | undefined; + if (requestChunks.length > 0) { + requestBuffer = Buffer.concat(requestChunks); + } + console.log("full request body", requestBuffer); + + const faultReqContext: FaultInjectionRequestContext = { + dropRequest: false, + method: req.method!!, + requestHeaders: req.headers, + requestUrl, + requestBody: requestBuffer, + }; + + for (const faultSpec of this.currentFaultSpecs) { + if (faultSpec.modifyRequest) { + await faultSpec.modifyRequest(faultReqContext); + } + } + + if (faultReqContext.dropRequest) { + res.destroy(); + return; + } + + const faultedUrl = new URL(faultReqContext.requestUrl); + + const proxyRequest = http.request({ + method: faultReqContext.method, + host: "localhost", + port: this.faultProxyConfig.targetPort, + path: faultedUrl.pathname + faultedUrl.search, + headers: faultReqContext.requestHeaders, + }); + + console.log( + `proxying request to target path '${ + faultedUrl.pathname + faultedUrl.search + }'`, + ); + + if (faultReqContext.requestBody) { + proxyRequest.write(faultReqContext.requestBody); + } + proxyRequest.end(); + proxyRequest.on("response", (proxyResp) => { + console.log("gotten response from target", proxyResp.statusCode); + const respChunks: Buffer[] = []; + proxyResp.on("data", (proxyRespData) => { + respChunks.push(proxyRespData); + }); + proxyResp.on("end", async () => { + console.log("end of target response"); + let responseBuffer: Buffer | undefined; + if (respChunks.length > 0) { + responseBuffer = Buffer.concat(respChunks); + } + const faultRespContext: FaultInjectionResponseContext = { + request: faultReqContext, + dropResponse: false, + responseBody: responseBuffer, + responseHeaders: proxyResp.headers, + statusCode: proxyResp.statusCode!!, + }; + for (const faultSpec of this.currentFaultSpecs) { + const modResponse = faultSpec.modifyResponse; + if (modResponse) { + await modResponse(faultRespContext); + } + } + if (faultRespContext.dropResponse) { + req.destroy(); + return; + } + if (faultRespContext.responseBody) { + // We must accommodate for potentially changed content length + faultRespContext.responseHeaders[ + "content-length" + ] = `${faultRespContext.responseBody.byteLength}`; + } + console.log("writing response head"); + res.writeHead( + faultRespContext.statusCode, + http.STATUS_CODES[faultRespContext.statusCode], + faultRespContext.responseHeaders, + ); + if (faultRespContext.responseBody) { + res.write(faultRespContext.responseBody); + } + res.end(); + }); + }); + }); + }); + + server.listen(this.faultProxyConfig.inboundPort); + this.globalTestState.servers.push(server); + } + + addFault(f: FaultSpec) { + this.currentFaultSpecs.push(f); + } + + clearAllFaults() { + this.currentFaultSpecs = []; + } +} + +export class FaultInjectedExchangeService implements ExchangeServiceInterface { + baseUrl: string; + port: number; + faultProxy: FaultProxy; + + get name(): string { + return this.innerExchange.name; + } + + get masterPub(): string { + return this.innerExchange.masterPub; + } + + private innerExchange: ExchangeService; + + constructor( + t: GlobalTestState, + e: ExchangeService, + proxyInboundPort: number, + ) { + this.innerExchange = e; + this.faultProxy = new FaultProxy(t, { + inboundPort: proxyInboundPort, + targetPort: e.port, + }); + this.faultProxy.start(); + + const exchangeUrl = new URL(e.baseUrl); + exchangeUrl.port = `${proxyInboundPort}`; + this.baseUrl = exchangeUrl.href; + this.port = proxyInboundPort; + } +} + +export class FaultInjectedMerchantService implements MerchantServiceInterface { + baseUrl: string; + port: number; + faultProxy: FaultProxy; + + get name(): string { + return this.innerMerchant.name; + } + + private innerMerchant: MerchantService; + private inboundPort: number; + + constructor( + t: GlobalTestState, + m: MerchantService, + proxyInboundPort: number, + ) { + this.innerMerchant = m; + this.faultProxy = new FaultProxy(t, { + inboundPort: proxyInboundPort, + targetPort: m.port, + }); + this.faultProxy.start(); + this.inboundPort = proxyInboundPort; + } + + makeInstanceBaseUrl(instanceName?: string | undefined): string { + const url = new URL(this.innerMerchant.makeInstanceBaseUrl(instanceName)); + url.port = `${this.inboundPort}`; + return url.href; + } +} diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts new file mode 100644 index 000000000..6f722dc8d --- /dev/null +++ b/packages/taler-harness/src/harness/harness.ts @@ -0,0 +1,2024 @@ +/* + 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 + */ + +/** + * Test harness for various GNU Taler components. + * Also provides a fault-injection proxy. + * + * @author Florian Dold + */ + +const logger = new Logger("harness.ts"); + +/** + * Imports + */ +import { + AmountJson, + Amounts, + AmountString, + Configuration, + CoreApiResponse, + createEddsaKeyPair, + Duration, + eddsaGetPublic, + EddsaKeyPair, + encodeCrock, + hash, + j2s, + Logger, + parsePaytoUri, + stringToBytes, + TalerProtocolDuration, +} from "@gnu-taler/taler-util"; +import { + BankAccessApi, + BankApi, + BankServiceHandle, + HarnessExchangeBankAccount, + NodeHttpLib, + openPromise, + TalerError, + WalletCoreApiClient, +} from "@gnu-taler/taler-wallet-core"; +import { deepStrictEqual } from "assert"; +import axiosImp, { AxiosError } from "axios"; +import { ChildProcess, spawn } from "child_process"; +import * as child_process from "child_process"; +import * as fs from "fs"; +import * as http from "http"; +import * as path from "path"; +import * as readline from "readline"; +import { URL } from "url"; +import * as util from "util"; +import { CoinConfig } from "./denomStructures.js"; +import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js"; +import { + codecForMerchantOrderPrivateStatusResponse, + codecForPostOrderResponse, + MerchantInstancesResponse, + MerchantOrderPrivateStatusResponse, + PostOrderRequest, + PostOrderResponse, + TipCreateConfirmation, + TipCreateRequest, + TippingReserveStatus, +} from "./merchantApiTypes.js"; + +const exec = util.promisify(child_process.exec); + +const axios = axiosImp.default; + +export async function delayMs(ms: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), ms); + }); +} + +export interface WithAuthorization { + Authorization?: string; +} + +interface WaitResult { + code: number | null; + signal: NodeJS.Signals | null; +} + +/** + * Run a shell command, return stdout. + */ +export async function sh( + t: GlobalTestState, + logName: string, + command: string, + env: { [index: string]: string | undefined } = process.env, +): Promise { + logger.info(`running command ${command}`); + return new Promise((resolve, reject) => { + const stdoutChunks: Buffer[] = []; + const proc = spawn(command, { + stdio: ["inherit", "pipe", "pipe"], + shell: true, + env: env, + }); + proc.stdout.on("data", (x) => { + if (x instanceof Buffer) { + stdoutChunks.push(x); + } else { + throw Error("unexpected data chunk type"); + } + }); + const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); + const stderrLog = fs.createWriteStream(stderrLogFileName, { + flags: "a", + }); + proc.stderr.pipe(stderrLog); + proc.on("exit", (code, signal) => { + logger.info(`child process exited (${code} / ${signal})`); + if (code != 0) { + reject(Error(`Unexpected exit code ${code} for '${command}'`)); + return; + } + const b = Buffer.concat(stdoutChunks).toString("utf-8"); + resolve(b); + }); + proc.on("error", () => { + reject(Error("Child process had error")); + }); + }); +} + +function shellescape(args: string[]) { + const ret = args.map((s) => { + if (/[^A-Za-z0-9_\/:=-]/.test(s)) { + s = "'" + s.replace(/'/g, "'\\''") + "'"; + s = s.replace(/^(?:'')+/g, "").replace(/\\'''/g, "\\'"); + } + return s; + }); + return ret.join(" "); +} + +/** + * Run a shell command, return stdout. + * + * Log stderr to a log file. + */ +export async function runCommand( + t: GlobalTestState, + logName: string, + command: string, + args: string[], + env: { [index: string]: string | undefined } = process.env, +): Promise { + logger.info(`running command ${shellescape([command, ...args])}`); + return new Promise((resolve, reject) => { + const stdoutChunks: Buffer[] = []; + const proc = spawn(command, args, { + stdio: ["inherit", "pipe", "pipe"], + shell: false, + env: env, + }); + proc.stdout.on("data", (x) => { + if (x instanceof Buffer) { + stdoutChunks.push(x); + } else { + throw Error("unexpected data chunk type"); + } + }); + const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); + const stderrLog = fs.createWriteStream(stderrLogFileName, { + flags: "a", + }); + proc.stderr.pipe(stderrLog); + proc.on("exit", (code, signal) => { + logger.info(`child process exited (${code} / ${signal})`); + if (code != 0) { + reject(Error(`Unexpected exit code ${code} for '${command}'`)); + return; + } + const b = Buffer.concat(stdoutChunks).toString("utf-8"); + resolve(b); + }); + proc.on("error", () => { + reject(Error("Child process had error")); + }); + }); +} + +export class ProcessWrapper { + private waitPromise: Promise; + constructor(public proc: ChildProcess) { + this.waitPromise = new Promise((resolve, reject) => { + proc.on("exit", (code, signal) => { + resolve({ code, signal }); + }); + proc.on("error", (err) => { + reject(err); + }); + }); + } + + wait(): Promise { + return this.waitPromise; + } +} + +export class GlobalTestParams { + testDir: string; +} + +export class GlobalTestState { + testDir: string; + procs: ProcessWrapper[]; + servers: http.Server[]; + inShutdown: boolean = false; + constructor(params: GlobalTestParams) { + this.testDir = params.testDir; + this.procs = []; + this.servers = []; + } + + async assertThrowsTalerErrorAsync( + block: () => Promise, + ): Promise { + try { + await block(); + } catch (e) { + if (e instanceof TalerError) { + return e; + } + throw Error(`expected TalerError to be thrown, but got ${e}`); + } + throw Error( + `expected TalerError to be thrown, but block finished without throwing`, + ); + } + + async assertThrowsAsync(block: () => Promise): Promise { + try { + await block(); + } catch (e) { + return e; + } + throw Error( + `expected exception to be thrown, but block finished without throwing`, + ); + } + + assertAxiosError(e: any): asserts e is AxiosError { + if (!e.isAxiosError) { + throw Error("expected axios error"); + } + } + + assertTrue(b: boolean): asserts b { + if (!b) { + throw Error("test assertion failed"); + } + } + + assertDeepEqual(actual: any, expected: T): asserts actual is T { + deepStrictEqual(actual, expected); + } + + assertAmountEquals( + amtActual: string | AmountJson, + amtExpected: string | AmountJson, + ): void { + if (Amounts.cmp(amtActual, amtExpected) != 0) { + throw Error( + `test assertion failed: expected ${Amounts.stringify( + amtExpected, + )} but got ${Amounts.stringify(amtActual)}`, + ); + } + } + + assertAmountLeq(a: string | AmountJson, b: string | AmountJson): void { + if (Amounts.cmp(a, b) > 0) { + throw Error( + `test assertion failed: expected ${Amounts.stringify( + a, + )} to be less or equal (leq) than ${Amounts.stringify(b)}`, + ); + } + } + + shutdownSync(): void { + for (const s of this.servers) { + s.close(); + s.removeAllListeners(); + } + for (const p of this.procs) { + if (p.proc.exitCode == null) { + p.proc.kill("SIGTERM"); + } + } + } + + spawnService( + command: string, + args: string[], + logName: string, + env: { [index: string]: string | undefined } = process.env, + ): ProcessWrapper { + logger.info( + `spawning process (${logName}): ${shellescape([command, ...args])}`, + ); + const proc = spawn(command, args, { + stdio: ["inherit", "pipe", "pipe"], + env: env, + }); + logger.info(`spawned process (${logName}) with pid ${proc.pid}`); + proc.on("error", (err) => { + logger.warn(`could not start process (${command})`, err); + }); + proc.on("exit", (code, signal) => { + logger.warn(`process ${logName} exited ${j2s({ code, signal })}`); + }); + const stderrLogFileName = this.testDir + `/${logName}-stderr.log`; + const stderrLog = fs.createWriteStream(stderrLogFileName, { + flags: "a", + }); + proc.stderr.pipe(stderrLog); + const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`; + const stdoutLog = fs.createWriteStream(stdoutLogFileName, { + flags: "a", + }); + proc.stdout.pipe(stdoutLog); + const procWrap = new ProcessWrapper(proc); + this.procs.push(procWrap); + return procWrap; + } + + async shutdown(): Promise { + if (this.inShutdown) { + return; + } + if (shouldLingerInTest()) { + logger.info("refusing to shut down, lingering was requested"); + return; + } + this.inShutdown = true; + logger.info("shutting down"); + for (const s of this.servers) { + s.close(); + s.removeAllListeners(); + } + for (const p of this.procs) { + if (p.proc.exitCode == null) { + logger.info(`killing process ${p.proc.pid}`); + p.proc.kill("SIGTERM"); + await p.wait(); + } + } + } +} + +export function shouldLingerInTest(): boolean { + return !!process.env["TALER_TEST_LINGER"]; +} + +export interface TalerConfigSection { + options: Record; +} + +export interface TalerConfig { + sections: Record; +} + +export interface DbInfo { + /** + * Postgres connection string. + */ + connStr: string; + + dbname: string; +} + +export async function setupDb(gc: GlobalTestState): Promise { + const dbname = "taler-integrationtest"; + await exec(`dropdb "${dbname}" || true`); + await exec(`createdb "${dbname}"`); + return { + connStr: `postgres:///${dbname}`, + dbname, + }; +} + +export interface BankConfig { + currency: string; + httpPort: number; + database: string; + allowRegistrations: boolean; + maxDebt?: string; +} + +export interface FakeBankConfig { + currency: string; + httpPort: number; +} + +function setTalerPaths(config: Configuration, home: string) { + config.setString("paths", "taler_home", home); + // We need to make sure that the path of taler_runtime_dir isn't too long, + // as it contains unix domain sockets (108 character limit). + const runDir = fs.mkdtempSync("/tmp/taler-test-"); + config.setString("paths", "taler_runtime_dir", runDir); + config.setString( + "paths", + "taler_data_home", + "$TALER_HOME/.local/share/taler/", + ); + config.setString("paths", "taler_config_home", "$TALER_HOME/.config/taler/"); + config.setString("paths", "taler_cache_home", "$TALER_HOME/.config/taler/"); +} + +function setCoin(config: Configuration, c: CoinConfig) { + const s = `coin_${c.name}`; + config.setString(s, "value", c.value); + config.setString(s, "duration_withdraw", c.durationWithdraw); + config.setString(s, "duration_spend", c.durationSpend); + config.setString(s, "duration_legal", c.durationLegal); + config.setString(s, "fee_deposit", c.feeDeposit); + config.setString(s, "fee_withdraw", c.feeWithdraw); + config.setString(s, "fee_refresh", c.feeRefresh); + config.setString(s, "fee_refund", c.feeRefund); + if (c.ageRestricted) { + config.setString(s, "age_restricted", "yes"); + } + if (c.cipher === "RSA") { + config.setString(s, "rsa_keysize", `${c.rsaKeySize}`); + config.setString(s, "cipher", "RSA"); + } else if (c.cipher === "CS") { + config.setString(s, "cipher", "CS"); + } else { + throw new Error(); + } +} + +/** + * Send an HTTP request until it succeeds or the process dies. + */ +export async function pingProc( + proc: ProcessWrapper | undefined, + url: string, + serviceName: string, +): Promise { + if (!proc || proc.proc.exitCode !== null) { + throw Error(`service process ${serviceName} not started, can't ping`); + } + while (true) { + try { + logger.info(`pinging ${serviceName} at ${url}`); + const resp = await axios.get(url); + logger.info(`service ${serviceName} available`); + return; + } catch (e: any) { + logger.info(`service ${serviceName} not ready:`, e.toString()); + //console.log(e); + await delayMs(1000); + } + if (!proc || proc.proc.exitCode != null || proc.proc.signalCode != null) { + throw Error(`service process ${serviceName} stopped unexpectedly`); + } + } +} + +class BankServiceBase { + proc: ProcessWrapper | undefined; + + protected constructor( + protected globalTestState: GlobalTestState, + protected bankConfig: BankConfig, + protected configFile: string, + ) {} +} + +/** + * Work in progress. The key point is that both Sandbox and Nexus + * will be configured and started by this class. + */ +class LibEuFinBankService extends BankServiceBase implements BankServiceHandle { + sandboxProc: ProcessWrapper | undefined; + nexusProc: ProcessWrapper | undefined; + + http = new NodeHttpLib(); + + static async create( + gc: GlobalTestState, + bc: BankConfig, + ): Promise { + return new LibEuFinBankService(gc, bc, "foo"); + } + + get port() { + return this.bankConfig.httpPort; + } + get nexusPort() { + return this.bankConfig.httpPort + 1000; + } + + get nexusDbConn(): string { + return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`; + } + + get sandboxDbConn(): string { + return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`; + } + + get nexusBaseUrl(): string { + return `http://localhost:${this.nexusPort}`; + } + + get baseUrlDemobank(): string { + let url = new URL("demobanks/default/", this.baseUrlNetloc); + return url.href; + } + + // FIXME: Duplicate? Where is this needed? + get baseUrlAccessApi(): string { + let url = new URL("access-api/", this.baseUrlDemobank); + return url.href; + } + + get bankAccessApiBaseUrl(): string { + let url = new URL("access-api/", this.baseUrlDemobank); + return url.href; + } + + get baseUrlNetloc(): string { + return `http://localhost:${this.bankConfig.httpPort}/`; + } + + get baseUrl(): string { + return this.baseUrlAccessApi; + } + + async setSuggestedExchange( + e: ExchangeServiceInterface, + exchangePayto: string, + ) { + await sh( + this.globalTestState, + "libeufin-sandbox-set-default-exchange", + `libeufin-sandbox default-exchange ${e.baseUrl} ${exchangePayto}`, + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn, + }, + ); + } + + // Create one at both sides: Sandbox and Nexus. + async createExchangeAccount( + accountName: string, + password: string, + ): Promise { + logger.info("Create Exchange account(s)!"); + /** + * Many test cases try to create a Exchange account before + * starting the bank; that's because the Pybank did it entirely + * via the configuration file. + */ + await this.start(); + await this.pingUntilAvailable(); + await LibeufinSandboxApi.createDemobankAccount(accountName, password, { + baseUrl: this.baseUrlAccessApi, + }); + let bankAccountLabel = accountName; + await LibeufinSandboxApi.createDemobankEbicsSubscriber( + { + hostID: "talertestEbicsHost", + userID: "exchangeEbicsUser", + partnerID: "exchangeEbicsPartner", + }, + bankAccountLabel, + { baseUrl: this.baseUrlDemobank }, + ); + + await LibeufinNexusApi.createUser( + { baseUrl: this.nexusBaseUrl }, + { + username: accountName, + password: password, + }, + ); + await LibeufinNexusApi.createEbicsBankConnection( + { baseUrl: this.nexusBaseUrl }, + { + name: "ebics-connection", // connection name. + ebicsURL: new URL("ebicsweb", this.baseUrlNetloc).href, + hostID: "talertestEbicsHost", + userID: "exchangeEbicsUser", + partnerID: "exchangeEbicsPartner", + }, + ); + await LibeufinNexusApi.connectBankConnection( + { baseUrl: this.nexusBaseUrl }, + "ebics-connection", + ); + await LibeufinNexusApi.fetchAccounts( + { baseUrl: this.nexusBaseUrl }, + "ebics-connection", + ); + await LibeufinNexusApi.importConnectionAccount( + { baseUrl: this.nexusBaseUrl }, + "ebics-connection", // connection name + accountName, // offered account label + `${accountName}-nexus-label`, // bank account label at Nexus + ); + await LibeufinNexusApi.createTwgFacade( + { baseUrl: this.nexusBaseUrl }, + { + name: "exchange-facade", + connectionName: "ebics-connection", + accountName: `${accountName}-nexus-label`, + currency: "EUR", + reserveTransferLevel: "report", + }, + ); + await LibeufinNexusApi.postPermission( + { baseUrl: this.nexusBaseUrl }, + { + action: "grant", + permission: { + subjectId: accountName, + subjectType: "user", + resourceType: "facade", + resourceId: "exchange-facade", // facade name + permissionName: "facade.talerWireGateway.transfer", + }, + }, + ); + await LibeufinNexusApi.postPermission( + { baseUrl: this.nexusBaseUrl }, + { + action: "grant", + permission: { + subjectId: accountName, + subjectType: "user", + resourceType: "facade", + resourceId: "exchange-facade", // facade name + permissionName: "facade.talerWireGateway.history", + }, + }, + ); + // Set fetch task. + await LibeufinNexusApi.postTask( + { baseUrl: this.nexusBaseUrl }, + `${accountName}-nexus-label`, + { + name: "wirewatch-task", + cronspec: "* * *", + type: "fetch", + params: { + level: "all", + rangeType: "all", + }, + }, + ); + await LibeufinNexusApi.postTask( + { baseUrl: this.nexusBaseUrl }, + `${accountName}-nexus-label`, + { + name: "aggregator-task", + cronspec: "* * *", + type: "submit", + params: {}, + }, + ); + let facadesResp = await LibeufinNexusApi.getAllFacades({ + baseUrl: this.nexusBaseUrl, + }); + let accountInfoResp = await LibeufinSandboxApi.demobankAccountInfo( + "admin", + "secret", + { baseUrl: this.baseUrlAccessApi }, + accountName, // bank account label. + ); + return { + accountName: accountName, + accountPassword: password, + accountPaytoUri: accountInfoResp.data.paytoUri, + wireGatewayApiBaseUrl: facadesResp.data.facades[0].baseUrl, + }; + } + + async start(): Promise { + /** + * Because many test cases try to create a Exchange bank + * account _before_ starting the bank (Pybank did it only via + * the config), it is possible that at this point Sandbox and + * Nexus are already running. Hence, this method only launches + * them if they weren't launched earlier. + */ + + // Only go ahead if BOTH aren't running. + if (this.sandboxProc || this.nexusProc) { + logger.info("Nexus or Sandbox already running, not taking any action."); + return; + } + await sh( + this.globalTestState, + "libeufin-sandbox-config-demobank", + `libeufin-sandbox config --currency=${this.bankConfig.currency} default`, + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn, + LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", + }, + ); + this.sandboxProc = this.globalTestState.spawnService( + "libeufin-sandbox", + ["serve", "--port", `${this.port}`], + "libeufin-sandbox", + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn, + LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", + }, + ); + await runCommand( + this.globalTestState, + "libeufin-nexus-superuser", + "libeufin-nexus", + ["superuser", "admin", "--password", "test"], + { + ...process.env, + LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn, + }, + ); + this.nexusProc = this.globalTestState.spawnService( + "libeufin-nexus", + ["serve", "--port", `${this.nexusPort}`], + "libeufin-nexus", + { + ...process.env, + LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn, + }, + ); + // need to wait here, because at this point + // a Ebics host needs to be created (RESTfully) + await this.pingUntilAvailable(); + LibeufinSandboxApi.createEbicsHost( + { baseUrl: this.baseUrlNetloc }, + "talertestEbicsHost", + ); + } + + async pingUntilAvailable(): Promise { + await pingProc( + this.sandboxProc, + `http://localhost:${this.bankConfig.httpPort}`, + "libeufin-sandbox", + ); + await pingProc( + this.nexusProc, + `${this.nexusBaseUrl}/config`, + "libeufin-nexus", + ); + } +} + +/** + * Implementation of the bank service using the "taler-fakebank-run" tool. + */ +export class FakebankService + extends BankServiceBase + implements BankServiceHandle +{ + proc: ProcessWrapper | undefined; + + http = new NodeHttpLib(); + + // We store "created" accounts during setup and + // register them after startup. + private accounts: { + accountName: string; + accountPassword: string; + }[] = []; + + static async create( + gc: GlobalTestState, + bc: BankConfig, + ): Promise { + const config = new Configuration(); + setTalerPaths(config, gc.testDir + "/talerhome"); + config.setString("taler", "currency", bc.currency); + config.setString("bank", "http_port", `${bc.httpPort}`); + config.setString("bank", "serve", "http"); + config.setString("bank", "max_debt_bank", `${bc.currency}:999999`); + config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`); + config.setString("bank", "ram_limit", `${1024}`); + const cfgFilename = gc.testDir + "/bank.conf"; + config.write(cfgFilename); + + return new FakebankService(gc, bc, cfgFilename); + } + + setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) { + if (!!this.proc) { + throw Error("Can't set suggested exchange while bank is running."); + } + const config = Configuration.load(this.configFile); + config.setString("bank", "suggested_exchange", e.baseUrl); + config.write(this.configFile); + } + + get baseUrl(): string { + return `http://localhost:${this.bankConfig.httpPort}/`; + } + + get bankAccessApiBaseUrl(): string { + let url = new URL("taler-bank-access/", this.baseUrl); + return url.href; + } + + async createExchangeAccount( + accountName: string, + password: string, + ): Promise { + this.accounts.push({ + accountName, + accountPassword: password, + }); + return { + accountName: accountName, + accountPassword: password, + accountPaytoUri: getPayto(accountName), + wireGatewayApiBaseUrl: `http://localhost:${this.bankConfig.httpPort}/taler-wire-gateway/${accountName}/`, + }; + } + + get port() { + return this.bankConfig.httpPort; + } + + async start(): Promise { + logger.info("starting fakebank"); + if (this.proc) { + logger.info("fakebank already running, not starting again"); + return; + } + this.proc = this.globalTestState.spawnService( + "taler-fakebank-run", + [ + "-c", + this.configFile, + "--signup-bonus", + `${this.bankConfig.currency}:100`, + ], + "bank", + ); + await this.pingUntilAvailable(); + for (const acc of this.accounts) { + await BankApi.registerAccount(this, acc.accountName, acc.accountPassword); + } + } + + async pingUntilAvailable(): Promise { + const url = `http://localhost:${this.bankConfig.httpPort}/taler-bank-integration/config`; + await pingProc(this.proc, url, "bank"); + } +} + +// Use libeufin bank instead of pybank. +const useLibeufinBank = false; + +export type BankService = BankServiceHandle; +export const BankService = FakebankService; + +export interface ExchangeConfig { + name: string; + currency: string; + roundUnit?: string; + httpPort: number; + database: string; +} + +export interface ExchangeServiceInterface { + readonly baseUrl: string; + readonly port: number; + readonly name: string; + readonly masterPub: string; +} + +export class ExchangeService implements ExchangeServiceInterface { + static fromExistingConfig(gc: GlobalTestState, exchangeName: string) { + const cfgFilename = gc.testDir + `/exchange-${exchangeName}.conf`; + const config = Configuration.load(cfgFilename); + const ec: ExchangeConfig = { + currency: config.getString("taler", "currency").required(), + database: config.getString("exchangedb-postgres", "config").required(), + httpPort: config.getNumber("exchange", "port").required(), + name: exchangeName, + roundUnit: config.getString("taler", "currency_round_unit").required(), + }; + const privFile = config.getPath("exchange", "master_priv_file").required(); + const eddsaPriv = fs.readFileSync(privFile); + const keyPair: EddsaKeyPair = { + eddsaPriv, + eddsaPub: eddsaGetPublic(eddsaPriv), + }; + return new ExchangeService(gc, ec, cfgFilename, keyPair); + } + + private currentTimetravel: Duration | undefined; + + setTimetravel(t: Duration | undefined): void { + if (this.isRunning()) { + throw Error("can't set time travel while the exchange is running"); + } + this.currentTimetravel = t; + } + + private get timetravelArg(): string | undefined { + if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { + // Convert to microseconds + return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; + } + return undefined; + } + + /** + * Return an empty array if no time travel is set, + * and an array with the time travel command line argument + * otherwise. + */ + private get timetravelArgArr(): string[] { + const tta = this.timetravelArg; + if (tta) { + return [tta]; + } + return []; + } + + async runWirewatchOnce() { + if (useLibeufinBank) { + // Not even 2 secods showed to be enough! + await waitMs(4000); + } + await runCommand( + this.globalState, + `exchange-${this.name}-wirewatch-once`, + "taler-exchange-wirewatch", + [...this.timetravelArgArr, "-c", this.configFilename, "-t"], + ); + } + + async runAggregatorOnce() { + try { + await runCommand( + this.globalState, + `exchange-${this.name}-aggregator-once`, + "taler-exchange-aggregator", + [...this.timetravelArgArr, "-c", this.configFilename, "-t", "-y"], + ); + } catch (e) { + logger.info( + "running aggregator with KYC off didn't work, might be old version, running again", + ); + await runCommand( + this.globalState, + `exchange-${this.name}-aggregator-once`, + "taler-exchange-aggregator", + [...this.timetravelArgArr, "-c", this.configFilename, "-t"], + ); + } + } + + async runTransferOnce() { + await runCommand( + this.globalState, + `exchange-${this.name}-transfer-once`, + "taler-exchange-transfer", + [...this.timetravelArgArr, "-c", this.configFilename, "-t"], + ); + } + + changeConfig(f: (config: Configuration) => void) { + const config = Configuration.load(this.configFilename); + f(config); + config.write(this.configFilename); + } + + static create(gc: GlobalTestState, e: ExchangeConfig) { + const config = new Configuration(); + config.setString("taler", "currency", e.currency); + config.setString( + "taler", + "currency_round_unit", + e.roundUnit ?? `${e.currency}:0.01`, + ); + setTalerPaths(config, gc.testDir + "/talerhome"); + config.setString( + "exchange", + "revocation_dir", + "${TALER_DATA_HOME}/exchange/revocations", + ); + config.setString("exchange", "max_keys_caching", "forever"); + config.setString("exchange", "db", "postgres"); + config.setString( + "exchange-offline", + "master_priv_file", + "${TALER_DATA_HOME}/exchange/offline-keys/master.priv", + ); + config.setString("exchange", "serve", "tcp"); + config.setString("exchange", "port", `${e.httpPort}`); + + config.setString("exchangedb-postgres", "config", e.database); + + config.setString("taler-exchange-secmod-eddsa", "lookahead_sign", "20 s"); + config.setString("taler-exchange-secmod-rsa", "lookahead_sign", "20 s"); + + const exchangeMasterKey = createEddsaKeyPair(); + + config.setString( + "exchange", + "master_public_key", + encodeCrock(exchangeMasterKey.eddsaPub), + ); + + const masterPrivFile = config + .getPath("exchange-offline", "master_priv_file") + .required(); + + fs.mkdirSync(path.dirname(masterPrivFile), { recursive: true }); + + fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv)); + + const cfgFilename = gc.testDir + `/exchange-${e.name}.conf`; + config.write(cfgFilename); + return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey); + } + + addOfferedCoins(offeredCoins: ((curr: string) => CoinConfig)[]) { + const config = Configuration.load(this.configFilename); + offeredCoins.forEach((cc) => + setCoin(config, cc(this.exchangeConfig.currency)), + ); + config.write(this.configFilename); + } + + addCoinConfigList(ccs: CoinConfig[]) { + const config = Configuration.load(this.configFilename); + ccs.forEach((cc) => setCoin(config, cc)); + config.write(this.configFilename); + } + + enableAgeRestrictions(maskStr: string) { + const config = Configuration.load(this.configFilename); + config.setString("exchange-extension-age_restriction", "enabled", "yes"); + config.setString( + "exchange-extension-age_restriction", + "age_groups", + maskStr, + ); + config.write(this.configFilename); + } + + get masterPub() { + return encodeCrock(this.keyPair.eddsaPub); + } + + get port() { + return this.exchangeConfig.httpPort; + } + + async addBankAccount( + localName: string, + exchangeBankAccount: HarnessExchangeBankAccount, + ): Promise { + const config = Configuration.load(this.configFilename); + config.setString( + `exchange-account-${localName}`, + "wire_response", + `\${TALER_DATA_HOME}/exchange/account-${localName}.json`, + ); + config.setString( + `exchange-account-${localName}`, + "payto_uri", + exchangeBankAccount.accountPaytoUri, + ); + config.setString(`exchange-account-${localName}`, "enable_credit", "yes"); + config.setString(`exchange-account-${localName}`, "enable_debit", "yes"); + config.setString( + `exchange-accountcredentials-${localName}`, + "wire_gateway_url", + exchangeBankAccount.wireGatewayApiBaseUrl, + ); + config.setString( + `exchange-accountcredentials-${localName}`, + "wire_gateway_auth_method", + "basic", + ); + config.setString( + `exchange-accountcredentials-${localName}`, + "username", + exchangeBankAccount.accountName, + ); + config.setString( + `exchange-accountcredentials-${localName}`, + "password", + exchangeBankAccount.accountPassword, + ); + config.write(this.configFilename); + } + + exchangeHttpProc: ProcessWrapper | undefined; + exchangeWirewatchProc: ProcessWrapper | undefined; + + helperCryptoRsaProc: ProcessWrapper | undefined; + helperCryptoEddsaProc: ProcessWrapper | undefined; + helperCryptoCsProc: ProcessWrapper | undefined; + + constructor( + private globalState: GlobalTestState, + private exchangeConfig: ExchangeConfig, + private configFilename: string, + private keyPair: EddsaKeyPair, + ) {} + + get name() { + return this.exchangeConfig.name; + } + + get baseUrl() { + return `http://localhost:${this.exchangeConfig.httpPort}/`; + } + + isRunning(): boolean { + return !!this.exchangeWirewatchProc || !!this.exchangeHttpProc; + } + + async stop(): Promise { + const wirewatch = this.exchangeWirewatchProc; + if (wirewatch) { + wirewatch.proc.kill("SIGTERM"); + await wirewatch.wait(); + this.exchangeWirewatchProc = undefined; + } + const httpd = this.exchangeHttpProc; + if (httpd) { + httpd.proc.kill("SIGTERM"); + await httpd.wait(); + this.exchangeHttpProc = undefined; + } + const cryptoRsa = this.helperCryptoRsaProc; + if (cryptoRsa) { + cryptoRsa.proc.kill("SIGTERM"); + await cryptoRsa.wait(); + this.helperCryptoRsaProc = undefined; + } + const cryptoEddsa = this.helperCryptoEddsaProc; + if (cryptoEddsa) { + cryptoEddsa.proc.kill("SIGTERM"); + await cryptoEddsa.wait(); + this.helperCryptoRsaProc = undefined; + } + const cryptoCs = this.helperCryptoCsProc; + if (cryptoCs) { + cryptoCs.proc.kill("SIGTERM"); + await cryptoCs.wait(); + this.helperCryptoCsProc = undefined; + } + } + + /** + * Update keys signing the keys generated by the security module + * with the offline signing key. + */ + async keyup(): Promise { + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + ["-c", this.configFilename, "download", "sign", "upload"], + ); + + const accounts: string[] = []; + const accountTargetTypes: Set = new Set(); + + const config = Configuration.load(this.configFilename); + for (const sectionName of config.getSectionNames()) { + if (sectionName.startsWith("EXCHANGE-ACCOUNT-")) { + const paytoUri = config.getString(sectionName, "payto_uri").required(); + const p = parsePaytoUri(paytoUri); + if (!p) { + throw Error(`invalid payto uri in exchange config: ${paytoUri}`); + } + accountTargetTypes.add(p?.targetType); + accounts.push(paytoUri); + } + } + + logger.info("configuring bank accounts", accounts); + + for (const acc of accounts) { + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + ["-c", this.configFilename, "enable-account", acc, "upload"], + ); + } + + const year = new Date().getFullYear(); + for (const accTargetType of accountTargetTypes.values()) { + for (let i = year; i < year + 5; i++) { + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + [ + "-c", + this.configFilename, + "wire-fee", + // Year + `${i}`, + // Wire method + accTargetType, + // Wire fee + `${this.exchangeConfig.currency}:0.01`, + // Closing fee + `${this.exchangeConfig.currency}:0.01`, + "upload", + ], + ); + } + } + + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + [ + "-c", + this.configFilename, + "global-fee", + // year + "now", + // history fee + `${this.exchangeConfig.currency}:0.01`, + // account fee + `${this.exchangeConfig.currency}:0.01`, + // purse fee + `${this.exchangeConfig.currency}:0.00`, + // purse timeout + "1h", + // history expiration + "1year", + // free purses per account + "5", + "upload", + ], + ); + } + + async revokeDenomination(denomPubHash: string) { + if (!this.isRunning()) { + throw Error("exchange must be running when revoking denominations"); + } + await runCommand( + this.globalState, + "exchange-offline", + "taler-exchange-offline", + [ + "-c", + this.configFilename, + "revoke-denomination", + denomPubHash, + "upload", + ], + ); + } + + async purgeSecmodKeys(): Promise { + const cfg = Configuration.load(this.configFilename); + const rsaKeydir = cfg + .getPath("taler-exchange-secmod-rsa", "KEY_DIR") + .required(); + const eddsaKeydir = cfg + .getPath("taler-exchange-secmod-eddsa", "KEY_DIR") + .required(); + // Be *VERY* careful when changing this, or you will accidentally delete user data. + await sh(this.globalState, "rm-secmod-keys", `rm -rf ${rsaKeydir}/COIN_*`); + await sh(this.globalState, "rm-secmod-keys", `rm ${eddsaKeydir}/*`); + } + + async purgeDatabase(): Promise { + await sh( + this.globalState, + "exchange-dbinit", + `taler-exchange-dbinit -r -c "${this.configFilename}"`, + ); + } + + async start(): Promise { + if (this.isRunning()) { + throw Error("exchange is already running"); + } + await sh( + this.globalState, + "exchange-dbinit", + `taler-exchange-dbinit -c "${this.configFilename}"`, + ); + + this.helperCryptoEddsaProc = this.globalState.spawnService( + "taler-exchange-secmod-eddsa", + ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], + `exchange-crypto-eddsa-${this.name}`, + ); + + this.helperCryptoCsProc = this.globalState.spawnService( + "taler-exchange-secmod-cs", + ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], + `exchange-crypto-cs-${this.name}`, + ); + + this.helperCryptoRsaProc = this.globalState.spawnService( + "taler-exchange-secmod-rsa", + ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], + `exchange-crypto-rsa-${this.name}`, + ); + + this.exchangeWirewatchProc = this.globalState.spawnService( + "taler-exchange-wirewatch", + ["-c", this.configFilename, ...this.timetravelArgArr], + `exchange-wirewatch-${this.name}`, + ); + + this.exchangeHttpProc = this.globalState.spawnService( + "taler-exchange-httpd", + ["-LINFO", "-c", this.configFilename, ...this.timetravelArgArr], + `exchange-httpd-${this.name}`, + ); + + await this.pingUntilAvailable(); + await this.keyup(); + } + + async pingUntilAvailable(): Promise { + // We request /management/keys, since /keys can block + // when we didn't do the key setup yet. + const url = `http://localhost:${this.exchangeConfig.httpPort}/management/keys`; + await pingProc(this.exchangeHttpProc, url, `exchange (${this.name})`); + } +} + +export interface MerchantConfig { + name: string; + currency: string; + httpPort: number; + database: string; +} + +export interface PrivateOrderStatusQuery { + instance?: string; + orderId: string; + sessionId?: string; +} + +export interface MerchantServiceInterface { + makeInstanceBaseUrl(instanceName?: string): string; + readonly port: number; + readonly name: string; +} + +export class MerchantApiClient { + constructor( + private baseUrl: string, + public readonly auth: MerchantAuthConfiguration, + ) {} + + async changeAuth(auth: MerchantAuthConfiguration): Promise { + const url = new URL("private/auth", this.baseUrl); + await axios.post(url.href, auth, { + headers: this.makeAuthHeader(), + }); + } + + async deleteInstance(instanceId: string) { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + await axios.delete(url.href, { + headers: this.makeAuthHeader(), + }); + } + + async createInstance(req: MerchantInstanceConfig): Promise { + const url = new URL("management/instances", this.baseUrl); + await axios.post(url.href, req, { + headers: this.makeAuthHeader(), + }); + } + + async getInstances(): Promise { + const url = new URL("management/instances", this.baseUrl); + const resp = await axios.get(url.href, { + headers: this.makeAuthHeader(), + }); + return resp.data; + } + + async getInstanceFullDetails(instanceId: string): Promise { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + try { + const resp = await axios.get(url.href, { + headers: this.makeAuthHeader(), + }); + return resp.data; + } catch (e) { + throw e; + } + } + + makeAuthHeader(): Record { + switch (this.auth.method) { + case "external": + return {}; + case "token": + return { + Authorization: `Bearer ${this.auth.token}`, + }; + } + } +} + +/** + * FIXME: This should be deprecated in favor of MerchantApiClient + */ +export namespace MerchantPrivateApi { + export async function createOrder( + merchantService: MerchantServiceInterface, + instanceName: string, + req: PostOrderRequest, + withAuthorization: WithAuthorization = {}, + ): Promise { + const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); + let url = new URL("private/orders", baseUrl); + const resp = await axios.post(url.href, req, { + headers: withAuthorization as Record, + }); + return codecForPostOrderResponse().decode(resp.data); + } + + export async function queryPrivateOrderStatus( + merchantService: MerchantServiceInterface, + query: PrivateOrderStatusQuery, + withAuthorization: WithAuthorization = {}, + ): Promise { + const reqUrl = new URL( + `private/orders/${query.orderId}`, + merchantService.makeInstanceBaseUrl(query.instance), + ); + if (query.sessionId) { + reqUrl.searchParams.set("session_id", query.sessionId); + } + const resp = await axios.get(reqUrl.href, { + headers: withAuthorization as Record, + }); + return codecForMerchantOrderPrivateStatusResponse().decode(resp.data); + } + + export async function giveRefund( + merchantService: MerchantServiceInterface, + r: { + instance: string; + orderId: string; + amount: string; + justification: string; + }, + ): Promise<{ talerRefundUri: string }> { + const reqUrl = new URL( + `private/orders/${r.orderId}/refund`, + merchantService.makeInstanceBaseUrl(r.instance), + ); + const resp = await axios.post(reqUrl.href, { + refund: r.amount, + reason: r.justification, + }); + return { + talerRefundUri: resp.data.taler_refund_uri, + }; + } + + export async function createTippingReserve( + merchantService: MerchantServiceInterface, + instance: string, + req: CreateMerchantTippingReserveRequest, + ): Promise { + const reqUrl = new URL( + `private/reserves`, + merchantService.makeInstanceBaseUrl(instance), + ); + const resp = await axios.post(reqUrl.href, req); + // FIXME: validate + return resp.data; + } + + export async function queryTippingReserves( + merchantService: MerchantServiceInterface, + instance: string, + ): Promise { + const reqUrl = new URL( + `private/reserves`, + merchantService.makeInstanceBaseUrl(instance), + ); + const resp = await axios.get(reqUrl.href); + // FIXME: validate + return resp.data; + } + + export async function giveTip( + merchantService: MerchantServiceInterface, + instance: string, + req: TipCreateRequest, + ): Promise { + const reqUrl = new URL( + `private/tips`, + merchantService.makeInstanceBaseUrl(instance), + ); + const resp = await axios.post(reqUrl.href, req); + // FIXME: validate + return resp.data; + } +} + +export interface CreateMerchantTippingReserveRequest { + // Amount that the merchant promises to put into the reserve + initial_balance: AmountString; + + // Exchange the merchant intends to use for tipping + exchange_url: string; + + // Desired wire method, for example "iban" or "x-taler-bank" + wire_method: string; +} + +export interface CreateMerchantTippingReserveConfirmation { + // Public key identifying the reserve + reserve_pub: string; + + // Wire account of the exchange where to transfer the funds + payto_uri: string; +} + +export class MerchantService implements MerchantServiceInterface { + static fromExistingConfig(gc: GlobalTestState, name: string) { + const cfgFilename = gc.testDir + `/merchant-${name}.conf`; + const config = Configuration.load(cfgFilename); + const mc: MerchantConfig = { + currency: config.getString("taler", "currency").required(), + database: config.getString("merchantdb-postgres", "config").required(), + httpPort: config.getNumber("merchant", "port").required(), + name, + }; + return new MerchantService(gc, mc, cfgFilename); + } + + proc: ProcessWrapper | undefined; + + constructor( + private globalState: GlobalTestState, + private merchantConfig: MerchantConfig, + private configFilename: string, + ) {} + + private currentTimetravel: Duration | undefined; + + private isRunning(): boolean { + return !!this.proc; + } + + setTimetravel(t: Duration | undefined): void { + if (this.isRunning()) { + throw Error("can't set time travel while the exchange is running"); + } + this.currentTimetravel = t; + } + + private get timetravelArg(): string | undefined { + if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { + // Convert to microseconds + return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; + } + return undefined; + } + + /** + * Return an empty array if no time travel is set, + * and an array with the time travel command line argument + * otherwise. + */ + private get timetravelArgArr(): string[] { + const tta = this.timetravelArg; + if (tta) { + return [tta]; + } + return []; + } + + get port(): number { + return this.merchantConfig.httpPort; + } + + get name(): string { + return this.merchantConfig.name; + } + + async stop(): Promise { + const httpd = this.proc; + if (httpd) { + httpd.proc.kill("SIGTERM"); + await httpd.wait(); + this.proc = undefined; + } + } + + async start(): Promise { + await exec(`taler-merchant-dbinit -c "${this.configFilename}"`); + + this.proc = this.globalState.spawnService( + "taler-merchant-httpd", + [ + "taler-merchant-httpd", + "-LDEBUG", + "-c", + this.configFilename, + ...this.timetravelArgArr, + ], + `merchant-${this.merchantConfig.name}`, + ); + } + + static async create( + gc: GlobalTestState, + mc: MerchantConfig, + ): Promise { + const config = new Configuration(); + config.setString("taler", "currency", mc.currency); + + const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`; + setTalerPaths(config, gc.testDir + "/talerhome"); + config.setString("merchant", "serve", "tcp"); + config.setString("merchant", "port", `${mc.httpPort}`); + config.setString( + "merchant", + "keyfile", + "${TALER_DATA_HOME}/merchant/merchant.priv", + ); + config.setString("merchantdb-postgres", "config", mc.database); + config.write(cfgFilename); + + return new MerchantService(gc, mc, cfgFilename); + } + + addExchange(e: ExchangeServiceInterface): void { + const config = Configuration.load(this.configFilename); + config.setString( + `merchant-exchange-${e.name}`, + "exchange_base_url", + e.baseUrl, + ); + config.setString( + `merchant-exchange-${e.name}`, + "currency", + this.merchantConfig.currency, + ); + config.setString(`merchant-exchange-${e.name}`, "master_key", e.masterPub); + config.write(this.configFilename); + } + + async addDefaultInstance(): Promise { + return await this.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [getPayto("merchant-default")], + auth: { + method: "external", + }, + }); + } + + async addInstance( + instanceConfig: PartialMerchantInstanceConfig, + ): Promise { + if (!this.proc) { + throw Error("merchant must be running to add instance"); + } + logger.info("adding instance"); + const url = `http://localhost:${this.merchantConfig.httpPort}/management/instances`; + const auth = instanceConfig.auth ?? { method: "external" }; + + const body: MerchantInstanceConfig = { + auth, + payto_uris: instanceConfig.paytoUris, + id: instanceConfig.id, + name: instanceConfig.name, + address: instanceConfig.address ?? {}, + jurisdiction: instanceConfig.jurisdiction ?? {}, + default_max_wire_fee: + instanceConfig.defaultMaxWireFee ?? + `${this.merchantConfig.currency}:1.0`, + default_wire_fee_amortization: + instanceConfig.defaultWireFeeAmortization ?? 3, + default_max_deposit_fee: + instanceConfig.defaultMaxDepositFee ?? + `${this.merchantConfig.currency}:1.0`, + default_wire_transfer_delay: + instanceConfig.defaultWireTransferDelay ?? + Duration.toTalerProtocolDuration( + Duration.fromSpec({ + days: 1, + }), + ), + default_pay_delay: + instanceConfig.defaultPayDelay ?? + Duration.toTalerProtocolDuration(Duration.getForever()), + }; + await axios.post(url, body); + } + + makeInstanceBaseUrl(instanceName?: string): string { + if (instanceName === undefined || instanceName === "default") { + return `http://localhost:${this.merchantConfig.httpPort}/`; + } else { + return `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/`; + } + } + + async pingUntilAvailable(): Promise { + const url = `http://localhost:${this.merchantConfig.httpPort}/config`; + await pingProc(this.proc, url, `merchant (${this.merchantConfig.name})`); + } +} + +export interface MerchantAuthConfiguration { + method: "external" | "token"; + token?: string; +} + +export interface PartialMerchantInstanceConfig { + auth?: MerchantAuthConfiguration; + id: string; + name: string; + paytoUris: string[]; + address?: unknown; + jurisdiction?: unknown; + defaultMaxWireFee?: string; + defaultMaxDepositFee?: string; + defaultWireFeeAmortization?: number; + defaultWireTransferDelay?: TalerProtocolDuration; + defaultPayDelay?: TalerProtocolDuration; +} + +export interface MerchantInstanceConfig { + auth: MerchantAuthConfiguration; + id: string; + name: string; + payto_uris: string[]; + address: unknown; + jurisdiction: unknown; + default_max_wire_fee: string; + default_max_deposit_fee: string; + default_wire_fee_amortization: number; + default_wire_transfer_delay: TalerProtocolDuration; + default_pay_delay: TalerProtocolDuration; +} + +type TestStatus = "pass" | "fail" | "skip"; + +export interface TestRunResult { + /** + * Name of the test. + */ + name: string; + + /** + * How long did the test run? + */ + timeSec: number; + + status: TestStatus; + + reason?: string; +} + +export async function runTestWithState( + gc: GlobalTestState, + testMain: (t: GlobalTestState) => Promise, + testName: string, + linger: boolean = false, +): Promise { + const startMs = new Date().getTime(); + + const p = openPromise(); + let status: TestStatus; + + const handleSignal = (s: string) => { + logger.warn( + `**** received fatal process event, terminating test ${testName}`, + ); + gc.shutdownSync(); + process.exit(1); + }; + + process.on("SIGINT", handleSignal); + process.on("SIGTERM", handleSignal); + process.on("unhandledRejection", handleSignal); + process.on("uncaughtException", handleSignal); + + try { + logger.info("running test in directory", gc.testDir); + await Promise.race([testMain(gc), p.promise]); + status = "pass"; + if (linger) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: true, + }); + await new Promise((resolve, reject) => { + rl.question("Press enter to shut down test.", () => { + logger.error("Requested shutdown"); + resolve(); + }); + }); + rl.close(); + } + } catch (e) { + console.error("FATAL: test failed with exception", e); + if (e instanceof TalerError) { + console.error(`error detail: ${j2s(e.errorDetail)}`); + } + status = "fail"; + } finally { + await gc.shutdown(); + } + const afterMs = new Date().getTime(); + return { + name: testName, + timeSec: (afterMs - startMs) / 1000, + status, + }; +} + +function shellWrap(s: string) { + return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"; +} + +export interface WalletCliOpts { + cryptoWorkerType?: "sync" | "node-worker-thread"; +} + +export class WalletCli { + private currentTimetravel: Duration | undefined; + private _client: WalletCoreApiClient; + + setTimetravel(d: Duration | undefined) { + this.currentTimetravel = d; + } + + private get timetravelArg(): string | undefined { + if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { + // Convert to microseconds + return `--timetravel=${this.currentTimetravel.d_ms * 1000}`; + } + return undefined; + } + + constructor( + private globalTestState: GlobalTestState, + private name: string = "default", + cliOpts: WalletCliOpts = {}, + ) { + const self = this; + this._client = { + async call(op: any, payload: any): Promise { + logger.info( + `calling wallet with timetravel arg ${j2s(self.timetravelArg)}`, + ); + const cryptoWorkerArg = cliOpts.cryptoWorkerType + ? `--crypto-worker=${cliOpts.cryptoWorkerType}` + : ""; + const resp = await sh( + self.globalTestState, + `wallet-${self.name}`, + `taler-wallet-cli ${ + self.timetravelArg ?? "" + } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${ + self.dbfile + }' api '${op}' ${shellWrap(JSON.stringify(payload))}`, + ); + logger.info("--- wallet core response ---"); + logger.info(resp); + logger.info("--- end of response ---"); + let ar: any; + try { + ar = JSON.parse(resp) as CoreApiResponse; + } catch (e) { + throw new Error("wallet CLI did not return a proper JSON response"); + } + if (ar.type === "error") { + throw TalerError.fromUncheckedDetail(ar.error); + } + return ar.result; + }, + }; + } + + get dbfile(): string { + return this.globalTestState.testDir + `/walletdb-${this.name}.json`; + } + + deleteDatabase() { + fs.unlinkSync(this.dbfile); + } + + private get timetravelArgArr(): string[] { + const tta = this.timetravelArg; + if (tta) { + return [tta]; + } + return []; + } + + get client(): WalletCoreApiClient { + return this._client; + } + + async runUntilDone(args: { maxRetries?: number } = {}): Promise { + await runCommand( + this.globalTestState, + `wallet-${this.name}`, + "taler-wallet-cli", + [ + "--no-throttle", + ...this.timetravelArgArr, + "-LTRACE", + "--skip-defaults", + "--wallet-db", + this.dbfile, + "run-until-done", + ...(args.maxRetries ? ["--max-retries", `${args.maxRetries}`] : []), + ], + ); + } + + async runPending(): Promise { + await runCommand( + this.globalTestState, + `wallet-${this.name}`, + "taler-wallet-cli", + [ + "--no-throttle", + "--skip-defaults", + "-LTRACE", + ...this.timetravelArgArr, + "--wallet-db", + this.dbfile, + "advanced", + "run-pending", + ], + ); + } +} + +export function getRandomIban(salt: string | null = null): string { + function getBban(salt: string | null): string { + if (!salt) return Math.random().toString().substring(2, 6); + let hashed = hash(stringToBytes(salt)); + let ret = ""; + for (let i = 0; i < hashed.length; i++) { + ret += hashed[i].toString(); + } + return ret.substring(0, 4); + } + + let cc_no_check = "131400"; // == DE00 + let bban = getBban(salt); + let check_digits = ( + 98 - + (Number.parseInt(`${bban}${cc_no_check}`) % 97) + ).toString(); + if (check_digits.length == 1) { + check_digits = `0${check_digits}`; + } + return `DE${check_digits}${bban}`; +} + +export function getWireMethodForTest(): string { + if (useLibeufinBank) return "iban"; + return "x-taler-bank"; +} + +/** + * Generate a payto address, whose authority depends + * on whether the banking is served by euFin or Pybank. + */ +export function getPayto(label: string): string { + if (useLibeufinBank) + return `payto://iban/SANDBOXX/${getRandomIban( + label, + )}?receiver-name=${label}`; + return `payto://x-taler-bank/localhost/${label}`; +} + +function waitMs(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts new file mode 100644 index 000000000..affaccd61 --- /dev/null +++ b/packages/taler-harness/src/harness/helpers.ts @@ -0,0 +1,444 @@ +/* + 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 + */ + +/** + * Helpers to create typical test environments. + * + * @author Florian Dold + */ + +/** + * Imports + */ +import { + AmountString, + ConfirmPayResultType, + MerchantContractTerms, + Duration, + PreparePayResultType, +} from "@gnu-taler/taler-util"; +import { + BankAccessApi, + BankApi, + HarnessExchangeBankAccount, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "./denomStructures.js"; +import { + FaultInjectedExchangeService, + FaultInjectedMerchantService, +} from "./faultInjection.js"; +import { + BankService, + DbInfo, + ExchangeService, + ExchangeServiceInterface, + getPayto, + GlobalTestState, + MerchantPrivateApi, + MerchantService, + MerchantServiceInterface, + setupDb, + WalletCli, + WithAuthorization, +} from "./harness.js"; + +export interface SimpleTestEnvironment { + commonDb: DbInfo; + bank: BankService; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + merchant: MerchantService; + wallet: WalletCli; +} + +export interface EnvOptions { + /** + * If provided, enable age restrictions with the specified age mask string. + */ + ageMaskSpec?: string; + + mixedAgeRestriction?: boolean; +} + +/** + * Run a test case with a simple TESTKUDOS Taler environment, consisting + * of one exchange, one bank and one merchant. + */ +export async function createSimpleTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise { + 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 ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + 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")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstance({ + id: "minst1", + name: "minst1", + paytoUris: [getPayto("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + console.log("setup done!"); + + const wallet = new WalletCli(t); + + return { + commonDb: db, + exchange, + merchant, + wallet, + bank, + exchangeBankAccount, + }; +} + +export interface FaultyMerchantTestEnvironment { + commonDb: DbInfo; + bank: BankService; + exchange: ExchangeService; + faultyExchange: FaultInjectedExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + merchant: MerchantService; + faultyMerchant: FaultInjectedMerchantService; + wallet: WalletCli; +} + +/** + * Run a test case with a simple TESTKUDOS Taler environment, consisting + * of one exchange, one bank and one merchant. + */ +export async function createFaultInjectedMerchantTestkudosEnvironment( + t: GlobalTestState, +): Promise { + 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(); + + merchant.addExchange(faultyExchange); + + 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, + }; +} + +/** + * Start withdrawing into the wallet. + * + * Only starts the operation, does not wait for it to finish. + */ +export async function startWithdrawViaBank( + t: GlobalTestState, + p: { + wallet: WalletCli; + bank: BankService; + exchange: ExchangeServiceInterface; + amount: AmountString; + restrictAge?: number; + }, +): Promise { + const { wallet, bank, exchange, amount } = p; + + const user = await BankApi.createRandomBankUser(bank); + const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount); + + // Hand it to the wallet + + await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }); + + await wallet.runPending(); + + // Withdraw (AKA select) + + await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }); + + // Confirm it + + await BankApi.confirmWithdrawalOperation(bank, user, wop); + + // We do *not* call runPending / runUntilDone on the wallet here. + // Some tests rely on the final withdraw failing. +} + +/** + * Withdraw balance. + */ +export async function withdrawViaBank( + t: GlobalTestState, + p: { + wallet: WalletCli; + bank: BankService; + exchange: ExchangeServiceInterface; + amount: AmountString; + restrictAge?: number; + }, +): Promise { + const { wallet } = p; + + await startWithdrawViaBank(t, p); + + await wallet.runUntilDone(); + + // Check balance + + await wallet.client.call(WalletApiOperation.GetBalances, {}); +} + +export async function applyTimeTravel( + timetravelDuration: Duration, + s: { + exchange?: ExchangeService; + merchant?: MerchantService; + wallet?: WalletCli; + }, +): Promise { + 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) { + s.wallet.setTimetravel(timetravelDuration); + } +} + +/** + * Make a simple payment and check that it succeeded. + */ +export async function makeTestPayment( + t: GlobalTestState, + args: { + merchant: MerchantServiceInterface; + wallet: WalletCli; + order: Partial; + instance?: string; + }, + auth: WithAuthorization = {}, +): Promise { + // Set up order. + + const { wallet, merchant } = args; + const instance = args.instance ?? "default"; + + const orderResp = await MerchantPrivateApi.createOrder( + merchant, + instance, + { + order: args.order, + }, + auth, + ); + + let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + }, + auth, + ); + + 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 r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { + proposalId: preparePayResult.proposalId, + }); + + t.assertTrue(r2.type === ConfirmPayResultType.Done); + + // Check if payment was successful. + + orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: orderResp.order_id, + instance, + }, + auth, + ); + + t.assertTrue(orderStatus.order_status === "paid"); +} diff --git a/packages/taler-harness/src/harness/libeufin-apis.ts b/packages/taler-harness/src/harness/libeufin-apis.ts new file mode 100644 index 000000000..f55275927 --- /dev/null +++ b/packages/taler-harness/src/harness/libeufin-apis.ts @@ -0,0 +1,872 @@ +/** + * This file defines most of the API calls offered + * by Nexus and Sandbox. They don't have state, + * therefore got moved away from libeufin.ts where + * the services get actually started and managed. + */ + +import axiosImp from "axios"; +const axios = axiosImp.default; +import { Logger, URL } from "@gnu-taler/taler-util"; + +export interface LibeufinSandboxServiceInterface { + baseUrl: string; +} + +export interface LibeufinNexusServiceInterface { + baseUrl: string; +} + +export interface CreateEbicsSubscriberRequest { + hostID: string; + userID: string; + partnerID: string; + systemID?: string; +} + +export interface BankAccountInfo { + iban: string; + bic: string; + name: string; + label: string; +} + +export interface CreateEbicsBankConnectionRequest { + name: string; // connection name. + ebicsURL: string; + hostID: string; + userID: string; + partnerID: string; + systemID?: string; +} + +export interface UpdateNexusUserRequest { + newPassword: string; +} + +export interface NexusAuth { + auth: { + username: string; + password: string; + }; +} + +export interface PostNexusTaskRequest { + name: string; + cronspec: string; + type: string; // fetch | submit + params: + | { + level: string; // report | statement | all + rangeType: string; // all | since-last | previous-days | latest + } + | {}; +} + +export interface CreateNexusUserRequest { + username: string; + password: string; +} + +export interface PostNexusPermissionRequest { + action: "revoke" | "grant"; + permission: { + subjectType: string; + subjectId: string; + resourceType: string; + resourceId: string; + permissionName: string; + }; +} + +export interface CreateAnastasisFacadeRequest { + name: string; + connectionName: string; + accountName: string; + currency: string; + reserveTransferLevel: "report" | "statement" | "notification"; +} + +export interface CreateTalerWireGatewayFacadeRequest { + name: string; + connectionName: string; + accountName: string; + currency: string; + reserveTransferLevel: "report" | "statement" | "notification"; +} + +export interface SandboxAccountTransactions { + payments: { + accountLabel: string; + creditorIban: string; + creditorBic?: string; + creditorName: string; + debtorIban: string; + debtorBic: string; + debtorName: string; + amount: string; + currency: string; + subject: string; + date: string; + creditDebitIndicator: "debit" | "credit"; + accountServicerReference: string; + }[]; +} + +export interface DeleteBankConnectionRequest { + bankConnectionId: string; +} + +export interface SimulateIncomingTransactionRequest { + debtorIban: string; + debtorBic: string; + debtorName: string; + + /** + * Subject / unstructured remittance info. + */ + subject: string; + + /** + * Decimal amount without currency. + */ + amount: string; +} + +export interface CreateEbicsBankAccountRequest { + subscriber: { + hostID: string; + partnerID: string; + userID: string; + systemID?: string; + }; + // IBAN + iban: string; + // BIC + bic: string; + // human name + name: string; + label: string; +} + +export interface LibeufinSandboxAddIncomingRequest { + creditorIban: string; + creditorBic: string; + creditorName: string; + debtorIban: string; + debtorBic: string; + debtorName: string; + subject: string; + amount: string; + currency: string; + uid: string; + direction: string; +} + +function getRandomString(): string { + return Math.random().toString(36).substring(2); +} + +/** + * APIs spread accross Legacy and Access, it is therefore + * the "base URL" relative to which API every call addresses. + */ +export namespace LibeufinSandboxApi { + // Need Access API base URL. + export async function demobankAccountInfo( + username: string, + password: string, + libeufinSandboxService: LibeufinSandboxServiceInterface, + accountLabel: string, + ) { + let url = new URL( + `accounts/${accountLabel}`, + libeufinSandboxService.baseUrl, + ); + return await axios.get(url.href, { + auth: { + username: username, + password: password, + }, + }); + } + + // Creates one bank account via the Access API. + // Need the /demobanks/$id/access-api as the base URL + export async function createDemobankAccount( + username: string, + password: string, + libeufinSandboxService: LibeufinSandboxServiceInterface, + iban: string|null = null, + ) { + let url = new URL( + "testing/register", + libeufinSandboxService.baseUrl + ); + await axios.post(url.href, { + username: username, + password: password, + iban: iban + }); + } + // Need /demobanks/$id as the base URL + export async function createDemobankEbicsSubscriber( + req: CreateEbicsSubscriberRequest, + demobankAccountLabel: string, + libeufinSandboxService: LibeufinSandboxServiceInterface, + username: string = "admin", + password: string = "secret", + ) { + // baseUrl should already be pointed to one demobank. + let url = new URL( + "ebics/subscribers", + libeufinSandboxService.baseUrl + ); + await axios.post( + url.href, + { + userID: req.userID, + hostID: req.hostID, + partnerID: req.partnerID, + demobankAccountLabel: demobankAccountLabel, + }, + { + auth: { + username: "admin", + password: "secret", + }, + }, + ); + } + + export async function rotateKeys( + libeufinSandboxService: LibeufinSandboxServiceInterface, + hostID: string, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL(`admin/ebics/hosts/${hostID}/rotate-keys`, baseUrl); + await axios.post( + url.href, + {}, + { + auth: { + username: "admin", + password: "secret", + }, + }, + ); + } + export async function createEbicsHost( + libeufinSandboxService: LibeufinSandboxServiceInterface, + hostID: string, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL("admin/ebics/hosts", baseUrl); + await axios.post( + url.href, + { + hostID, + ebicsVersion: "2.5", + }, + { + auth: { + username: "admin", + password: "secret", + }, + }, + ); + } + + export async function createBankAccount( + libeufinSandboxService: LibeufinSandboxServiceInterface, + req: BankAccountInfo, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL(`admin/bank-accounts/${req.label}`, baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + /** + * This function is useless. It creates a Ebics subscriber + * but never gives it a bank account. To be removed + */ + export async function createEbicsSubscriber( + libeufinSandboxService: LibeufinSandboxServiceInterface, + req: CreateEbicsSubscriberRequest, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL("admin/ebics/subscribers", baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + /** + * Create a new bank account and associate it to + * a existing EBICS subscriber. + */ + export async function createEbicsBankAccount( + libeufinSandboxService: LibeufinSandboxServiceInterface, + req: CreateEbicsBankAccountRequest, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL("admin/ebics/bank-accounts", baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + export async function simulateIncomingTransaction( + libeufinSandboxService: LibeufinSandboxServiceInterface, + accountLabel: string, + req: SimulateIncomingTransactionRequest, + ) { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL( + `admin/bank-accounts/${accountLabel}/simulate-incoming-transaction`, + baseUrl, + ); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "secret", + }, + }); + } + + export async function getAccountTransactions( + libeufinSandboxService: LibeufinSandboxServiceInterface, + accountLabel: string, + ): Promise { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL( + `admin/bank-accounts/${accountLabel}/transactions`, + baseUrl, + ); + const res = await axios.get(url.href, { + auth: { + username: "admin", + password: "secret", + }, + }); + return res.data as SandboxAccountTransactions; + } + + export async function getCamt053( + libeufinSandboxService: LibeufinSandboxServiceInterface, + accountLabel: string, + ): Promise { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL("admin/payments/camt", baseUrl); + return await axios.post( + url.href, + { + bankaccount: accountLabel, + type: 53, + }, + { + auth: { + username: "admin", + password: "secret", + }, + }, + ); + } + + export async function getAccountInfoWithBalance( + libeufinSandboxService: LibeufinSandboxServiceInterface, + accountLabel: string, + ): Promise { + const baseUrl = libeufinSandboxService.baseUrl; + let url = new URL(`admin/bank-accounts/${accountLabel}`, baseUrl); + return await axios.get(url.href, { + auth: { + username: "admin", + password: "secret", + }, + }); + } +} + +export namespace LibeufinNexusApi { + export async function getAllConnections( + nexus: LibeufinNexusServiceInterface, + ): Promise { + let url = new URL("bank-connections", nexus.baseUrl); + const res = await axios.get(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + return res; + } + + export async function deleteBankConnection( + libeufinNexusService: LibeufinNexusServiceInterface, + req: DeleteBankConnectionRequest, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("bank-connections/delete-connection", baseUrl); + return await axios.post(url.href, req, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function createEbicsBankConnection( + libeufinNexusService: LibeufinNexusServiceInterface, + req: CreateEbicsBankConnectionRequest, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("bank-connections", baseUrl); + await axios.post( + url.href, + { + source: "new", + type: "ebics", + name: req.name, + data: { + ebicsURL: req.ebicsURL, + hostID: req.hostID, + userID: req.userID, + partnerID: req.partnerID, + systemID: req.systemID, + }, + }, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function getBankAccount( + libeufinNexusService: LibeufinNexusServiceInterface, + accountName: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`bank-accounts/${accountName}`, baseUrl); + return await axios.get(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function submitInitiatedPayment( + libeufinNexusService: LibeufinNexusServiceInterface, + accountName: string, + paymentId: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `bank-accounts/${accountName}/payment-initiations/${paymentId}/submit`, + baseUrl, + ); + await axios.post( + url.href, + {}, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function fetchAccounts( + libeufinNexusService: LibeufinNexusServiceInterface, + connectionName: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `bank-connections/${connectionName}/fetch-accounts`, + baseUrl, + ); + await axios.post( + url.href, + {}, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function importConnectionAccount( + libeufinNexusService: LibeufinNexusServiceInterface, + connectionName: string, + offeredAccountId: string, + nexusBankAccountId: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `bank-connections/${connectionName}/import-account`, + baseUrl, + ); + await axios.post( + url.href, + { + offeredAccountId, + nexusBankAccountId, + }, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function connectBankConnection( + libeufinNexusService: LibeufinNexusServiceInterface, + connectionName: string, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`bank-connections/${connectionName}/connect`, baseUrl); + await axios.post( + url.href, + {}, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function getPaymentInitiations( + libeufinNexusService: LibeufinNexusServiceInterface, + accountName: string, + username: string = "admin", + password: string = "test", + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `/bank-accounts/${accountName}/payment-initiations`, + baseUrl, + ); + let response = await axios.get(url.href, { + auth: { + username: username, + password: password, + }, + }); + console.log( + `Payment initiations of: ${accountName}`, + JSON.stringify(response.data, null, 2), + ); + } + + export async function getConfig( + libeufinNexusService: LibeufinNexusServiceInterface, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/config`, baseUrl); + let response = await axios.get(url.href); + } + + // Uses the Anastasis API to get a list of transactions. + export async function getAnastasisTransactions( + libeufinNexusService: LibeufinNexusServiceInterface, + anastasisBaseUrl: string, + params: {}, // of the request: {delta: 5, ..} + username: string = "admin", + password: string = "test", + ): Promise { + let url = new URL("history/incoming", anastasisBaseUrl); + let response = await axios.get(url.href, { + params: params, + auth: { + username: username, + password: password, + }, + }); + return response; + } + + // FIXME: this function should return some structured + // object that represents a history. + export async function getAccountTransactions( + libeufinNexusService: LibeufinNexusServiceInterface, + accountName: string, + username: string = "admin", + password: string = "test", + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/bank-accounts/${accountName}/transactions`, baseUrl); + let response = await axios.get(url.href, { + auth: { + username: username, + password: password, + }, + }); + return response; + } + + export async function fetchTransactions( + libeufinNexusService: LibeufinNexusServiceInterface, + accountName: string, + rangeType: string = "all", + level: string = "report", + username: string = "admin", + password: string = "test", + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `/bank-accounts/${accountName}/fetch-transactions`, + baseUrl, + ); + return await axios.post( + url.href, + { + rangeType: rangeType, + level: level, + }, + { + auth: { + username: username, + password: password, + }, + }, + ); + } + + export async function changePassword( + libeufinNexusService: LibeufinNexusServiceInterface, + username: string, + req: UpdateNexusUserRequest, + auth: NexusAuth, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/users/${username}/password`, baseUrl); + await axios.post(url.href, req, auth); + } + + export async function getUser( + libeufinNexusService: LibeufinNexusServiceInterface, + auth: NexusAuth, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/user`, baseUrl); + return await axios.get(url.href, auth); + } + + export async function createUser( + libeufinNexusService: LibeufinNexusServiceInterface, + req: CreateNexusUserRequest, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/users`, baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function getAllPermissions( + libeufinNexusService: LibeufinNexusServiceInterface, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/permissions`, baseUrl); + return await axios.get(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function postPermission( + libeufinNexusService: LibeufinNexusServiceInterface, + req: PostNexusPermissionRequest, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/permissions`, baseUrl); + await axios.post(url.href, req, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function getTasks( + libeufinNexusService: LibeufinNexusServiceInterface, + bankAccountName: string, + // When void, the request returns the list of all the + // tasks under this bank account. + taskName: string | void, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); + if (taskName) url = new URL(taskName, `${url.href}/`); + + // It's caller's responsibility to interpret the response. + return await axios.get(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function deleteTask( + libeufinNexusService: LibeufinNexusServiceInterface, + bankAccountName: string, + taskName: string, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `/bank-accounts/${bankAccountName}/schedule/${taskName}`, + baseUrl, + ); + await axios.delete(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function postTask( + libeufinNexusService: LibeufinNexusServiceInterface, + bankAccountName: string, + req: PostNexusTaskRequest, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); + return await axios.post(url.href, req, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function deleteFacade( + libeufinNexusService: LibeufinNexusServiceInterface, + facadeName: string, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL(`facades/${facadeName}`, baseUrl); + return await axios.delete(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function getAllFacades( + libeufinNexusService: LibeufinNexusServiceInterface, + ): Promise { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("facades", baseUrl); + return await axios.get(url.href, { + auth: { + username: "admin", + password: "test", + }, + }); + } + + export async function createAnastasisFacade( + libeufinNexusService: LibeufinNexusServiceInterface, + req: CreateAnastasisFacadeRequest, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("facades", baseUrl); + await axios.post( + url.href, + { + name: req.name, + type: "anastasis", + config: { + bankAccount: req.accountName, + bankConnection: req.connectionName, + currency: req.currency, + reserveTransferLevel: req.reserveTransferLevel, + }, + }, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function createTwgFacade( + libeufinNexusService: LibeufinNexusServiceInterface, + req: CreateTalerWireGatewayFacadeRequest, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL("facades", baseUrl); + await axios.post( + url.href, + { + name: req.name, + type: "taler-wire-gateway", + config: { + bankAccount: req.accountName, + bankConnection: req.connectionName, + currency: req.currency, + reserveTransferLevel: req.reserveTransferLevel, + }, + }, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } + + export async function submitAllPaymentInitiations( + libeufinNexusService: LibeufinNexusServiceInterface, + accountId: string, + ) { + const baseUrl = libeufinNexusService.baseUrl; + let url = new URL( + `/bank-accounts/${accountId}/submit-all-payment-initiations`, + baseUrl, + ); + await axios.post( + url.href, + {}, + { + auth: { + username: "admin", + password: "test", + }, + }, + ); + } +} diff --git a/packages/taler-harness/src/harness/libeufin.ts b/packages/taler-harness/src/harness/libeufin.ts new file mode 100644 index 000000000..638c8ed90 --- /dev/null +++ b/packages/taler-harness/src/harness/libeufin.ts @@ -0,0 +1,910 @@ +/* + 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 + */ + +/** + * This file defines euFin test logic that needs state + * and that depends on the main harness.ts. The other + * definitions - mainly helper functions to call RESTful + * APIs - moved to libeufin-apis.ts. That enables harness.ts + * to depend on such API calls, in contrast to the previous + * situation where harness.ts had to include this file causing + * a circular dependency. */ + +/** + * Imports. + */ +import axios from "axios"; +import { URL, Logger } from "@gnu-taler/taler-util"; +import { + GlobalTestState, + DbInfo, + pingProc, + ProcessWrapper, + runCommand, + setupDb, + sh, + getRandomIban, +} from "../harness/harness.js"; +import { + LibeufinSandboxApi, + LibeufinNexusApi, + CreateEbicsBankAccountRequest, + LibeufinSandboxServiceInterface, + CreateTalerWireGatewayFacadeRequest, + SimulateIncomingTransactionRequest, + SandboxAccountTransactions, + DeleteBankConnectionRequest, + CreateEbicsBankConnectionRequest, + UpdateNexusUserRequest, + NexusAuth, + CreateAnastasisFacadeRequest, + PostNexusTaskRequest, + PostNexusPermissionRequest, + CreateNexusUserRequest, +} from "../harness/libeufin-apis.js"; + + +const logger = new Logger("libeufin.ts"); + +export { LibeufinSandboxApi, LibeufinNexusApi }; + +export interface LibeufinServices { + libeufinSandbox: LibeufinSandboxService; + libeufinNexus: LibeufinNexusService; + commonDb: DbInfo; +} + +export interface LibeufinSandboxConfig { + httpPort: number; + databaseJdbcUri: string; +} + +export interface LibeufinNexusConfig { + httpPort: number; + databaseJdbcUri: string; +} + +interface LibeufinNexusMoneyMovement { + amount: string; + creditDebitIndicator: string; + details: { + debtor: { + name: string; + }; + debtorAccount: { + iban: string; + }; + debtorAgent: { + bic: string; + }; + creditor: { + name: string; + }; + creditorAccount: { + iban: string; + }; + creditorAgent: { + bic: string; + }; + endToEndId: string; + unstructuredRemittanceInformation: string; + }; +} + +interface LibeufinNexusBatches { + batchTransactions: Array; +} + +interface LibeufinNexusTransaction { + amount: string; + creditDebitIndicator: string; + status: string; + bankTransactionCode: string; + valueDate: string; + bookingDate: string; + accountServicerRef: string; + batches: Array; +} + +interface LibeufinNexusTransactions { + transactions: Array; +} + +export interface LibeufinCliDetails { + nexusUrl: string; + sandboxUrl: string; + nexusDatabaseUri: string; + sandboxDatabaseUri: string; + nexusUser: LibeufinNexusUser; +} + +export interface LibeufinEbicsSubscriberDetails { + hostId: string; + partnerId: string; + userId: string; +} + +export interface LibeufinEbicsConnectionDetails { + subscriberDetails: LibeufinEbicsSubscriberDetails; + ebicsUrl: string; + connectionName: string; +} + +export interface LibeufinBankAccountDetails { + currency: string; + iban: string; + bic: string; + personName: string; + accountName: string; +} + +export interface LibeufinNexusUser { + username: string; + password: string; +} + +export interface LibeufinBackupFileDetails { + passphrase: string; + outputFile: string; + connectionName: string; +} + +export interface LibeufinKeyLetterDetails { + outputFile: string; + connectionName: string; +} + +export interface LibeufinBankAccountImportDetails { + offeredBankAccountName: string; + nexusBankAccountName: string; + connectionName: string; +} + +export interface LibeufinPreparedPaymentDetails { + creditorIban: string; + creditorBic: string; + creditorName: string; + subject: string; + amount: string; + currency: string; + nexusBankAccountName: string; +} + +export class LibeufinSandboxService implements LibeufinSandboxServiceInterface { + static async create( + gc: GlobalTestState, + sandboxConfig: LibeufinSandboxConfig, + ): Promise { + return new LibeufinSandboxService(gc, sandboxConfig); + } + + sandboxProc: ProcessWrapper | undefined; + globalTestState: GlobalTestState; + + constructor( + gc: GlobalTestState, + private sandboxConfig: LibeufinSandboxConfig, + ) { + this.globalTestState = gc; + } + + get baseUrl(): string { + return `http://localhost:${this.sandboxConfig.httpPort}/`; + } + + async start(): Promise { + await sh( + this.globalTestState, + "libeufin-sandbox-config", + "libeufin-sandbox config default", + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, + }, + ); + + this.sandboxProc = this.globalTestState.spawnService( + "libeufin-sandbox", + ["serve", "--port", `${this.sandboxConfig.httpPort}`], + "libeufin-sandbox", + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, + LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", + }, + ); + } + + async c53tick(): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-sandbox-c53tick", + "libeufin-sandbox camt053tick", + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, + }, + ); + return stdout; + } + + async makeTransaction( + debit: string, + credit: string, + amount: string, // $currency:x.y + subject: string, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-sandbox-maketransfer", + `libeufin-sandbox make-transaction --debit-account=${debit} --credit-account=${credit} ${amount} "${subject}"`, + { + ...process.env, + LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, + }, + ); + return stdout; + } + + async pingUntilAvailable(): Promise { + const url = this.baseUrl; + await pingProc(this.sandboxProc, url, "libeufin-sandbox"); + } +} + +export class LibeufinNexusService { + static async create( + gc: GlobalTestState, + nexusConfig: LibeufinNexusConfig, + ): Promise { + return new LibeufinNexusService(gc, nexusConfig); + } + + nexusProc: ProcessWrapper | undefined; + globalTestState: GlobalTestState; + + constructor(gc: GlobalTestState, private nexusConfig: LibeufinNexusConfig) { + this.globalTestState = gc; + } + + get baseUrl(): string { + return `http://localhost:${this.nexusConfig.httpPort}/`; + } + + async start(): Promise { + await runCommand( + this.globalTestState, + "libeufin-nexus-superuser", + "libeufin-nexus", + ["superuser", "admin", "--password", "test"], + { + ...process.env, + LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, + }, + ); + + this.nexusProc = this.globalTestState.spawnService( + "libeufin-nexus", + ["serve", "--port", `${this.nexusConfig.httpPort}`], + "libeufin-nexus", + { + ...process.env, + LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, + }, + ); + } + + async pingUntilAvailable(): Promise { + const url = `${this.baseUrl}config`; + await pingProc(this.nexusProc, url, "libeufin-nexus"); + } + + async createNexusSuperuser(details: LibeufinNexusUser): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-nexus", + `libeufin-nexus superuser ${details.username} --password=${details.password}`, + { + ...process.env, + LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, + }, + ); + console.log(stdout); + } +} + +export interface TwgAddIncomingRequest { + amount: string; + reserve_pub: string; + debit_account: string; +} + +/** + * The bundle aims at minimizing the amount of input + * data that is required to initialize a new user + Ebics + * connection. + */ +export class NexusUserBundle { + userReq: CreateNexusUserRequest; + connReq: CreateEbicsBankConnectionRequest; + anastasisReq: CreateAnastasisFacadeRequest; + twgReq: CreateTalerWireGatewayFacadeRequest; + twgTransferPermission: PostNexusPermissionRequest; + twgHistoryPermission: PostNexusPermissionRequest; + twgAddIncomingPermission: PostNexusPermissionRequest; + localAccountName: string; + remoteAccountName: string; + + constructor(salt: string, ebicsURL: string) { + this.userReq = { + username: `username-${salt}`, + password: `password-${salt}`, + }; + + this.connReq = { + name: `connection-${salt}`, + ebicsURL: ebicsURL, + hostID: `ebicshost,${salt}`, + partnerID: `ebicspartner,${salt}`, + userID: `ebicsuser,${salt}`, + }; + + this.twgReq = { + currency: "EUR", + name: `twg-${salt}`, + reserveTransferLevel: "report", + accountName: `local-account-${salt}`, + connectionName: `connection-${salt}`, + }; + this.anastasisReq = { + currency: "EUR", + name: `anastasis-${salt}`, + reserveTransferLevel: "report", + accountName: `local-account-${salt}`, + connectionName: `connection-${salt}`, + }; + this.remoteAccountName = `remote-account-${salt}`; + this.localAccountName = `local-account-${salt}`; + this.twgTransferPermission = { + action: "grant", + permission: { + subjectId: `username-${salt}`, + subjectType: "user", + resourceType: "facade", + resourceId: `twg-${salt}`, + permissionName: "facade.talerWireGateway.transfer", + }, + }; + this.twgHistoryPermission = { + action: "grant", + permission: { + subjectId: `username-${salt}`, + subjectType: "user", + resourceType: "facade", + resourceId: `twg-${salt}`, + permissionName: "facade.talerWireGateway.history", + }, + }; + } +} + +/** + * The bundle aims at minimizing the amount of input + * data that is required to initialize a new Sandbox + * customer, associating their bank account with a Ebics + * subscriber. + */ +export class SandboxUserBundle { + ebicsBankAccount: CreateEbicsBankAccountRequest; + constructor(salt: string) { + this.ebicsBankAccount = { + bic: "BELADEBEXXX", + iban: getRandomIban(), + label: `remote-account-${salt}`, + name: `Taler Exchange: ${salt}`, + subscriber: { + hostID: `ebicshost,${salt}`, + partnerID: `ebicspartner,${salt}`, + userID: `ebicsuser,${salt}`, + }, + }; + } +} + +export class LibeufinCli { + cliDetails: LibeufinCliDetails; + globalTestState: GlobalTestState; + + constructor(gc: GlobalTestState, cd: LibeufinCliDetails) { + this.globalTestState = gc; + this.cliDetails = cd; + } + + env(): any { + return { + ...process.env, + LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl, + LIBEUFIN_SANDBOX_USERNAME: "admin", + LIBEUFIN_SANDBOX_PASSWORD: "secret", + }; + } + + async checkSandbox(): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-checksandbox", + "libeufin-cli sandbox check", + this.env(), + ); + } + + async registerBankCustomer(username: string, password: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-registercustomer", + "libeufin-cli sandbox demobank register --name='Test Customer'", + { + ...process.env, + LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl + "/demobanks/default", + LIBEUFIN_SANDBOX_USERNAME: username, + LIBEUFIN_SANDBOX_PASSWORD: password, + }, + ); + console.log(stdout); + } + + async createEbicsHost(hostId: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createebicshost", + `libeufin-cli sandbox ebicshost create --host-id=${hostId}`, + this.env(), + ); + console.log(stdout); + } + + async createEbicsSubscriber( + details: LibeufinEbicsSubscriberDetails, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createebicssubscriber", + "libeufin-cli sandbox ebicssubscriber create" + + ` --host-id=${details.hostId}` + + ` --partner-id=${details.partnerId}` + + ` --user-id=${details.userId}`, + this.env(), + ); + console.log(stdout); + } + + async createEbicsBankAccount( + sd: LibeufinEbicsSubscriberDetails, + bankAccountDetails: LibeufinBankAccountDetails, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createebicsbankaccount", + "libeufin-cli sandbox ebicsbankaccount create" + + ` --iban=${bankAccountDetails.iban}` + + ` --bic=${bankAccountDetails.bic}` + + ` --person-name='${bankAccountDetails.personName}'` + + ` --account-name=${bankAccountDetails.accountName}` + + ` --ebics-host-id=${sd.hostId}` + + ` --ebics-partner-id=${sd.partnerId}` + + ` --ebics-user-id=${sd.userId}`, + this.env(), + ); + console.log(stdout); + } + + async generateTransactions(accountName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-generatetransactions", + `libeufin-cli sandbox bankaccount generate-transactions ${accountName}`, + this.env(), + ); + console.log(stdout); + } + + async showSandboxTransactions(accountName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-showsandboxtransactions", + `libeufin-cli sandbox bankaccount transactions ${accountName}`, + this.env(), + ); + console.log(stdout); + } + + async createEbicsConnection( + connectionDetails: LibeufinEbicsConnectionDetails, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createebicsconnection", + `libeufin-cli connections new-ebics-connection` + + ` --ebics-url=${connectionDetails.ebicsUrl}` + + ` --host-id=${connectionDetails.subscriberDetails.hostId}` + + ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` + + ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` + + ` ${connectionDetails.connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async createBackupFile(details: LibeufinBackupFileDetails): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createbackupfile", + `libeufin-cli connections export-backup` + + ` --passphrase=${details.passphrase}` + + ` --output-file=${details.outputFile}` + + ` ${details.connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async createKeyLetter(details: LibeufinKeyLetterDetails): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-createkeyletter", + `libeufin-cli connections get-key-letter` + + ` ${details.connectionName} ${details.outputFile}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async connect(connectionName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-connect", + `libeufin-cli connections connect ${connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async downloadBankAccounts(connectionName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-downloadbankaccounts", + `libeufin-cli connections download-bank-accounts ${connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async listOfferedBankAccounts(connectionName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-listofferedbankaccounts", + `libeufin-cli connections list-offered-bank-accounts ${connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async importBankAccount( + importDetails: LibeufinBankAccountImportDetails, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-importbankaccount", + "libeufin-cli connections import-bank-account" + + ` --offered-account-id=${importDetails.offeredBankAccountName}` + + ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` + + ` ${importDetails.connectionName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async fetchTransactions(bankAccountName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-fetchtransactions", + `libeufin-cli accounts fetch-transactions ${bankAccountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async transactions(bankAccountName: string): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-transactions", + `libeufin-cli accounts transactions ${bankAccountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async preparePayment(details: LibeufinPreparedPaymentDetails): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-preparepayment", + `libeufin-cli accounts prepare-payment` + + ` --creditor-iban=${details.creditorIban}` + + ` --creditor-bic=${details.creditorBic}` + + ` --creditor-name='${details.creditorName}'` + + ` --payment-subject='${details.subject}'` + + ` --payment-amount=${details.currency}:${details.amount}` + + ` ${details.nexusBankAccountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async submitPayment( + details: LibeufinPreparedPaymentDetails, + paymentUuid: string, + ): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-submitpayments", + `libeufin-cli accounts submit-payments` + + ` --payment-uuid=${paymentUuid}` + + ` ${details.nexusBankAccountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async newAnastasisFacade(req: NewAnastasisFacadeReq): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-new-anastasis-facade", + `libeufin-cli facades new-anastasis-facade` + + ` --currency ${req.currency}` + + ` --facade-name ${req.facadeName}` + + ` ${req.connectionName} ${req.accountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async newTalerWireGatewayFacade(req: NewTalerWireGatewayReq): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-new-taler-wire-gateway-facade", + `libeufin-cli facades new-taler-wire-gateway-facade` + + ` --currency ${req.currency}` + + ` --facade-name ${req.facadeName}` + + ` ${req.connectionName} ${req.accountName}`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } + + async listFacades(): Promise { + const stdout = await sh( + this.globalTestState, + "libeufin-cli-facades-list", + `libeufin-cli facades list`, + { + ...process.env, + LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, + LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, + LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, + }, + ); + console.log(stdout); + } +} + +interface NewAnastasisFacadeReq { + facadeName: string; + connectionName: string; + accountName: string; + currency: string; +} + +interface NewTalerWireGatewayReq { + facadeName: string; + connectionName: string; + accountName: string; + currency: string; +} + +/** + * Launch Nexus and Sandbox AND creates users / facades / bank accounts / + * .. all that's required to start making bank traffic. + */ +export async function launchLibeufinServices( + t: GlobalTestState, + nexusUserBundle: NexusUserBundle[], + sandboxUserBundle: SandboxUserBundle[] = [], + withFacades: string[] = [], // takes only "twg" and/or "anastasis" +): Promise { + 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(); + console.log("Libeufin services launched!"); + + for (let sb of sandboxUserBundle) { + await LibeufinSandboxApi.createEbicsHost( + libeufinSandbox, + sb.ebicsBankAccount.subscriber.hostID, + ); + await LibeufinSandboxApi.createEbicsSubscriber( + libeufinSandbox, + sb.ebicsBankAccount.subscriber, + ); + await LibeufinSandboxApi.createDemobankAccount( + sb.ebicsBankAccount.label, + "password-unused", + { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/access-api/" } + ); + await LibeufinSandboxApi.createDemobankEbicsSubscriber( + sb.ebicsBankAccount.subscriber, + sb.ebicsBankAccount.label, + { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/" } + ); + } + console.log("Sandbox user(s) / account(s) / subscriber(s): created"); + + for (let nb of nexusUserBundle) { + await LibeufinNexusApi.createEbicsBankConnection(libeufinNexus, nb.connReq); + await LibeufinNexusApi.connectBankConnection( + libeufinNexus, + nb.connReq.name, + ); + await LibeufinNexusApi.fetchAccounts(libeufinNexus, nb.connReq.name); + await LibeufinNexusApi.importConnectionAccount( + libeufinNexus, + nb.connReq.name, + nb.remoteAccountName, + nb.localAccountName, + ); + await LibeufinNexusApi.createUser(libeufinNexus, nb.userReq); + for (let facade of withFacades) { + switch (facade) { + case "twg": + await LibeufinNexusApi.createTwgFacade(libeufinNexus, nb.twgReq); + await LibeufinNexusApi.postPermission( + libeufinNexus, + nb.twgTransferPermission, + ); + await LibeufinNexusApi.postPermission( + libeufinNexus, + nb.twgHistoryPermission, + ); + break; + case "anastasis": + await LibeufinNexusApi.createAnastasisFacade( + libeufinNexus, + nb.anastasisReq, + ); + } + } + } + console.log( + "Nexus user(s) / connection(s) / facade(s) / permission(s): created", + ); + + return { + commonDb: db, + libeufinNexus: libeufinNexus, + libeufinSandbox: libeufinSandbox, + }; +} + +/** + * Helper function that searches a payment among + * a list, as returned by Nexus. The key is just + * the payment subject. + */ +export function findNexusPayment( + key: string, + payments: LibeufinNexusTransactions, +): LibeufinNexusMoneyMovement | void { + let transactions = payments["transactions"]; + for (let i = 0; i < transactions.length; i++) { + let batches = transactions[i]["batches"]; + for (let y = 0; y < batches.length; y++) { + let movements = batches[y]["batchTransactions"]; + for (let z = 0; z < movements.length; z++) { + let movement = movements[z]; + if (movement["details"]["unstructuredRemittanceInformation"] == key) + return movement; + } + } + } +} diff --git a/packages/taler-harness/src/harness/merchantApiTypes.ts b/packages/taler-harness/src/harness/merchantApiTypes.ts new file mode 100644 index 000000000..2a59b0160 --- /dev/null +++ b/packages/taler-harness/src/harness/merchantApiTypes.ts @@ -0,0 +1,337 @@ +/* + 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 + */ + +/** + * Test harness for various GNU Taler components. + * Also provides a fault-injection proxy. + * + * @author Florian Dold + */ + +/** + * Imports. + */ +import { + MerchantContractTerms, + Duration, + Codec, + buildCodecForObject, + codecForString, + codecOptional, + codecForConstString, + codecForBoolean, + codecForNumber, + codecForMerchantContractTerms, + codecForAny, + buildCodecForUnion, + AmountString, + AbsoluteTime, + CoinPublicKeyString, + EddsaPublicKeyString, + codecForAmountString, + TalerProtocolDuration, + codecForTimestamp, + TalerProtocolTimestamp, +} from "@gnu-taler/taler-util"; + +export interface PostOrderRequest { + // The order must at least contain the minimal + // order detail, but can override all + order: Partial; + + // if set, the backend will then set the refund deadline to the current + // time plus the specified delay. + refund_delay?: TalerProtocolDuration; + + // specifies the payment target preferred by the client. Can be used + // to select among the various (active) wire methods supported by the instance. + payment_target?: string; + + // FIXME: some fields are missing + + // Should a token for claiming the order be generated? + // False can make sense if the ORDER_ID is sufficiently + // high entropy to prevent adversarial claims (like it is + // if the backend auto-generates one). Default is 'true'. + create_token?: boolean; +} + +export type ClaimToken = string; + +export interface PostOrderResponse { + order_id: string; + token?: ClaimToken; +} + +export const codecForPostOrderResponse = (): Codec => + buildCodecForObject() + .property("order_id", codecForString()) + .property("token", codecOptional(codecForString())) + .build("PostOrderResponse"); + + +export const codecForRefundDetails = (): Codec => + buildCodecForObject() + .property("reason", codecForString()) + .property("pending", codecForBoolean()) + .property("amount", codecForString()) + .property("timestamp", codecForTimestamp) + .build("PostOrderResponse"); + +export const codecForCheckPaymentPaidResponse = + (): Codec => + buildCodecForObject() + .property("order_status_url", codecForString()) + .property("order_status", codecForConstString("paid")) + .property("refunded", codecForBoolean()) + .property("wired", codecForBoolean()) + .property("deposit_total", codecForAmountString()) + .property("exchange_ec", codecForNumber()) + .property("exchange_hc", codecForNumber()) + .property("refund_amount", codecForAmountString()) + .property("contract_terms", codecForMerchantContractTerms()) + // FIXME: specify + .property("wire_details", codecForAny()) + .property("wire_reports", codecForAny()) + .property("refund_details", codecForAny()) + .build("CheckPaymentPaidResponse"); + +export const codecForCheckPaymentUnpaidResponse = + (): Codec => + buildCodecForObject() + .property("order_status", codecForConstString("unpaid")) + .property("taler_pay_uri", codecForString()) + .property("order_status_url", codecForString()) + .property("already_paid_order_id", codecOptional(codecForString())) + .build("CheckPaymentPaidResponse"); + +export const codecForCheckPaymentClaimedResponse = + (): Codec => + buildCodecForObject() + .property("order_status", codecForConstString("claimed")) + .property("contract_terms", codecForMerchantContractTerms()) + .build("CheckPaymentClaimedResponse"); + +export const codecForMerchantOrderPrivateStatusResponse = + (): Codec => + buildCodecForUnion() + .discriminateOn("order_status") + .alternative("paid", codecForCheckPaymentPaidResponse()) + .alternative("unpaid", codecForCheckPaymentUnpaidResponse()) + .alternative("claimed", codecForCheckPaymentClaimedResponse()) + .build("MerchantOrderPrivateStatusResponse"); + +export type MerchantOrderPrivateStatusResponse = + | CheckPaymentPaidResponse + | CheckPaymentUnpaidResponse + | CheckPaymentClaimedResponse; + +export interface CheckPaymentClaimedResponse { + // Wallet claimed the order, but didn't pay yet. + order_status: "claimed"; + + contract_terms: MerchantContractTerms; +} + +export interface CheckPaymentPaidResponse { + // did the customer pay for this contract + order_status: "paid"; + + // Was the payment refunded (even partially) + refunded: boolean; + + // Did the exchange wire us the funds + wired: boolean; + + // Total amount the exchange deposited into our bank account + // for this contract, excluding fees. + deposit_total: AmountString; + + // Numeric error code indicating errors the exchange + // encountered tracking the wire transfer for this purchase (before + // we even got to specific coin issues). + // 0 if there were no issues. + exchange_ec: number; + + // HTTP status code returned by the exchange when we asked for + // information to track the wire transfer for this purchase. + // 0 if there were no issues. + exchange_hc: number; + + // Total amount that was refunded, 0 if refunded is false. + refund_amount: AmountString; + + // Contract terms + contract_terms: MerchantContractTerms; + + // Ihe wire transfer status from the exchange for this order if available, otherwise empty array + wire_details: TransactionWireTransfer[]; + + // Reports about trouble obtaining wire transfer details, empty array if no trouble were encountered. + wire_reports: TransactionWireReport[]; + + // The refund details for this order. One entry per + // refunded coin; empty array if there are no refunds. + refund_details: RefundDetails[]; + + order_status_url: string; +} + +export interface CheckPaymentUnpaidResponse { + order_status: "unpaid"; + + // URI that the wallet must process to complete the payment. + taler_pay_uri: string; + + order_status_url: string; + + // Alternative order ID which was paid for already in the same session. + // Only given if the same product was purchased before in the same session. + already_paid_order_id?: string; + + // We do we NOT return the contract terms here because they may not + // exist in case the wallet did not yet claim them. +} + +export interface RefundDetails { + // Reason given for the refund + reason: string; + + // when was the refund approved + timestamp: TalerProtocolTimestamp; + + // has not been taken yet + pending: boolean; + + // Total amount that was refunded (minus a refund fee). + amount: AmountString; +} + +export interface TransactionWireTransfer { + // Responsible exchange + exchange_url: string; + + // 32-byte wire transfer identifier + wtid: string; + + // execution time of the wire transfer + execution_time: AbsoluteTime; + + // Total amount that has been wire transferred + // to the merchant + amount: AmountString; + + // Was this transfer confirmed by the merchant via the + // POST /transfers API, or is it merely claimed by the exchange? + confirmed: boolean; +} + +export interface TransactionWireReport { + // Numerical error code + code: number; + + // Human-readable error description + hint: string; + + // Numerical error code from the exchange. + exchange_ec: number; + + // HTTP status code received from the exchange. + exchange_hc: number; + + // Public key of the coin for which we got the exchange error. + coin_pub: CoinPublicKeyString; +} + +export interface TippingReserveStatus { + // Array of all known reserves (possibly empty!) + reserves: ReserveStatusEntry[]; +} + +export interface ReserveStatusEntry { + // Public key of the reserve + reserve_pub: string; + + // Timestamp when it was established + creation_time: AbsoluteTime; + + // Timestamp when it expires + expiration_time: AbsoluteTime; + + // Initial amount as per reserve creation call + merchant_initial_amount: AmountString; + + // Initial amount as per exchange, 0 if exchange did + // not confirm reserve creation yet. + exchange_initial_amount: AmountString; + + // Amount picked up so far. + pickup_amount: AmountString; + + // Amount approved for tips that exceeds the pickup_amount. + committed_amount: AmountString; + + // Is this reserve active (false if it was deleted but not purged) + active: boolean; +} + +export interface TipCreateConfirmation { + // Unique tip identifier for the tip that was created. + tip_id: string; + + // taler://tip URI for the tip + taler_tip_uri: string; + + // URL that will directly trigger processing + // the tip when the browser is redirected to it + tip_status_url: string; + + // when does the tip expire + tip_expiration: AbsoluteTime; +} + +export interface TipCreateRequest { + // Amount that the customer should be tipped + amount: AmountString; + + // Justification for giving the tip + justification: string; + + // URL that the user should be directed to after tipping, + // will be included in the tip_token. + next_url: string; +} + +export interface MerchantInstancesResponse { + // List of instances that are present in the backend (see Instance) + instances: MerchantInstanceDetail[]; +} + +export interface MerchantInstanceDetail { + // Merchant name corresponding to this instance. + name: string; + + // Merchant instance this response is about ($INSTANCE) + id: string; + + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKeyString; + + // List of the payment targets supported by this instance. Clients can + // specify the desired payment target in /order requests. Note that + // front-ends do not have to support wallets selecting payment targets. + payment_targets: string[]; +} diff --git a/packages/taler-harness/src/harness/sync.ts b/packages/taler-harness/src/harness/sync.ts new file mode 100644 index 000000000..a9e8de412 --- /dev/null +++ b/packages/taler-harness/src/harness/sync.ts @@ -0,0 +1,119 @@ +/* + 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 + */ + +/** + * Imports. + */ +import { URL } from "@gnu-taler/taler-util"; +import * as fs from "fs"; +import * as util from "util"; +import { + GlobalTestState, + pingProc, + ProcessWrapper, +} from "../harness/harness.js"; +import { Configuration } from "@gnu-taler/taler-util"; +import * as child_process from "child_process"; + +const exec = util.promisify(child_process.exec); + +export interface SyncConfig { + /** + * Human-readable name used in the test harness logs. + */ + name: string; + + httpPort: number; + + /** + * Database connection string (only postgres is supported). + */ + database: string; + + annualFee: string; + + currency: string; + + uploadLimitMb: number; + + /** + * Fulfillment URL used for contract terms related to + * sync. + */ + fulfillmentUrl: string; + + paymentBackendUrl: string; +} + +function setSyncPaths(config: Configuration, home: string) { + config.setString("paths", "sync_home", home); + // We need to make sure that the path of taler_runtime_dir isn't too long, + // as it contains unix domain sockets (108 character limit). + const runDir = fs.mkdtempSync("/tmp/taler-test-"); + config.setString("paths", "sync_runtime_dir", runDir); + config.setString("paths", "sync_data_home", "$SYNC_HOME/.local/share/sync/"); + config.setString("paths", "sync_config_home", "$SYNC_HOME/.config/sync/"); + config.setString("paths", "sync_cache_home", "$SYNC_HOME/.config/sync/"); +} + +export class SyncService { + static async create( + gc: GlobalTestState, + sc: SyncConfig, + ): Promise { + const config = new Configuration(); + + const cfgFilename = gc.testDir + `/sync-${sc.name}.conf`; + setSyncPaths(config, gc.testDir + "/synchome"); + config.setString("taler", "currency", sc.currency); + config.setString("sync", "serve", "tcp"); + config.setString("sync", "port", `${sc.httpPort}`); + config.setString("sync", "db", "postgres"); + config.setString("syncdb-postgres", "config", sc.database); + config.setString("sync", "payment_backend_url", sc.paymentBackendUrl); + config.setString("sync", "upload_limit_mb", `${sc.uploadLimitMb}`); + config.write(cfgFilename); + + return new SyncService(gc, sc, cfgFilename); + } + + proc: ProcessWrapper | undefined; + + get baseUrl(): string { + return `http://localhost:${this.syncConfig.httpPort}/`; + } + + async start(): Promise { + await exec(`sync-dbinit -c "${this.configFilename}"`); + + this.proc = this.globalState.spawnService( + "sync-httpd", + ["-LDEBUG", "-c", this.configFilename], + `sync-${this.syncConfig.name}`, + ); + } + + async pingUntilAvailable(): Promise { + const url = new URL("config", this.baseUrl).href; + await pingProc(this.proc, url, "sync"); + } + + constructor( + private globalState: GlobalTestState, + private syncConfig: SyncConfig, + private configFilename: string, + ) { } +} diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts new file mode 100644 index 000000000..632bd5877 --- /dev/null +++ b/packages/taler-harness/src/index.ts @@ -0,0 +1,338 @@ +/* + This file is part of GNU Taler + (C) 2019 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 + */ + +/** + * Imports. + */ +import { deepStrictEqual } from "assert"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { + Amounts, + clk, + Configuration, + decodeCrock, + Logger, + rsaBlind, + setGlobalLogLevelFromString, +} from "@gnu-taler/taler-util"; +import { runBench1 } from "./bench1.js"; +import { runBench2 } from "./bench2.js"; +import { runBench3 } from "./bench3.js"; +import { runEnv1 } from "./env1.js"; +import { GlobalTestState, runTestWithState } from "./harness/harness.js"; +import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; +import { lintExchangeDeployment } from "./lint.js"; +import { runEnvFull } from "./env-full.js"; + +const logger = new Logger("taler-harness:index.ts"); + +process.on("unhandledRejection", (error: any) => { + logger.error("unhandledRejection", error.message); + logger.error("stack", error.stack); + process.exit(2); +}); + +declare const __VERSION__: string; +function printVersion(): void { + console.log(__VERSION__); + process.exit(0); +} + +export const testingCli = clk + .program("testing", { + help: "Command line interface for the GNU Taler test/deployment harness.", + }) + .maybeOption("log", ["-L", "--log"], clk.STRING, { + help: "configure log level (NONE, ..., TRACE)", + onPresentHandler: (x) => { + setGlobalLogLevelFromString(x); + }, + }) + .flag("version", ["-v", "--version"], { + onPresentHandler: printVersion, + }) + .flag("verbose", ["-V", "--verbose"], { + help: "Enable verbose output.", + }); + +const advancedCli = testingCli.subcommand("advancedArgs", "advanced", { + help: "Subcommands for advanced operations (only use if you know what you're doing!).", +}); + +advancedCli + .subcommand("bench1", "bench1", { + help: "Run the 'bench1' benchmark", + }) + .requiredOption("configJson", ["--config-json"], clk.STRING) + .action(async (args) => { + let config: any; + try { + config = JSON.parse(args.bench1.configJson); + } catch (e) { + console.log("Could not parse config JSON"); + } + await runBench1(config); + }); + +advancedCli + .subcommand("bench2", "bench2", { + help: "Run the 'bench2' benchmark", + }) + .requiredOption("configJson", ["--config-json"], clk.STRING) + .action(async (args) => { + let config: any; + try { + config = JSON.parse(args.bench2.configJson); + } catch (e) { + console.log("Could not parse config JSON"); + } + await runBench2(config); + }); + +advancedCli + .subcommand("bench3", "bench3", { + help: "Run the 'bench3' benchmark", + }) + .requiredOption("configJson", ["--config-json"], clk.STRING) + .action(async (args) => { + let config: any; + try { + config = JSON.parse(args.bench3.configJson); + } catch (e) { + console.log("Could not parse config JSON"); + } + await runBench3(config); + }); + +advancedCli + .subcommand("envFull", "env-full", { + help: "Run a test environment for bench1", + }) + .action(async (args) => { + const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env-full-")); + const testState = new GlobalTestState({ + testDir, + }); + await runTestWithState(testState, runEnvFull, "env-full", true); + }); + +advancedCli + .subcommand("env1", "env1", { + help: "Run a test environment for bench1", + }) + .action(async (args) => { + const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-")); + const testState = new GlobalTestState({ + testDir, + }); + await runTestWithState(testState, runEnv1, "env1", true); + }); + +const deploymentCli = testingCli.subcommand("deploymentArgs", "deployment", { + help: "Subcommands for handling GNU Taler deployments.", +}); + +deploymentCli + .subcommand("lintExchange", "lint-exchange", { + help: "Run checks on the exchange deployment.", + }) + .flag("cont", ["--continue"], { + help: "Continue after errors if possible", + }) + .flag("debug", ["--debug"], { + help: "Output extra debug info", + }) + .action(async (args) => { + await lintExchangeDeployment( + args.lintExchange.debug, + args.lintExchange.cont, + ); + }); + +deploymentCli + .subcommand("coincfg", "gen-coin-config", { + help: "Generate a coin/denomination configuration for the exchange.", + }) + .requiredOption("minAmount", ["--min-amount"], clk.STRING, { + help: "Smallest denomination", + }) + .requiredOption("maxAmount", ["--max-amount"], clk.STRING, { + help: "Largest denomination", + }) + .action(async (args) => { + let out = ""; + + const stamp = Math.floor(new Date().getTime() / 1000); + + const min = Amounts.parseOrThrow(args.coincfg.minAmount); + const max = Amounts.parseOrThrow(args.coincfg.maxAmount); + if (min.currency != max.currency) { + console.error("currency mismatch"); + process.exit(1); + } + const currency = min.currency; + let x = min; + let n = 1; + + out += "# Coin configuration for the exchange.\n"; + out += '# Should be placed in "/etc/taler/conf.d/exchange-coins.conf".\n'; + out += "\n"; + + while (Amounts.cmp(x, max) < 0) { + out += `[COIN-${currency}-n${n}-t${stamp}]\n`; + out += `VALUE = ${Amounts.stringify(x)}\n`; + out += `DURATION_WITHDRAW = 7 days\n`; + out += `DURATION_SPEND = 2 years\n`; + out += `DURATION_LEGAL = 6 years\n`; + out += `FEE_WITHDRAW = ${currency}:0\n`; + out += `FEE_DEPOSIT = ${Amounts.stringify(min)}\n`; + out += `FEE_REFRESH = ${currency}:0\n`; + out += `FEE_REFUND = ${currency}:0\n`; + out += `RSA_KEYSIZE = 2048\n`; + out += "\n"; + x = Amounts.add(x, x).amount; + n++; + } + + console.log(out); + }); + +const deploymentConfigCli = deploymentCli.subcommand("configArgs", "config", { + help: "Subcommands the Taler configuration.", +}); + +deploymentConfigCli + .subcommand("show", "show") + .flag("diagnostics", ["-d", "--diagnostics"]) + .maybeArgument("cfgfile", clk.STRING, {}) + .action(async (args) => { + const cfg = Configuration.load(args.show.cfgfile); + console.log( + cfg.stringify({ + diagnostics: args.show.diagnostics, + }), + ); + }); + + +testingCli.subcommand("logtest", "logtest").action(async (args) => { + logger.trace("This is a trace message."); + logger.info("This is an info message."); + logger.warn("This is an warning message."); + logger.error("This is an error message."); +}); + +testingCli + .subcommand("listIntegrationtests", "list-integrationtests") + .action(async (args) => { + for (const t of getTestInfo()) { + let s = t.name; + if (t.suites.length > 0) { + s += ` (suites: ${t.suites.join(",")})`; + } + if (t.excludeByDefault) { + s += ` [excluded by default]`; + } + console.log(s); + } + }); + +testingCli + .subcommand("runIntegrationtests", "run-integrationtests") + .maybeArgument("pattern", clk.STRING, { + help: "Glob pattern to select which tests to run", + }) + .maybeOption("suites", ["--suites"], clk.STRING, { + help: "Only run selected suites (comma-separated list)", + }) + .flag("dryRun", ["--dry"], { + help: "Only print tests that will be selected to run.", + }) + .flag("quiet", ["--quiet"], { + help: "Produce less output.", + }) + .action(async (args) => { + await runTests({ + includePattern: args.runIntegrationtests.pattern, + suiteSpec: args.runIntegrationtests.suites, + dryRun: args.runIntegrationtests.dryRun, + verbosity: args.runIntegrationtests.quiet ? 0 : 1, + }); + }); + +async function read(stream: NodeJS.ReadStream) { + const chunks = []; + for await (const chunk of stream) chunks.push(chunk); + return Buffer.concat(chunks).toString("utf8"); +} + +testingCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => { + const data = await read(process.stdin); + + const lines = data.match(/[^\r\n]+/g); + + if (!lines) { + throw Error("can't split lines"); + } + + const vals: Record = {}; + + let inBlindSigningSection = false; + + for (const line of lines) { + if (line === "blind signing:") { + inBlindSigningSection = true; + continue; + } + if (line[0] !== " ") { + inBlindSigningSection = false; + continue; + } + if (inBlindSigningSection) { + const m = line.match(/ (\w+) (\w+)/); + if (!m) { + console.log("bad format"); + process.exit(2); + } + vals[m[1]] = m[2]; + } + } + + console.log(vals); + + const req = (k: string) => { + if (!vals[k]) { + throw Error(`no value for ${k}`); + } + return decodeCrock(vals[k]); + }; + + const myBm = rsaBlind( + req("message_hash"), + req("blinding_key_secret"), + req("rsa_public_key"), + ); + + deepStrictEqual(req("blinded_message"), myBm); + + console.log("check passed!"); +}); + +export function main() { + testingCli.run(); +} 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 { + // 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 + */ + +/** + * 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 { + 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 + */ + +/** + * 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 { + 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 { + 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 = { + 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 { + 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 { + 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 { + 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 + */ + +/** + * 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 { + 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 { + 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 { + const { wallet, exchange, merchant } = req; + const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {}); + console.log(coinDump); + const usedDenomHashes = new Set(); + 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 { + 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 + */ + +/** + * 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 { + 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 + */ + +/** + * 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 + */ + +/** + * 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 => { + 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 { + 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +/** + * 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 + */ + +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; + 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 | 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 = 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); + }); +} diff --git a/packages/taler-harness/src/lint.ts b/packages/taler-harness/src/lint.ts new file mode 100644 index 000000000..49fb9dc86 --- /dev/null +++ b/packages/taler-harness/src/lint.ts @@ -0,0 +1,534 @@ +/* + 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 + */ + +/** + * The deployment linter implements checks for a deployment + * of the GNU Taler exchange. It is meant to help sysadmins + * when setting up an exchange. + * + * The linter does checks in the configuration and uses + * various tools of the exchange in test mode (-t). + * + * To be able to run the tools as the right user, the linter should be + * run as root. + * + * @author Florian Dold + */ + +/** + * Imports. + */ +import { + codecForExchangeKeysJson, + codecForKeysManagementResponse, + Configuration, + decodeCrock, +} from "@gnu-taler/taler-util"; +import { + NodeHttpLib, + readSuccessResponseJsonOrThrow, +} from "@gnu-taler/taler-wallet-core"; +import { URL } from "url"; +import { spawn } from "child_process"; +import { delayMs } from "./harness/harness.js"; + +interface BasicConf { + mainCurrency: string; +} + +interface PubkeyConf { + masterPublicKey: string; +} + +const httpLib = new NodeHttpLib(); + +interface ShellResult { + stdout: string; + stderr: string; + status: number; +} + +interface LintContext { + /** + * Be more verbose. + */ + verbose: boolean; + + /** + * Always continue even after errors. + */ + cont: boolean; + + cfg: Configuration; + + numErr: number; +} + +/** + * Run a shell command, return stdout. + */ +export async function sh( + context: LintContext, + command: string, + env: { [index: string]: string | undefined } = process.env, +): Promise { + if (context.verbose) { + console.log("executing command:", command); + } + return new Promise((resolve, reject) => { + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + const proc = spawn(command, { + stdio: ["inherit", "pipe", "pipe"], + shell: true, + env: env, + }); + proc.stdout.on("data", (x) => { + if (x instanceof Buffer) { + stdoutChunks.push(x); + } else { + throw Error("unexpected data chunk type"); + } + }); + proc.stderr.on("data", (x) => { + if (x instanceof Buffer) { + stderrChunks.push(x); + } else { + throw Error("unexpected data chunk type"); + } + }); + proc.on("exit", (code, signal) => { + if (code != 0 && context.verbose) { + console.log(`child process exited (${code} / ${signal})`); + } + const bOut = Buffer.concat(stdoutChunks).toString("utf-8"); + const bErr = Buffer.concat(stderrChunks).toString("utf-8"); + resolve({ + status: code ?? -1, + stderr: bErr, + stdout: bOut, + }); + }); + proc.on("error", () => { + reject(Error("Child process had error")); + }); + }); +} + +function checkBasicConf(context: LintContext): BasicConf { + const cfg = context.cfg; + const currencyEntry = cfg.getString("taler", "currency"); + let mainCurrency: string | undefined; + + if (!currencyEntry.value) { + context.numErr++; + console.log("error: currency not defined in section TALER option CURRENCY"); + console.log("Aborting further checks."); + process.exit(1); + } else { + mainCurrency = currencyEntry.value.toUpperCase(); + } + + if (mainCurrency === "KUDOS") { + console.log( + "warning: section TALER option CURRENCY contains toy currency value KUDOS", + ); + } + + const roundUnit = cfg.getAmount("taler", "currency_round_unit"); + const ru = roundUnit.required(); + if (ru.currency.toLowerCase() != mainCurrency.toLowerCase()) { + context.numErr++; + console.log( + "error: [TALER]/CURRENCY_ROUND_UNIT: currency does not match main currency", + ); + } + return { mainCurrency }; +} + +function checkCoinConfig(context: LintContext, basic: BasicConf): void { + const cfg = context.cfg; + const coinPrefix1 = "COIN_"; + const coinPrefix2 = "COIN-"; + let numCoins = 0; + + for (const secName of cfg.getSectionNames()) { + if (!(secName.startsWith(coinPrefix1) || secName.startsWith(coinPrefix2))) { + continue; + } + numCoins++; + + // FIXME: check that section is well-formed + } + + if (numCoins == 0) { + context.numErr++; + console.log( + "error: no coin denomination configured, please configure [coin-*] sections", + ); + } +} + +async function checkWireConfig(context: LintContext): Promise { + const cfg = context.cfg; + const accountPrefix = "EXCHANGE-ACCOUNT-"; + const accountCredentialsPrefix = "EXCHANGE-ACCOUNTCREDENTIALS-"; + + let accounts = new Set(); + let credentials = new Set(); + + for (const secName of cfg.getSectionNames()) { + if (secName.startsWith(accountPrefix)) { + accounts.add(secName.slice(accountPrefix.length)); + // FIXME: check settings + } + + if (secName.startsWith(accountCredentialsPrefix)) { + credentials.add(secName.slice(accountCredentialsPrefix.length)); + // FIXME: check settings + } + } + + if (accounts.size === 0) { + context.numErr++; + console.log( + "error: No accounts configured (no sections EXCHANGE-ACCOUNT-*).", + ); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + + for (const acc of accounts) { + if (!credentials.has(acc)) { + console.log( + `warning: no credentials configured for exchange-account-${acc}`, + ); + } + } + + for (const acc of accounts) { + // test credit history + { + const res = await sh( + context, + "su -l --shell /bin/sh " + + `-c 'taler-exchange-wire-gateway-client -s exchange-accountcredentials-${acc} --credit-history' ` + + "taler-exchange-wire", + ); + if (res.status != 0) { + context.numErr++; + console.log(res.stdout); + console.log(res.stderr); + console.log( + "error: Could not run taler-exchange-wire-gateway-client. Please review logs above.", + ); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + } + } + + // TWG client + { + const res = await sh( + context, + `su -l --shell /bin/sh -c 'taler-exchange-wirewatch -t' taler-exchange-wire`, + ); + if (res.status != 0) { + context.numErr++; + console.log(res.stdout); + console.log(res.stderr); + console.log("error: Could not run wirewatch. Please review logs above."); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + } + + // Wirewatch + { + const res = await sh( + context, + `su -l --shell /bin/sh -c 'taler-exchange-wirewatch -t' taler-exchange-wire`, + ); + if (res.status != 0) { + context.numErr++; + console.log(res.stdout); + console.log(res.stderr); + console.log("error: Could not run wirewatch. Please review logs above."); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + } + + // Closer + { + const res = await sh( + context, + `su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`, + ); + if (res.status != 0) { + context.numErr++; + console.log(res.stdout); + console.log(res.stderr); + console.log("error: Could not run closer. Please review logs above."); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + } +} + +async function checkAggregatorConfig(context: LintContext) { + const res = await sh( + context, + "su -l --shell /bin/sh -c 'taler-exchange-aggregator -t' taler-exchange-aggregator", + ); + if (res.status != 0) { + context.numErr++; + console.log(res.stdout); + console.log(res.stderr); + console.log("error: Could not run aggregator. Please review logs above."); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } +} + +async function checkCloserConfig(context: LintContext) { + const res = await sh( + context, + `su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`, + ); + if (res.status != 0) { + context.numErr++; + console.log(res.stdout); + console.log(res.stderr); + console.log("error: Could not run closer. Please review logs above."); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } +} + +function checkMasterPublicKeyConfig(context: LintContext): PubkeyConf { + const cfg = context.cfg; + const pub = cfg.getString("exchange", "master_public_key"); + + const pubDecoded = decodeCrock(pub.required()); + + if (pubDecoded.length != 32) { + context.numErr++; + console.log("error: invalid master public key"); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + + return { + masterPublicKey: pub.required(), + }; +} + +export async function checkExchangeHttpd( + context: LintContext, + pubConf: PubkeyConf, +): Promise { + const cfg = context.cfg; + const baseUrlEntry = cfg.getString("exchange", "base_url"); + + if (!baseUrlEntry.isDefined) { + context.numErr++; + console.log( + "error: configuration needs to specify section EXCHANGE option BASE_URL", + ); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + + const baseUrl = baseUrlEntry.required(); + + if (!baseUrl.startsWith("http")) { + context.numErr++; + console.log( + "error: section EXCHANGE option BASE_URL needs to be an http or https URL", + ); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + + if (!baseUrl.endsWith("/")) { + context.numErr++; + console.log( + "error: section EXCHANGE option BASE_URL needs to end with a slash", + ); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + + if (!baseUrl.startsWith("https://")) { + console.log( + "warning: section EXCHANGE option BASE_URL: it is recommended to serve the exchange via HTTPS", + ); + } + + { + const mgmtUrl = new URL("management/keys", baseUrl); + const resp = await httpLib.get(mgmtUrl.href); + + const futureKeys = await readSuccessResponseJsonOrThrow( + resp, + codecForKeysManagementResponse(), + ); + + if (futureKeys.future_denoms.length > 0) { + console.log( + `warning: exchange has denomination keys that need to be signed by the offline signing procedure`, + ); + } + + if (futureKeys.future_signkeys.length > 0) { + console.log( + `warning: exchange has signing keys that need to be signed by the offline signing procedure`, + ); + } + } + + // Check if we can use /keys already + { + const keysUrl = new URL("keys", baseUrl); + + const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); + + if (!resp) { + context.numErr++; + console.log( + "error: request to /keys timed out. " + + "Make sure to sign and upload denomination and signing keys " + + "with taler-exchange-offline.", + ); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } else { + const keys = await readSuccessResponseJsonOrThrow( + resp, + codecForExchangeKeysJson(), + ); + + if (keys.master_public_key !== pubConf.masterPublicKey) { + context.numErr++; + console.log( + "error: master public key of exchange does not match public key of live exchange", + ); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } + } + } + + // Check /wire + { + const keysUrl = new URL("wire", baseUrl); + + const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); + + if (!resp) { + context.numErr++; + console.log( + "error: request to /wire timed out. " + + "Make sure to sign and upload accounts and wire fees " + + "using the taler-exchange-offline tool.", + ); + if (!context.cont) { + console.log("Aborting further checks."); + process.exit(1); + } + } else { + if (resp.status !== 200) { + console.log( + "error: Can't access exchange /wire. Please check " + + "the logs of taler-exchange-httpd for further information.", + ); + } + } + } +} + +/** + * Do some basic checks in the configuration of a Taler deployment. + */ +export async function lintExchangeDeployment( + verbose: boolean, + cont: boolean, +): Promise { + if (process.getuid!() != 0) { + console.log( + "warning: the exchange deployment linter is designed to be run as root", + ); + } + + const cfg = Configuration.load(); + + const context: LintContext = { + cont, + verbose, + cfg, + numErr: 0, + }; + + const basic = checkBasicConf(context); + + checkCoinConfig(context, basic); + + await checkWireConfig(context); + + await checkAggregatorConfig(context); + + await checkCloserConfig(context); + + const pubConf = checkMasterPublicKeyConfig(context); + + await checkExchangeHttpd(context, pubConf); + + if (context.numErr == 0) { + console.log("Linting completed without errors."); + process.exit(0); + } else { + console.log(`Linting completed with ${context.numErr} errors.`); + process.exit(1); + } +} diff --git a/packages/taler-harness/tsconfig.json b/packages/taler-harness/tsconfig.json new file mode 100644 index 000000000..447d3f946 --- /dev/null +++ b/packages/taler-harness/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "composite": true, + "target": "ES2018", + "module": "ESNext", + "moduleResolution": "Node16", + "sourceMap": true, + "lib": ["es6"], + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "strict": true, + "strictPropertyInitialization": false, + "outDir": "lib", + "noImplicitAny": true, + "noImplicitThis": true, + "incremental": true, + "esModuleInterop": true, + "importHelpers": true, + "rootDir": "src", + "baseUrl": "./src", + "typeRoots": ["./node_modules/@types"] + }, + "include": ["src/**/*"], + "references": [ + { + "path": "../taler-wallet-core/" + }, + { + "path": "../taler-util/" + } + ] +} diff --git a/packages/taler-wallet-cli/assets/.gitkeep b/packages/taler-wallet-cli/assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/taler-wallet-cli/build.mjs b/packages/taler-wallet-cli/build.mjs index b6b3f4a16..14b626815 100755 --- a/packages/taler-wallet-cli/build.mjs +++ b/packages/taler-wallet-cli/build.mjs @@ -53,17 +53,15 @@ function git_hash() { export const buildConfig = { entryPoints: ["src/index.ts"], - outfile: "dist/taler-wallet-cli.cjs", + outfile: "dist/taler-wallet-cli.mjs", bundle: true, minify: false, target: [ - 'es6' + 'es2020' ], - format: 'cjs', - platform: 'node', + format: 'esm', + platform: 'neutral', sourcemap: true, - jsxFactory: 'h', - jsxFragment: 'Fragment', define: { '__VERSION__': `"${_package.version}"`, '__GIT_HASH__': `"${GIT_HASH}"`, @@ -76,4 +74,3 @@ esbuild console.log(e) process.exit(1) }); - diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json index 455c5d0cb..8069cc5dd 100644 --- a/packages/taler-wallet-cli/package.json +++ b/packages/taler-wallet-cli/package.json @@ -35,7 +35,7 @@ "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-replace": "^4.0.0", - "@types/node": "^18.8.5", + "@types/node": "^18.11.17", "prettier": "^2.5.1", "rimraf": "^3.0.2", "rollup": "^2.79.0", diff --git a/packages/taler-wallet-cli/src/assets.ts b/packages/taler-wallet-cli/src/assets.ts deleted file mode 100644 index 8e9306780..000000000 --- a/packages/taler-wallet-cli/src/assets.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - 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 - */ - -/** - * Imports. - */ -import path from "path"; -import fs from "fs"; - -const assetFileUrl = import.meta.url; - -/** - * Resolve an asset name into an absolute filename. - * - * The asset file should be placed in the "assets" directory - * at the top level of the package (i.e. next to package.json). - */ -export function resolveAsset(name: string): string { - const n = path.basename(assetFileUrl); - const d = path.dirname(assetFileUrl); - let assetPath: string; - // Currently both asset paths are the same. - // This might change if the file that contains "resolveAsset" - // ever moves. Thus, we're keeping the case distinction. - if (n.endsWith("assets.js")) { - // We're not bundled. Path is relative to the current file. - assetPath = path.resolve(path.join(d, "..", "assets", name)); - } else if (n.endsWith("taler-wallet-cli.js")) { - // We're bundled. Currently, this path is the same - // FIXME: Take into account some ASSETS environment variable? - assetPath = path.resolve(path.join(d, "..", "assets", name)); - } else { - throw Error("Can't resolve asset (unknown)"); - } - if (!fs.existsSync(assetPath)) { - throw Error(`Asset '${name} not found'`); - } - return assetPath; -} diff --git a/packages/taler-wallet-cli/src/bench1.ts b/packages/taler-wallet-cli/src/bench1.ts deleted file mode 100644 index 84786d25a..000000000 --- a/packages/taler-wallet-cli/src/bench1.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - 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 - */ - -/** - * Imports. - */ -import { - buildCodecForObject, - codecForNumber, - codecForString, - codecForBoolean, - codecOptional, - j2s, - Logger, -} from "@gnu-taler/taler-util"; -import { - getDefaultNodeWallet2, - NodeHttpLib, - WalletApiOperation, - Wallet, - AccessStats, -} from "@gnu-taler/taler-wallet-core"; - -/** - * Entry point for the benchmark. - * - * The benchmark runs against an existing Taler deployment and does not - * set up its own services. - */ -export async function runBench1(configJson: any): Promise { - const logger = new Logger("Bench1"); - - // Validate the configuration file for this benchmark. - const b1conf = codecForBench1Config().decode(configJson); - - const myHttpLib = new NodeHttpLib(); - myHttpLib.setThrottling(false); - - const numIter = b1conf.iterations ?? 1; - const numDeposits = b1conf.deposits ?? 5; - const restartWallet = b1conf.restartAfter ?? 20; - - const withdrawOnly = b1conf.withdrawOnly ?? false; - const withdrawAmount = (numDeposits + 1) * 10; - - logger.info( - `Starting Benchmark iterations=${numIter} deposits=${numDeposits}`, - ); - - const trustExchange = !!process.env["TALER_WALLET_INSECURE_TRUST_EXCHANGE"]; - if (trustExchange) { - logger.info("trusting exchange (not validating signatures)"); - } else { - logger.info("not trusting exchange (validating signatures)"); - } - const batchWithdrawal = !!process.env["TALER_WALLET_BATCH_WITHDRAWAL"]; - - let wallet = {} as Wallet; - let getDbStats: () => AccessStats; - - for (let i = 0; i < numIter; i++) { - // Create a new wallet in each iteration - // otherwise the TPS go down - // my assumption is that the in-memory db file gets too large - if (i % restartWallet == 0) { - if (Object.keys(wallet).length !== 0) { - wallet.stop(); - console.log("wallet DB stats", j2s(getDbStats!())); - } - - const res = await getDefaultNodeWallet2({ - // No persistent DB storage. - persistentStoragePath: undefined, - httpLib: myHttpLib, - }); - wallet = res.wallet; - getDbStats = res.getDbStats; - if (trustExchange) { - wallet.setInsecureTrustExchange(); - } - wallet.setBatchWithdrawal(batchWithdrawal); - await wallet.client.call(WalletApiOperation.InitWallet, {}); - } - - logger.trace(`Starting withdrawal amount=${withdrawAmount}`); - let start = Date.now(); - - await wallet.client.call(WalletApiOperation.WithdrawFakebank, { - amount: b1conf.currency + ":" + withdrawAmount, - bank: b1conf.bank, - exchange: b1conf.exchange, - }); - - await wallet.runTaskLoop({ - stopWhenDone: true, - }); - - logger.info( - `Finished withdrawal amount=${withdrawAmount} time=${Date.now() - start}`, - ); - - if (!withdrawOnly) { - for (let i = 0; i < numDeposits; i++) { - logger.trace(`Starting deposit amount=10`); - start = Date.now(); - - await wallet.client.call(WalletApiOperation.CreateDepositGroup, { - amount: b1conf.currency + ":10", - depositPaytoUri: b1conf.payto, - }); - - await wallet.runTaskLoop({ - stopWhenDone: true, - }); - - logger.info(`Finished deposit amount=10 time=${Date.now() - start}`); - } - } - } - - wallet.stop(); - console.log("wallet DB stats", j2s(getDbStats!())); -} - -/** - * Format of the configuration file passed to the benchmark - */ -interface Bench1Config { - /** - * Base URL of the bank. - */ - bank: string; - - /** - * Payto url for deposits. - */ - payto: string; - - /** - * Base URL of the exchange. - */ - exchange: string; - - /** - * How many withdraw/deposit iterations should be made? - * Defaults to 1. - */ - iterations?: number; - - currency: string; - - deposits?: number; - - /** - * How any iterations run until the wallet db gets purged - * Defaults to 20. - */ - restartAfter?: number; - - withdrawOnly?: boolean; -} - -/** - * Schema validation codec for Bench1Config. - */ -const codecForBench1Config = () => - buildCodecForObject() - .property("bank", codecForString()) - .property("payto", codecForString()) - .property("exchange", codecForString()) - .property("iterations", codecOptional(codecForNumber())) - .property("deposits", codecOptional(codecForNumber())) - .property("currency", codecForString()) - .property("restartAfter", codecOptional(codecForNumber())) - .property("withdrawOnly", codecOptional(codecForBoolean())) - .build("Bench1Config"); diff --git a/packages/taler-wallet-cli/src/bench2.ts b/packages/taler-wallet-cli/src/bench2.ts deleted file mode 100644 index 196737436..000000000 --- a/packages/taler-wallet-cli/src/bench2.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* - 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 - */ - -/** - * Imports. - */ -import { - buildCodecForObject, - codecForNumber, - codecForString, - codecOptional, - Logger, -} from "@gnu-taler/taler-util"; -import { - checkReserve, - createFakebankReserve, - CryptoDispatcher, - depositCoin, - downloadExchangeInfo, - findDenomOrThrow, - NodeHttpLib, - refreshCoin, - SynchronousCryptoWorkerFactoryNode, - withdrawCoin, -} from "@gnu-taler/taler-wallet-core"; - -/** - * Entry point for the benchmark. - * - * The benchmark runs against an existing Taler deployment and does not - * set up its own services. - */ -export async function runBench2(configJson: any): Promise { - const logger = new Logger("Bench2"); - - // Validate the configuration file for this benchmark. - const benchConf = codecForBench2Config().decode(configJson); - const curr = benchConf.currency; - const cryptoDisp = new CryptoDispatcher(new SynchronousCryptoWorkerFactoryNode()); - const cryptoApi = cryptoDisp.cryptoApi; - - const http = new NodeHttpLib(); - http.setThrottling(false); - - const numIter = benchConf.iterations ?? 1; - const numDeposits = benchConf.deposits ?? 5; - - const reserveAmount = (numDeposits + 1) * 10; - - for (let i = 0; i < numIter; i++) { - const exchangeInfo = await downloadExchangeInfo(benchConf.exchange, http); - - const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); - - console.log("creating fakebank reserve"); - - await createFakebankReserve({ - amount: `${curr}:${reserveAmount}`, - exchangeInfo, - fakebankBaseUrl: benchConf.bank, - http, - reservePub: reserveKeyPair.pub, - }); - - console.log("waiting for reserve"); - - await checkReserve(http, benchConf.exchange, reserveKeyPair.pub); - - console.log("reserve found"); - - const d1 = findDenomOrThrow(exchangeInfo, `${curr}:8`); - - for (let j = 0; j < numDeposits; j++) { - console.log("withdrawing coin"); - const coin = await withdrawCoin({ - http, - cryptoApi, - reserveKeyPair: { - reservePriv: reserveKeyPair.priv, - reservePub: reserveKeyPair.pub, - }, - denom: d1, - exchangeBaseUrl: benchConf.exchange, - }); - - console.log("depositing coin"); - - await depositCoin({ - amount: `${curr}:4`, - coin: coin, - cryptoApi, - exchangeBaseUrl: benchConf.exchange, - http, - depositPayto: benchConf.payto, - }); - - const refreshDenoms = [ - findDenomOrThrow(exchangeInfo, `${curr}:1`), - findDenomOrThrow(exchangeInfo, `${curr}:1`), - ]; - - console.log("refreshing coin"); - - await refreshCoin({ - oldCoin: coin, - cryptoApi, - http, - newDenoms: refreshDenoms, - }); - - console.log("refresh done"); - } - } -} - -/** - * Format of the configuration file passed to the benchmark - */ -interface Bench2Config { - /** - * Base URL of the bank. - */ - bank: string; - - /** - * Payto url for deposits. - */ - payto: string; - - /** - * Base URL of the exchange. - */ - exchange: string; - - /** - * How many withdraw/deposit iterations should be made? - * Defaults to 1. - */ - iterations?: number; - - currency: string; - - deposits?: number; -} - -/** - * Schema validation codec for Bench1Config. - */ -const codecForBench2Config = () => - buildCodecForObject() - .property("bank", codecForString()) - .property("payto", codecForString()) - .property("exchange", codecForString()) - .property("iterations", codecOptional(codecForNumber())) - .property("deposits", codecOptional(codecForNumber())) - .property("currency", codecForString()) - .build("Bench2Config"); diff --git a/packages/taler-wallet-cli/src/bench3.ts b/packages/taler-wallet-cli/src/bench3.ts deleted file mode 100644 index 6041c525c..000000000 --- a/packages/taler-wallet-cli/src/bench3.ts +++ /dev/null @@ -1,205 +0,0 @@ -/* - 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 - */ - -/** - * Imports. - */ -import { - buildCodecForObject, - codecForNumber, - codecForString, - codecOptional, - j2s, - Logger, -} from "@gnu-taler/taler-util"; -import { - getDefaultNodeWallet2, - NodeHttpLib, - WalletApiOperation, - Wallet, - AccessStats, -} from "@gnu-taler/taler-wallet-core"; -import benchMerchantIDGenerator from "./benchMerchantIDGenerator.js"; - -/** - * Entry point for the benchmark. - * - * The benchmark runs against an existing Taler deployment and does not - * set up its own services. - */ -export async function runBench3(configJson: any): Promise { - const logger = new Logger("Bench3"); - - // Validate the configuration file for this benchmark. - const b3conf = codecForBench3Config().decode(configJson); - - if (!b3conf.paytoTemplate.includes("${id")) { - throw new Error("Payto template url must contain '${id}' placeholder"); - } - - const myHttpLib = new NodeHttpLib(); - myHttpLib.setThrottling(false); - - const numIter = b3conf.iterations ?? 1; - const numDeposits = b3conf.deposits ?? 5; - const restartWallet = b3conf.restartAfter ?? 20; - - const withdrawAmount = (numDeposits + 1) * 10; - - const IDGenerator = benchMerchantIDGenerator(b3conf.randomAlg, b3conf.numMerchants ?? 100); - - logger.info( - `Starting Benchmark iterations=${numIter} deposits=${numDeposits} with ${b3conf.randomAlg} merchant selection`, - ); - - const trustExchange = !!process.env["TALER_WALLET_INSECURE_TRUST_EXCHANGE"]; - if (trustExchange) { - logger.info("trusting exchange (not validating signatures)"); - } else { - logger.info("not trusting exchange (validating signatures)"); - } - const batchWithdrawal = !!process.env["TALER_WALLET_BATCH_WITHDRAWAL"]; - - let wallet = {} as Wallet; - let getDbStats: () => AccessStats; - - for (let i = 0; i < numIter; i++) { - // Create a new wallet in each iteration - // otherwise the TPS go down - // my assumption is that the in-memory db file gets too large - if (i % restartWallet == 0) { - if (Object.keys(wallet).length !== 0) { - wallet.stop(); - console.log("wallet DB stats", j2s(getDbStats!())); - } - - const res = await getDefaultNodeWallet2({ - // No persistent DB storage. - persistentStoragePath: undefined, - httpLib: myHttpLib, - }); - wallet = res.wallet; - getDbStats = res.getDbStats; - if (trustExchange) { - wallet.setInsecureTrustExchange(); - } - wallet.setBatchWithdrawal(batchWithdrawal); - await wallet.client.call(WalletApiOperation.InitWallet, {}); - } - - logger.trace(`Starting withdrawal amount=${withdrawAmount}`); - let start = Date.now(); - - await wallet.client.call(WalletApiOperation.WithdrawFakebank, { - amount: b3conf.currency + ":" + withdrawAmount, - bank: b3conf.bank, - exchange: b3conf.exchange, - }); - - await wallet.runTaskLoop({ - stopWhenDone: true, - }); - - logger.info( - `Finished withdrawal amount=${withdrawAmount} time=${Date.now() - start}`, - ); - - for (let i = 0; i < numDeposits; i++) { - logger.trace(`Starting deposit amount=10`); - start = Date.now(); - - let merchID = IDGenerator.getRandomMerchantID(); - let payto = b3conf.paytoTemplate.replace("${id}", merchID.toString()); - - await wallet.client.call(WalletApiOperation.CreateDepositGroup, { - amount: b3conf.currency + ":10", - depositPaytoUri: payto, - }); - - await wallet.runTaskLoop({ - stopWhenDone: true, - }); - - logger.info(`Finished deposit amount=10 time=${Date.now() - start}`); - } - } - - wallet.stop(); - console.log("wallet DB stats", j2s(getDbStats!())); -} - -/** - * Format of the configuration file passed to the benchmark - */ -interface Bench3Config { - /** - * Base URL of the bank. - */ - bank: string; - - /** - * Payto url template for deposits, must contain '${id}' for replacements. - */ - paytoTemplate: string; - - /** - * Base URL of the exchange. - */ - exchange: string; - - /** - * How many withdraw/deposit iterations should be made? - * Defaults to 1. - */ - iterations?: number; - - currency: string; - - deposits?: number; - - /** - * How any iterations run until the wallet db gets purged - * Defaults to 20. - */ - restartAfter?: number; - - /** - * Number of merchants to select from randomly - */ - numMerchants?: number; - - /** - * Which random generator to use. - * Possible values: 'zipf', 'rand' - */ - randomAlg: string; -} - -/** - * Schema validation codec for Bench1Config. - */ -const codecForBench3Config = () => - buildCodecForObject() - .property("bank", codecForString()) - .property("paytoTemplate", codecForString()) - .property("numMerchants", codecOptional(codecForNumber())) - .property("randomAlg", codecForString()) - .property("exchange", codecForString()) - .property("iterations", codecOptional(codecForNumber())) - .property("deposits", codecOptional(codecForNumber())) - .property("currency", codecForString()) - .property("restartAfter", codecOptional(codecForNumber())) - .build("Bench1Config"); diff --git a/packages/taler-wallet-cli/src/benchMerchantIDGenerator.ts b/packages/taler-wallet-cli/src/benchMerchantIDGenerator.ts deleted file mode 100644 index b83c12bb8..000000000 --- a/packages/taler-wallet-cli/src/benchMerchantIDGenerator.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - 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 - - @author: Boss Marco - */ - -const getRandomInt = function(max: number) { - return Math.floor(Math.random() * max); -} - -abstract class BenchMerchantIDGenerator { - abstract getRandomMerchantID(): number -} - -class ZipfGenerator extends BenchMerchantIDGenerator { - - weights: number[]; - total_weight: number; - - constructor(numMerchants: number) { - super(); - this.weights = new Array(numMerchants); - for (var i = 0; i < this.weights.length; i++) { - /* we use integers (floor), make sure we have big enough values - * by multiplying with - * numMerchants again */ - this.weights[i] = Math.floor((numMerchants/(i+1)) * numMerchants); - } - this.total_weight = this.weights.reduce((p, n) => p + n); - } - - getRandomMerchantID(): number { - let random = getRandomInt(this.total_weight); - let current = 0; - - for (var i = 0; i < this.weights.length; i++) { - current += this.weights[i]; - if (random <= current) { - return i+1; - } - } - - /* should never come here */ - return getRandomInt(this.weights.length); - } -} - -class RandomGenerator extends BenchMerchantIDGenerator { - - max: number - - constructor(numMerchants: number) { - super(); - this.max = numMerchants - } - - getRandomMerchantID() { - return getRandomInt(this.max); - } -} - -export default function(type: string, maxID: number): BenchMerchantIDGenerator { - switch (type) { - case "zipf": - return new ZipfGenerator(maxID); - case "rand": - return new RandomGenerator(maxID); - default: - throw new Error("Valid types are 'zipf' and 'rand'"); - } -} diff --git a/packages/taler-wallet-cli/src/env-full.ts b/packages/taler-wallet-cli/src/env-full.ts deleted file mode 100644 index 3a684db0b..000000000 --- a/packages/taler-wallet-cli/src/env-full.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - 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 - */ - -/** - * Imports. - */ -import { Duration, j2s, URL } from "@gnu-taler/taler-util"; -import { CoinConfig, defaultCoinConfig } from "./harness/denomStructures.js"; -import { - GlobalTestState, - setupDb, - ExchangeService, - FakebankService, - MerchantService, - getPayto, -} from "./harness/harness.js"; - -/** - * Entry point for the full Taler test environment. - */ -export async function runEnvFull(t: GlobalTestState): Promise { - 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", - ); - console.log("exchange bank account", j2s(exchangeBankAccount)); - 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(); - - await merchant.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [getPayto("merchant-default")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: [getPayto("minst1")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); - - console.log("setup done!"); -} diff --git a/packages/taler-wallet-cli/src/env1.ts b/packages/taler-wallet-cli/src/env1.ts deleted file mode 100644 index aec0b7b8f..000000000 --- a/packages/taler-wallet-cli/src/env1.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - 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 - */ - -/** - * Imports. - */ -import { URL } from "@gnu-taler/taler-util"; -import { CoinConfig, defaultCoinConfig } from "./harness/denomStructures.js"; -import { - GlobalTestState, - setupDb, - ExchangeService, - FakebankService, -} from "./harness/harness.js"; - -/** - * Entry point for the benchmark. - * - * The benchmark runs against an existing Taler deployment and does not - * set up its own services. - */ -export async function runEnv1(t: GlobalTestState): Promise { - const db = await setupDb(t); - - const bank = await FakebankService.create(t, { - currency: "TESTKUDOS", - httpPort: 8082, - allowRegistrations: true, - 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!"); -} diff --git a/packages/taler-wallet-cli/src/harness/denomStructures.ts b/packages/taler-wallet-cli/src/harness/denomStructures.ts deleted file mode 100644 index b12857c7e..000000000 --- a/packages/taler-wallet-cli/src/harness/denomStructures.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - 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 - */ - -export interface CoinCoinfigCommon { - name: string; - value: string; - durationWithdraw: string; - durationSpend: string; - durationLegal: string; - feeWithdraw: string; - feeDeposit: string; - feeRefresh: string; - feeRefund: string; - ageRestricted?: boolean; -} - -export interface CoinConfigRsa extends CoinCoinfigCommon { - cipher: "RSA"; - rsaKeySize: number; -} - -/** - * Clause Schnorr coin config. - */ -export interface CoinConfigCs extends CoinCoinfigCommon { - cipher: "CS"; -} - -export type CoinConfig = CoinConfigRsa | CoinConfigCs; - -const coinRsaCommon = { - cipher: "RSA" as const, - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, -}; - -export const coin_ct1 = (curr: string): CoinConfig => ({ - ...coinRsaCommon, - name: `${curr}_ct1`, - value: `${curr}:0.01`, - feeDeposit: `${curr}:0.00`, - feeRefresh: `${curr}:0.01`, - feeRefund: `${curr}:0.00`, - feeWithdraw: `${curr}:0.01`, -}); - -export const coin_ct10 = (curr: string): CoinConfig => ({ - ...coinRsaCommon, - name: `${curr}_ct10`, - value: `${curr}:0.10`, - feeDeposit: `${curr}:0.01`, - feeRefresh: `${curr}:0.01`, - feeRefund: `${curr}:0.00`, - feeWithdraw: `${curr}:0.01`, -}); - -export const coin_u1 = (curr: string): CoinConfig => ({ - ...coinRsaCommon, - name: `${curr}_u1`, - value: `${curr}:1`, - feeDeposit: `${curr}:0.02`, - feeRefresh: `${curr}:0.02`, - feeRefund: `${curr}:0.02`, - feeWithdraw: `${curr}:0.02`, -}); - -export const coin_u2 = (curr: string): CoinConfig => ({ - ...coinRsaCommon, - name: `${curr}_u2`, - value: `${curr}:2`, - feeDeposit: `${curr}:0.02`, - feeRefresh: `${curr}:0.02`, - feeRefund: `${curr}:0.02`, - feeWithdraw: `${curr}:0.02`, -}); - -export const coin_u4 = (curr: string): CoinConfig => ({ - ...coinRsaCommon, - name: `${curr}_u4`, - value: `${curr}:4`, - feeDeposit: `${curr}:0.02`, - feeRefresh: `${curr}:0.02`, - feeRefund: `${curr}:0.02`, - feeWithdraw: `${curr}:0.02`, -}); - -export const coin_u8 = (curr: string): CoinConfig => ({ - ...coinRsaCommon, - name: `${curr}_u8`, - value: `${curr}:8`, - feeDeposit: `${curr}:0.16`, - feeRefresh: `${curr}:0.16`, - feeRefund: `${curr}:0.16`, - feeWithdraw: `${curr}:0.16`, -}); - -const coin_u10 = (curr: string): CoinConfig => ({ - ...coinRsaCommon, - name: `${curr}_u10`, - value: `${curr}:10`, - feeDeposit: `${curr}:0.2`, - feeRefresh: `${curr}:0.2`, - feeRefund: `${curr}:0.2`, - feeWithdraw: `${curr}:0.2`, -}); - -export const defaultCoinConfig = [ - coin_ct1, - coin_ct10, - coin_u1, - coin_u2, - coin_u4, - coin_u8, - coin_u10, -]; - -export function makeNoFeeCoinConfig(curr: string): CoinConfig[] { - const cc: CoinConfig[] = []; - - for (let i = 0; i < 16; i++) { - const ct = 2 ** i; - - const unit = Math.floor(ct / 100); - const cent = ct % 100; - - cc.push({ - cipher: "RSA", - durationLegal: "3 years", - durationSpend: "2 years", - durationWithdraw: "7 days", - rsaKeySize: 1024, - name: `${curr}-u${i}`, - feeDeposit: `${curr}:0`, - feeRefresh: `${curr}:0`, - feeRefund: `${curr}:0`, - feeWithdraw: `${curr}:0`, - value: `${curr}:${unit}.${cent}`, - }); - } - - return cc; -} diff --git a/packages/taler-wallet-cli/src/harness/faultInjection.ts b/packages/taler-wallet-cli/src/harness/faultInjection.ts deleted file mode 100644 index 4c3d0c123..000000000 --- a/packages/taler-wallet-cli/src/harness/faultInjection.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - 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 - */ - -/** - * Fault injection proxy. - * - * @author Florian Dold - */ - -/** - * Imports - */ -import * as http from "http"; -import { URL } from "url"; -import { - GlobalTestState, - ExchangeService, - ExchangeServiceInterface, - MerchantServiceInterface, - MerchantService, -} from "../harness/harness.js"; - -export interface FaultProxyConfig { - inboundPort: number; - targetPort: number; -} - -/** - * Fault injection context. Modified by fault injection functions. - */ -export interface FaultInjectionRequestContext { - requestUrl: string; - method: string; - requestHeaders: Record; - requestBody?: Buffer; - dropRequest: boolean; -} - -export interface FaultInjectionResponseContext { - request: FaultInjectionRequestContext; - statusCode: number; - responseHeaders: Record; - responseBody: Buffer | undefined; - dropResponse: boolean; -} - -export interface FaultSpec { - modifyRequest?: (ctx: FaultInjectionRequestContext) => Promise; - modifyResponse?: (ctx: FaultInjectionResponseContext) => Promise; -} - -export class FaultProxy { - constructor( - private globalTestState: GlobalTestState, - private faultProxyConfig: FaultProxyConfig, - ) {} - - private currentFaultSpecs: FaultSpec[] = []; - - start() { - const server = http.createServer((req, res) => { - const requestChunks: Buffer[] = []; - const requestUrl = `http://localhost:${this.faultProxyConfig.inboundPort}${req.url}`; - console.log("request for", new URL(requestUrl)); - req.on("data", (chunk) => { - requestChunks.push(chunk); - }); - req.on("end", async () => { - console.log("end of data"); - let requestBuffer: Buffer | undefined; - if (requestChunks.length > 0) { - requestBuffer = Buffer.concat(requestChunks); - } - console.log("full request body", requestBuffer); - - const faultReqContext: FaultInjectionRequestContext = { - dropRequest: false, - method: req.method!!, - requestHeaders: req.headers, - requestUrl, - requestBody: requestBuffer, - }; - - for (const faultSpec of this.currentFaultSpecs) { - if (faultSpec.modifyRequest) { - await faultSpec.modifyRequest(faultReqContext); - } - } - - if (faultReqContext.dropRequest) { - res.destroy(); - return; - } - - const faultedUrl = new URL(faultReqContext.requestUrl); - - const proxyRequest = http.request({ - method: faultReqContext.method, - host: "localhost", - port: this.faultProxyConfig.targetPort, - path: faultedUrl.pathname + faultedUrl.search, - headers: faultReqContext.requestHeaders, - }); - - console.log( - `proxying request to target path '${ - faultedUrl.pathname + faultedUrl.search - }'`, - ); - - if (faultReqContext.requestBody) { - proxyRequest.write(faultReqContext.requestBody); - } - proxyRequest.end(); - proxyRequest.on("response", (proxyResp) => { - console.log("gotten response from target", proxyResp.statusCode); - const respChunks: Buffer[] = []; - proxyResp.on("data", (proxyRespData) => { - respChunks.push(proxyRespData); - }); - proxyResp.on("end", async () => { - console.log("end of target response"); - let responseBuffer: Buffer | undefined; - if (respChunks.length > 0) { - responseBuffer = Buffer.concat(respChunks); - } - const faultRespContext: FaultInjectionResponseContext = { - request: faultReqContext, - dropResponse: false, - responseBody: responseBuffer, - responseHeaders: proxyResp.headers, - statusCode: proxyResp.statusCode!!, - }; - for (const faultSpec of this.currentFaultSpecs) { - const modResponse = faultSpec.modifyResponse; - if (modResponse) { - await modResponse(faultRespContext); - } - } - if (faultRespContext.dropResponse) { - req.destroy(); - return; - } - if (faultRespContext.responseBody) { - // We must accommodate for potentially changed content length - faultRespContext.responseHeaders[ - "content-length" - ] = `${faultRespContext.responseBody.byteLength}`; - } - console.log("writing response head"); - res.writeHead( - faultRespContext.statusCode, - http.STATUS_CODES[faultRespContext.statusCode], - faultRespContext.responseHeaders, - ); - if (faultRespContext.responseBody) { - res.write(faultRespContext.responseBody); - } - res.end(); - }); - }); - }); - }); - - server.listen(this.faultProxyConfig.inboundPort); - this.globalTestState.servers.push(server); - } - - addFault(f: FaultSpec) { - this.currentFaultSpecs.push(f); - } - - clearAllFaults() { - this.currentFaultSpecs = []; - } -} - -export class FaultInjectedExchangeService implements ExchangeServiceInterface { - baseUrl: string; - port: number; - faultProxy: FaultProxy; - - get name(): string { - return this.innerExchange.name; - } - - get masterPub(): string { - return this.innerExchange.masterPub; - } - - private innerExchange: ExchangeService; - - constructor( - t: GlobalTestState, - e: ExchangeService, - proxyInboundPort: number, - ) { - this.innerExchange = e; - this.faultProxy = new FaultProxy(t, { - inboundPort: proxyInboundPort, - targetPort: e.port, - }); - this.faultProxy.start(); - - const exchangeUrl = new URL(e.baseUrl); - exchangeUrl.port = `${proxyInboundPort}`; - this.baseUrl = exchangeUrl.href; - this.port = proxyInboundPort; - } -} - -export class FaultInjectedMerchantService implements MerchantServiceInterface { - baseUrl: string; - port: number; - faultProxy: FaultProxy; - - get name(): string { - return this.innerMerchant.name; - } - - private innerMerchant: MerchantService; - private inboundPort: number; - - constructor( - t: GlobalTestState, - m: MerchantService, - proxyInboundPort: number, - ) { - this.innerMerchant = m; - this.faultProxy = new FaultProxy(t, { - inboundPort: proxyInboundPort, - targetPort: m.port, - }); - this.faultProxy.start(); - this.inboundPort = proxyInboundPort; - } - - makeInstanceBaseUrl(instanceName?: string | undefined): string { - const url = new URL(this.innerMerchant.makeInstanceBaseUrl(instanceName)); - url.port = `${this.inboundPort}`; - return url.href; - } -} diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts deleted file mode 100644 index 6f722dc8d..000000000 --- a/packages/taler-wallet-cli/src/harness/harness.ts +++ /dev/null @@ -1,2024 +0,0 @@ -/* - 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 - */ - -/** - * Test harness for various GNU Taler components. - * Also provides a fault-injection proxy. - * - * @author Florian Dold - */ - -const logger = new Logger("harness.ts"); - -/** - * Imports - */ -import { - AmountJson, - Amounts, - AmountString, - Configuration, - CoreApiResponse, - createEddsaKeyPair, - Duration, - eddsaGetPublic, - EddsaKeyPair, - encodeCrock, - hash, - j2s, - Logger, - parsePaytoUri, - stringToBytes, - TalerProtocolDuration, -} from "@gnu-taler/taler-util"; -import { - BankAccessApi, - BankApi, - BankServiceHandle, - HarnessExchangeBankAccount, - NodeHttpLib, - openPromise, - TalerError, - WalletCoreApiClient, -} from "@gnu-taler/taler-wallet-core"; -import { deepStrictEqual } from "assert"; -import axiosImp, { AxiosError } from "axios"; -import { ChildProcess, spawn } from "child_process"; -import * as child_process from "child_process"; -import * as fs from "fs"; -import * as http from "http"; -import * as path from "path"; -import * as readline from "readline"; -import { URL } from "url"; -import * as util from "util"; -import { CoinConfig } from "./denomStructures.js"; -import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js"; -import { - codecForMerchantOrderPrivateStatusResponse, - codecForPostOrderResponse, - MerchantInstancesResponse, - MerchantOrderPrivateStatusResponse, - PostOrderRequest, - PostOrderResponse, - TipCreateConfirmation, - TipCreateRequest, - TippingReserveStatus, -} from "./merchantApiTypes.js"; - -const exec = util.promisify(child_process.exec); - -const axios = axiosImp.default; - -export async function delayMs(ms: number): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => resolve(), ms); - }); -} - -export interface WithAuthorization { - Authorization?: string; -} - -interface WaitResult { - code: number | null; - signal: NodeJS.Signals | null; -} - -/** - * Run a shell command, return stdout. - */ -export async function sh( - t: GlobalTestState, - logName: string, - command: string, - env: { [index: string]: string | undefined } = process.env, -): Promise { - logger.info(`running command ${command}`); - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const proc = spawn(command, { - stdio: ["inherit", "pipe", "pipe"], - shell: true, - env: env, - }); - proc.stdout.on("data", (x) => { - if (x instanceof Buffer) { - stdoutChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); - const stderrLog = fs.createWriteStream(stderrLogFileName, { - flags: "a", - }); - proc.stderr.pipe(stderrLog); - proc.on("exit", (code, signal) => { - logger.info(`child process exited (${code} / ${signal})`); - if (code != 0) { - reject(Error(`Unexpected exit code ${code} for '${command}'`)); - return; - } - const b = Buffer.concat(stdoutChunks).toString("utf-8"); - resolve(b); - }); - proc.on("error", () => { - reject(Error("Child process had error")); - }); - }); -} - -function shellescape(args: string[]) { - const ret = args.map((s) => { - if (/[^A-Za-z0-9_\/:=-]/.test(s)) { - s = "'" + s.replace(/'/g, "'\\''") + "'"; - s = s.replace(/^(?:'')+/g, "").replace(/\\'''/g, "\\'"); - } - return s; - }); - return ret.join(" "); -} - -/** - * Run a shell command, return stdout. - * - * Log stderr to a log file. - */ -export async function runCommand( - t: GlobalTestState, - logName: string, - command: string, - args: string[], - env: { [index: string]: string | undefined } = process.env, -): Promise { - logger.info(`running command ${shellescape([command, ...args])}`); - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const proc = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - shell: false, - env: env, - }); - proc.stdout.on("data", (x) => { - if (x instanceof Buffer) { - stdoutChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - const stderrLogFileName = path.join(t.testDir, `${logName}-stderr.log`); - const stderrLog = fs.createWriteStream(stderrLogFileName, { - flags: "a", - }); - proc.stderr.pipe(stderrLog); - proc.on("exit", (code, signal) => { - logger.info(`child process exited (${code} / ${signal})`); - if (code != 0) { - reject(Error(`Unexpected exit code ${code} for '${command}'`)); - return; - } - const b = Buffer.concat(stdoutChunks).toString("utf-8"); - resolve(b); - }); - proc.on("error", () => { - reject(Error("Child process had error")); - }); - }); -} - -export class ProcessWrapper { - private waitPromise: Promise; - constructor(public proc: ChildProcess) { - this.waitPromise = new Promise((resolve, reject) => { - proc.on("exit", (code, signal) => { - resolve({ code, signal }); - }); - proc.on("error", (err) => { - reject(err); - }); - }); - } - - wait(): Promise { - return this.waitPromise; - } -} - -export class GlobalTestParams { - testDir: string; -} - -export class GlobalTestState { - testDir: string; - procs: ProcessWrapper[]; - servers: http.Server[]; - inShutdown: boolean = false; - constructor(params: GlobalTestParams) { - this.testDir = params.testDir; - this.procs = []; - this.servers = []; - } - - async assertThrowsTalerErrorAsync( - block: () => Promise, - ): Promise { - try { - await block(); - } catch (e) { - if (e instanceof TalerError) { - return e; - } - throw Error(`expected TalerError to be thrown, but got ${e}`); - } - throw Error( - `expected TalerError to be thrown, but block finished without throwing`, - ); - } - - async assertThrowsAsync(block: () => Promise): Promise { - try { - await block(); - } catch (e) { - return e; - } - throw Error( - `expected exception to be thrown, but block finished without throwing`, - ); - } - - assertAxiosError(e: any): asserts e is AxiosError { - if (!e.isAxiosError) { - throw Error("expected axios error"); - } - } - - assertTrue(b: boolean): asserts b { - if (!b) { - throw Error("test assertion failed"); - } - } - - assertDeepEqual(actual: any, expected: T): asserts actual is T { - deepStrictEqual(actual, expected); - } - - assertAmountEquals( - amtActual: string | AmountJson, - amtExpected: string | AmountJson, - ): void { - if (Amounts.cmp(amtActual, amtExpected) != 0) { - throw Error( - `test assertion failed: expected ${Amounts.stringify( - amtExpected, - )} but got ${Amounts.stringify(amtActual)}`, - ); - } - } - - assertAmountLeq(a: string | AmountJson, b: string | AmountJson): void { - if (Amounts.cmp(a, b) > 0) { - throw Error( - `test assertion failed: expected ${Amounts.stringify( - a, - )} to be less or equal (leq) than ${Amounts.stringify(b)}`, - ); - } - } - - shutdownSync(): void { - for (const s of this.servers) { - s.close(); - s.removeAllListeners(); - } - for (const p of this.procs) { - if (p.proc.exitCode == null) { - p.proc.kill("SIGTERM"); - } - } - } - - spawnService( - command: string, - args: string[], - logName: string, - env: { [index: string]: string | undefined } = process.env, - ): ProcessWrapper { - logger.info( - `spawning process (${logName}): ${shellescape([command, ...args])}`, - ); - const proc = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - env: env, - }); - logger.info(`spawned process (${logName}) with pid ${proc.pid}`); - proc.on("error", (err) => { - logger.warn(`could not start process (${command})`, err); - }); - proc.on("exit", (code, signal) => { - logger.warn(`process ${logName} exited ${j2s({ code, signal })}`); - }); - const stderrLogFileName = this.testDir + `/${logName}-stderr.log`; - const stderrLog = fs.createWriteStream(stderrLogFileName, { - flags: "a", - }); - proc.stderr.pipe(stderrLog); - const stdoutLogFileName = this.testDir + `/${logName}-stdout.log`; - const stdoutLog = fs.createWriteStream(stdoutLogFileName, { - flags: "a", - }); - proc.stdout.pipe(stdoutLog); - const procWrap = new ProcessWrapper(proc); - this.procs.push(procWrap); - return procWrap; - } - - async shutdown(): Promise { - if (this.inShutdown) { - return; - } - if (shouldLingerInTest()) { - logger.info("refusing to shut down, lingering was requested"); - return; - } - this.inShutdown = true; - logger.info("shutting down"); - for (const s of this.servers) { - s.close(); - s.removeAllListeners(); - } - for (const p of this.procs) { - if (p.proc.exitCode == null) { - logger.info(`killing process ${p.proc.pid}`); - p.proc.kill("SIGTERM"); - await p.wait(); - } - } - } -} - -export function shouldLingerInTest(): boolean { - return !!process.env["TALER_TEST_LINGER"]; -} - -export interface TalerConfigSection { - options: Record; -} - -export interface TalerConfig { - sections: Record; -} - -export interface DbInfo { - /** - * Postgres connection string. - */ - connStr: string; - - dbname: string; -} - -export async function setupDb(gc: GlobalTestState): Promise { - const dbname = "taler-integrationtest"; - await exec(`dropdb "${dbname}" || true`); - await exec(`createdb "${dbname}"`); - return { - connStr: `postgres:///${dbname}`, - dbname, - }; -} - -export interface BankConfig { - currency: string; - httpPort: number; - database: string; - allowRegistrations: boolean; - maxDebt?: string; -} - -export interface FakeBankConfig { - currency: string; - httpPort: number; -} - -function setTalerPaths(config: Configuration, home: string) { - config.setString("paths", "taler_home", home); - // We need to make sure that the path of taler_runtime_dir isn't too long, - // as it contains unix domain sockets (108 character limit). - const runDir = fs.mkdtempSync("/tmp/taler-test-"); - config.setString("paths", "taler_runtime_dir", runDir); - config.setString( - "paths", - "taler_data_home", - "$TALER_HOME/.local/share/taler/", - ); - config.setString("paths", "taler_config_home", "$TALER_HOME/.config/taler/"); - config.setString("paths", "taler_cache_home", "$TALER_HOME/.config/taler/"); -} - -function setCoin(config: Configuration, c: CoinConfig) { - const s = `coin_${c.name}`; - config.setString(s, "value", c.value); - config.setString(s, "duration_withdraw", c.durationWithdraw); - config.setString(s, "duration_spend", c.durationSpend); - config.setString(s, "duration_legal", c.durationLegal); - config.setString(s, "fee_deposit", c.feeDeposit); - config.setString(s, "fee_withdraw", c.feeWithdraw); - config.setString(s, "fee_refresh", c.feeRefresh); - config.setString(s, "fee_refund", c.feeRefund); - if (c.ageRestricted) { - config.setString(s, "age_restricted", "yes"); - } - if (c.cipher === "RSA") { - config.setString(s, "rsa_keysize", `${c.rsaKeySize}`); - config.setString(s, "cipher", "RSA"); - } else if (c.cipher === "CS") { - config.setString(s, "cipher", "CS"); - } else { - throw new Error(); - } -} - -/** - * Send an HTTP request until it succeeds or the process dies. - */ -export async function pingProc( - proc: ProcessWrapper | undefined, - url: string, - serviceName: string, -): Promise { - if (!proc || proc.proc.exitCode !== null) { - throw Error(`service process ${serviceName} not started, can't ping`); - } - while (true) { - try { - logger.info(`pinging ${serviceName} at ${url}`); - const resp = await axios.get(url); - logger.info(`service ${serviceName} available`); - return; - } catch (e: any) { - logger.info(`service ${serviceName} not ready:`, e.toString()); - //console.log(e); - await delayMs(1000); - } - if (!proc || proc.proc.exitCode != null || proc.proc.signalCode != null) { - throw Error(`service process ${serviceName} stopped unexpectedly`); - } - } -} - -class BankServiceBase { - proc: ProcessWrapper | undefined; - - protected constructor( - protected globalTestState: GlobalTestState, - protected bankConfig: BankConfig, - protected configFile: string, - ) {} -} - -/** - * Work in progress. The key point is that both Sandbox and Nexus - * will be configured and started by this class. - */ -class LibEuFinBankService extends BankServiceBase implements BankServiceHandle { - sandboxProc: ProcessWrapper | undefined; - nexusProc: ProcessWrapper | undefined; - - http = new NodeHttpLib(); - - static async create( - gc: GlobalTestState, - bc: BankConfig, - ): Promise { - return new LibEuFinBankService(gc, bc, "foo"); - } - - get port() { - return this.bankConfig.httpPort; - } - get nexusPort() { - return this.bankConfig.httpPort + 1000; - } - - get nexusDbConn(): string { - return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`; - } - - get sandboxDbConn(): string { - return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`; - } - - get nexusBaseUrl(): string { - return `http://localhost:${this.nexusPort}`; - } - - get baseUrlDemobank(): string { - let url = new URL("demobanks/default/", this.baseUrlNetloc); - return url.href; - } - - // FIXME: Duplicate? Where is this needed? - get baseUrlAccessApi(): string { - let url = new URL("access-api/", this.baseUrlDemobank); - return url.href; - } - - get bankAccessApiBaseUrl(): string { - let url = new URL("access-api/", this.baseUrlDemobank); - return url.href; - } - - get baseUrlNetloc(): string { - return `http://localhost:${this.bankConfig.httpPort}/`; - } - - get baseUrl(): string { - return this.baseUrlAccessApi; - } - - async setSuggestedExchange( - e: ExchangeServiceInterface, - exchangePayto: string, - ) { - await sh( - this.globalTestState, - "libeufin-sandbox-set-default-exchange", - `libeufin-sandbox default-exchange ${e.baseUrl} ${exchangePayto}`, - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn, - }, - ); - } - - // Create one at both sides: Sandbox and Nexus. - async createExchangeAccount( - accountName: string, - password: string, - ): Promise { - logger.info("Create Exchange account(s)!"); - /** - * Many test cases try to create a Exchange account before - * starting the bank; that's because the Pybank did it entirely - * via the configuration file. - */ - await this.start(); - await this.pingUntilAvailable(); - await LibeufinSandboxApi.createDemobankAccount(accountName, password, { - baseUrl: this.baseUrlAccessApi, - }); - let bankAccountLabel = accountName; - await LibeufinSandboxApi.createDemobankEbicsSubscriber( - { - hostID: "talertestEbicsHost", - userID: "exchangeEbicsUser", - partnerID: "exchangeEbicsPartner", - }, - bankAccountLabel, - { baseUrl: this.baseUrlDemobank }, - ); - - await LibeufinNexusApi.createUser( - { baseUrl: this.nexusBaseUrl }, - { - username: accountName, - password: password, - }, - ); - await LibeufinNexusApi.createEbicsBankConnection( - { baseUrl: this.nexusBaseUrl }, - { - name: "ebics-connection", // connection name. - ebicsURL: new URL("ebicsweb", this.baseUrlNetloc).href, - hostID: "talertestEbicsHost", - userID: "exchangeEbicsUser", - partnerID: "exchangeEbicsPartner", - }, - ); - await LibeufinNexusApi.connectBankConnection( - { baseUrl: this.nexusBaseUrl }, - "ebics-connection", - ); - await LibeufinNexusApi.fetchAccounts( - { baseUrl: this.nexusBaseUrl }, - "ebics-connection", - ); - await LibeufinNexusApi.importConnectionAccount( - { baseUrl: this.nexusBaseUrl }, - "ebics-connection", // connection name - accountName, // offered account label - `${accountName}-nexus-label`, // bank account label at Nexus - ); - await LibeufinNexusApi.createTwgFacade( - { baseUrl: this.nexusBaseUrl }, - { - name: "exchange-facade", - connectionName: "ebics-connection", - accountName: `${accountName}-nexus-label`, - currency: "EUR", - reserveTransferLevel: "report", - }, - ); - await LibeufinNexusApi.postPermission( - { baseUrl: this.nexusBaseUrl }, - { - action: "grant", - permission: { - subjectId: accountName, - subjectType: "user", - resourceType: "facade", - resourceId: "exchange-facade", // facade name - permissionName: "facade.talerWireGateway.transfer", - }, - }, - ); - await LibeufinNexusApi.postPermission( - { baseUrl: this.nexusBaseUrl }, - { - action: "grant", - permission: { - subjectId: accountName, - subjectType: "user", - resourceType: "facade", - resourceId: "exchange-facade", // facade name - permissionName: "facade.talerWireGateway.history", - }, - }, - ); - // Set fetch task. - await LibeufinNexusApi.postTask( - { baseUrl: this.nexusBaseUrl }, - `${accountName}-nexus-label`, - { - name: "wirewatch-task", - cronspec: "* * *", - type: "fetch", - params: { - level: "all", - rangeType: "all", - }, - }, - ); - await LibeufinNexusApi.postTask( - { baseUrl: this.nexusBaseUrl }, - `${accountName}-nexus-label`, - { - name: "aggregator-task", - cronspec: "* * *", - type: "submit", - params: {}, - }, - ); - let facadesResp = await LibeufinNexusApi.getAllFacades({ - baseUrl: this.nexusBaseUrl, - }); - let accountInfoResp = await LibeufinSandboxApi.demobankAccountInfo( - "admin", - "secret", - { baseUrl: this.baseUrlAccessApi }, - accountName, // bank account label. - ); - return { - accountName: accountName, - accountPassword: password, - accountPaytoUri: accountInfoResp.data.paytoUri, - wireGatewayApiBaseUrl: facadesResp.data.facades[0].baseUrl, - }; - } - - async start(): Promise { - /** - * Because many test cases try to create a Exchange bank - * account _before_ starting the bank (Pybank did it only via - * the config), it is possible that at this point Sandbox and - * Nexus are already running. Hence, this method only launches - * them if they weren't launched earlier. - */ - - // Only go ahead if BOTH aren't running. - if (this.sandboxProc || this.nexusProc) { - logger.info("Nexus or Sandbox already running, not taking any action."); - return; - } - await sh( - this.globalTestState, - "libeufin-sandbox-config-demobank", - `libeufin-sandbox config --currency=${this.bankConfig.currency} default`, - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn, - LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", - }, - ); - this.sandboxProc = this.globalTestState.spawnService( - "libeufin-sandbox", - ["serve", "--port", `${this.port}`], - "libeufin-sandbox", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn, - LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", - }, - ); - await runCommand( - this.globalTestState, - "libeufin-nexus-superuser", - "libeufin-nexus", - ["superuser", "admin", "--password", "test"], - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn, - }, - ); - this.nexusProc = this.globalTestState.spawnService( - "libeufin-nexus", - ["serve", "--port", `${this.nexusPort}`], - "libeufin-nexus", - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusDbConn, - }, - ); - // need to wait here, because at this point - // a Ebics host needs to be created (RESTfully) - await this.pingUntilAvailable(); - LibeufinSandboxApi.createEbicsHost( - { baseUrl: this.baseUrlNetloc }, - "talertestEbicsHost", - ); - } - - async pingUntilAvailable(): Promise { - await pingProc( - this.sandboxProc, - `http://localhost:${this.bankConfig.httpPort}`, - "libeufin-sandbox", - ); - await pingProc( - this.nexusProc, - `${this.nexusBaseUrl}/config`, - "libeufin-nexus", - ); - } -} - -/** - * Implementation of the bank service using the "taler-fakebank-run" tool. - */ -export class FakebankService - extends BankServiceBase - implements BankServiceHandle -{ - proc: ProcessWrapper | undefined; - - http = new NodeHttpLib(); - - // We store "created" accounts during setup and - // register them after startup. - private accounts: { - accountName: string; - accountPassword: string; - }[] = []; - - static async create( - gc: GlobalTestState, - bc: BankConfig, - ): Promise { - const config = new Configuration(); - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString("taler", "currency", bc.currency); - config.setString("bank", "http_port", `${bc.httpPort}`); - config.setString("bank", "serve", "http"); - config.setString("bank", "max_debt_bank", `${bc.currency}:999999`); - config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`); - config.setString("bank", "ram_limit", `${1024}`); - const cfgFilename = gc.testDir + "/bank.conf"; - config.write(cfgFilename); - - return new FakebankService(gc, bc, cfgFilename); - } - - setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) { - if (!!this.proc) { - throw Error("Can't set suggested exchange while bank is running."); - } - const config = Configuration.load(this.configFile); - config.setString("bank", "suggested_exchange", e.baseUrl); - config.write(this.configFile); - } - - get baseUrl(): string { - return `http://localhost:${this.bankConfig.httpPort}/`; - } - - get bankAccessApiBaseUrl(): string { - let url = new URL("taler-bank-access/", this.baseUrl); - return url.href; - } - - async createExchangeAccount( - accountName: string, - password: string, - ): Promise { - this.accounts.push({ - accountName, - accountPassword: password, - }); - return { - accountName: accountName, - accountPassword: password, - accountPaytoUri: getPayto(accountName), - wireGatewayApiBaseUrl: `http://localhost:${this.bankConfig.httpPort}/taler-wire-gateway/${accountName}/`, - }; - } - - get port() { - return this.bankConfig.httpPort; - } - - async start(): Promise { - logger.info("starting fakebank"); - if (this.proc) { - logger.info("fakebank already running, not starting again"); - return; - } - this.proc = this.globalTestState.spawnService( - "taler-fakebank-run", - [ - "-c", - this.configFile, - "--signup-bonus", - `${this.bankConfig.currency}:100`, - ], - "bank", - ); - await this.pingUntilAvailable(); - for (const acc of this.accounts) { - await BankApi.registerAccount(this, acc.accountName, acc.accountPassword); - } - } - - async pingUntilAvailable(): Promise { - const url = `http://localhost:${this.bankConfig.httpPort}/taler-bank-integration/config`; - await pingProc(this.proc, url, "bank"); - } -} - -// Use libeufin bank instead of pybank. -const useLibeufinBank = false; - -export type BankService = BankServiceHandle; -export const BankService = FakebankService; - -export interface ExchangeConfig { - name: string; - currency: string; - roundUnit?: string; - httpPort: number; - database: string; -} - -export interface ExchangeServiceInterface { - readonly baseUrl: string; - readonly port: number; - readonly name: string; - readonly masterPub: string; -} - -export class ExchangeService implements ExchangeServiceInterface { - static fromExistingConfig(gc: GlobalTestState, exchangeName: string) { - const cfgFilename = gc.testDir + `/exchange-${exchangeName}.conf`; - const config = Configuration.load(cfgFilename); - const ec: ExchangeConfig = { - currency: config.getString("taler", "currency").required(), - database: config.getString("exchangedb-postgres", "config").required(), - httpPort: config.getNumber("exchange", "port").required(), - name: exchangeName, - roundUnit: config.getString("taler", "currency_round_unit").required(), - }; - const privFile = config.getPath("exchange", "master_priv_file").required(); - const eddsaPriv = fs.readFileSync(privFile); - const keyPair: EddsaKeyPair = { - eddsaPriv, - eddsaPub: eddsaGetPublic(eddsaPriv), - }; - return new ExchangeService(gc, ec, cfgFilename, keyPair); - } - - private currentTimetravel: Duration | undefined; - - setTimetravel(t: Duration | undefined): void { - if (this.isRunning()) { - throw Error("can't set time travel while the exchange is running"); - } - this.currentTimetravel = t; - } - - private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { - // Convert to microseconds - return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; - } - return undefined; - } - - /** - * Return an empty array if no time travel is set, - * and an array with the time travel command line argument - * otherwise. - */ - private get timetravelArgArr(): string[] { - const tta = this.timetravelArg; - if (tta) { - return [tta]; - } - return []; - } - - async runWirewatchOnce() { - if (useLibeufinBank) { - // Not even 2 secods showed to be enough! - await waitMs(4000); - } - await runCommand( - this.globalState, - `exchange-${this.name}-wirewatch-once`, - "taler-exchange-wirewatch", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); - } - - async runAggregatorOnce() { - try { - await runCommand( - this.globalState, - `exchange-${this.name}-aggregator-once`, - "taler-exchange-aggregator", - [...this.timetravelArgArr, "-c", this.configFilename, "-t", "-y"], - ); - } catch (e) { - logger.info( - "running aggregator with KYC off didn't work, might be old version, running again", - ); - await runCommand( - this.globalState, - `exchange-${this.name}-aggregator-once`, - "taler-exchange-aggregator", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); - } - } - - async runTransferOnce() { - await runCommand( - this.globalState, - `exchange-${this.name}-transfer-once`, - "taler-exchange-transfer", - [...this.timetravelArgArr, "-c", this.configFilename, "-t"], - ); - } - - changeConfig(f: (config: Configuration) => void) { - const config = Configuration.load(this.configFilename); - f(config); - config.write(this.configFilename); - } - - static create(gc: GlobalTestState, e: ExchangeConfig) { - const config = new Configuration(); - config.setString("taler", "currency", e.currency); - config.setString( - "taler", - "currency_round_unit", - e.roundUnit ?? `${e.currency}:0.01`, - ); - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString( - "exchange", - "revocation_dir", - "${TALER_DATA_HOME}/exchange/revocations", - ); - config.setString("exchange", "max_keys_caching", "forever"); - config.setString("exchange", "db", "postgres"); - config.setString( - "exchange-offline", - "master_priv_file", - "${TALER_DATA_HOME}/exchange/offline-keys/master.priv", - ); - config.setString("exchange", "serve", "tcp"); - config.setString("exchange", "port", `${e.httpPort}`); - - config.setString("exchangedb-postgres", "config", e.database); - - config.setString("taler-exchange-secmod-eddsa", "lookahead_sign", "20 s"); - config.setString("taler-exchange-secmod-rsa", "lookahead_sign", "20 s"); - - const exchangeMasterKey = createEddsaKeyPair(); - - config.setString( - "exchange", - "master_public_key", - encodeCrock(exchangeMasterKey.eddsaPub), - ); - - const masterPrivFile = config - .getPath("exchange-offline", "master_priv_file") - .required(); - - fs.mkdirSync(path.dirname(masterPrivFile), { recursive: true }); - - fs.writeFileSync(masterPrivFile, Buffer.from(exchangeMasterKey.eddsaPriv)); - - const cfgFilename = gc.testDir + `/exchange-${e.name}.conf`; - config.write(cfgFilename); - return new ExchangeService(gc, e, cfgFilename, exchangeMasterKey); - } - - addOfferedCoins(offeredCoins: ((curr: string) => CoinConfig)[]) { - const config = Configuration.load(this.configFilename); - offeredCoins.forEach((cc) => - setCoin(config, cc(this.exchangeConfig.currency)), - ); - config.write(this.configFilename); - } - - addCoinConfigList(ccs: CoinConfig[]) { - const config = Configuration.load(this.configFilename); - ccs.forEach((cc) => setCoin(config, cc)); - config.write(this.configFilename); - } - - enableAgeRestrictions(maskStr: string) { - const config = Configuration.load(this.configFilename); - config.setString("exchange-extension-age_restriction", "enabled", "yes"); - config.setString( - "exchange-extension-age_restriction", - "age_groups", - maskStr, - ); - config.write(this.configFilename); - } - - get masterPub() { - return encodeCrock(this.keyPair.eddsaPub); - } - - get port() { - return this.exchangeConfig.httpPort; - } - - async addBankAccount( - localName: string, - exchangeBankAccount: HarnessExchangeBankAccount, - ): Promise { - const config = Configuration.load(this.configFilename); - config.setString( - `exchange-account-${localName}`, - "wire_response", - `\${TALER_DATA_HOME}/exchange/account-${localName}.json`, - ); - config.setString( - `exchange-account-${localName}`, - "payto_uri", - exchangeBankAccount.accountPaytoUri, - ); - config.setString(`exchange-account-${localName}`, "enable_credit", "yes"); - config.setString(`exchange-account-${localName}`, "enable_debit", "yes"); - config.setString( - `exchange-accountcredentials-${localName}`, - "wire_gateway_url", - exchangeBankAccount.wireGatewayApiBaseUrl, - ); - config.setString( - `exchange-accountcredentials-${localName}`, - "wire_gateway_auth_method", - "basic", - ); - config.setString( - `exchange-accountcredentials-${localName}`, - "username", - exchangeBankAccount.accountName, - ); - config.setString( - `exchange-accountcredentials-${localName}`, - "password", - exchangeBankAccount.accountPassword, - ); - config.write(this.configFilename); - } - - exchangeHttpProc: ProcessWrapper | undefined; - exchangeWirewatchProc: ProcessWrapper | undefined; - - helperCryptoRsaProc: ProcessWrapper | undefined; - helperCryptoEddsaProc: ProcessWrapper | undefined; - helperCryptoCsProc: ProcessWrapper | undefined; - - constructor( - private globalState: GlobalTestState, - private exchangeConfig: ExchangeConfig, - private configFilename: string, - private keyPair: EddsaKeyPair, - ) {} - - get name() { - return this.exchangeConfig.name; - } - - get baseUrl() { - return `http://localhost:${this.exchangeConfig.httpPort}/`; - } - - isRunning(): boolean { - return !!this.exchangeWirewatchProc || !!this.exchangeHttpProc; - } - - async stop(): Promise { - const wirewatch = this.exchangeWirewatchProc; - if (wirewatch) { - wirewatch.proc.kill("SIGTERM"); - await wirewatch.wait(); - this.exchangeWirewatchProc = undefined; - } - const httpd = this.exchangeHttpProc; - if (httpd) { - httpd.proc.kill("SIGTERM"); - await httpd.wait(); - this.exchangeHttpProc = undefined; - } - const cryptoRsa = this.helperCryptoRsaProc; - if (cryptoRsa) { - cryptoRsa.proc.kill("SIGTERM"); - await cryptoRsa.wait(); - this.helperCryptoRsaProc = undefined; - } - const cryptoEddsa = this.helperCryptoEddsaProc; - if (cryptoEddsa) { - cryptoEddsa.proc.kill("SIGTERM"); - await cryptoEddsa.wait(); - this.helperCryptoRsaProc = undefined; - } - const cryptoCs = this.helperCryptoCsProc; - if (cryptoCs) { - cryptoCs.proc.kill("SIGTERM"); - await cryptoCs.wait(); - this.helperCryptoCsProc = undefined; - } - } - - /** - * Update keys signing the keys generated by the security module - * with the offline signing key. - */ - async keyup(): Promise { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - ["-c", this.configFilename, "download", "sign", "upload"], - ); - - const accounts: string[] = []; - const accountTargetTypes: Set = new Set(); - - const config = Configuration.load(this.configFilename); - for (const sectionName of config.getSectionNames()) { - if (sectionName.startsWith("EXCHANGE-ACCOUNT-")) { - const paytoUri = config.getString(sectionName, "payto_uri").required(); - const p = parsePaytoUri(paytoUri); - if (!p) { - throw Error(`invalid payto uri in exchange config: ${paytoUri}`); - } - accountTargetTypes.add(p?.targetType); - accounts.push(paytoUri); - } - } - - logger.info("configuring bank accounts", accounts); - - for (const acc of accounts) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - ["-c", this.configFilename, "enable-account", acc, "upload"], - ); - } - - const year = new Date().getFullYear(); - for (const accTargetType of accountTargetTypes.values()) { - for (let i = year; i < year + 5; i++) { - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - "wire-fee", - // Year - `${i}`, - // Wire method - accTargetType, - // Wire fee - `${this.exchangeConfig.currency}:0.01`, - // Closing fee - `${this.exchangeConfig.currency}:0.01`, - "upload", - ], - ); - } - } - - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - "global-fee", - // year - "now", - // history fee - `${this.exchangeConfig.currency}:0.01`, - // account fee - `${this.exchangeConfig.currency}:0.01`, - // purse fee - `${this.exchangeConfig.currency}:0.00`, - // purse timeout - "1h", - // history expiration - "1year", - // free purses per account - "5", - "upload", - ], - ); - } - - async revokeDenomination(denomPubHash: string) { - if (!this.isRunning()) { - throw Error("exchange must be running when revoking denominations"); - } - await runCommand( - this.globalState, - "exchange-offline", - "taler-exchange-offline", - [ - "-c", - this.configFilename, - "revoke-denomination", - denomPubHash, - "upload", - ], - ); - } - - async purgeSecmodKeys(): Promise { - const cfg = Configuration.load(this.configFilename); - const rsaKeydir = cfg - .getPath("taler-exchange-secmod-rsa", "KEY_DIR") - .required(); - const eddsaKeydir = cfg - .getPath("taler-exchange-secmod-eddsa", "KEY_DIR") - .required(); - // Be *VERY* careful when changing this, or you will accidentally delete user data. - await sh(this.globalState, "rm-secmod-keys", `rm -rf ${rsaKeydir}/COIN_*`); - await sh(this.globalState, "rm-secmod-keys", `rm ${eddsaKeydir}/*`); - } - - async purgeDatabase(): Promise { - await sh( - this.globalState, - "exchange-dbinit", - `taler-exchange-dbinit -r -c "${this.configFilename}"`, - ); - } - - async start(): Promise { - if (this.isRunning()) { - throw Error("exchange is already running"); - } - await sh( - this.globalState, - "exchange-dbinit", - `taler-exchange-dbinit -c "${this.configFilename}"`, - ); - - this.helperCryptoEddsaProc = this.globalState.spawnService( - "taler-exchange-secmod-eddsa", - ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], - `exchange-crypto-eddsa-${this.name}`, - ); - - this.helperCryptoCsProc = this.globalState.spawnService( - "taler-exchange-secmod-cs", - ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], - `exchange-crypto-cs-${this.name}`, - ); - - this.helperCryptoRsaProc = this.globalState.spawnService( - "taler-exchange-secmod-rsa", - ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], - `exchange-crypto-rsa-${this.name}`, - ); - - this.exchangeWirewatchProc = this.globalState.spawnService( - "taler-exchange-wirewatch", - ["-c", this.configFilename, ...this.timetravelArgArr], - `exchange-wirewatch-${this.name}`, - ); - - this.exchangeHttpProc = this.globalState.spawnService( - "taler-exchange-httpd", - ["-LINFO", "-c", this.configFilename, ...this.timetravelArgArr], - `exchange-httpd-${this.name}`, - ); - - await this.pingUntilAvailable(); - await this.keyup(); - } - - async pingUntilAvailable(): Promise { - // We request /management/keys, since /keys can block - // when we didn't do the key setup yet. - const url = `http://localhost:${this.exchangeConfig.httpPort}/management/keys`; - await pingProc(this.exchangeHttpProc, url, `exchange (${this.name})`); - } -} - -export interface MerchantConfig { - name: string; - currency: string; - httpPort: number; - database: string; -} - -export interface PrivateOrderStatusQuery { - instance?: string; - orderId: string; - sessionId?: string; -} - -export interface MerchantServiceInterface { - makeInstanceBaseUrl(instanceName?: string): string; - readonly port: number; - readonly name: string; -} - -export class MerchantApiClient { - constructor( - private baseUrl: string, - public readonly auth: MerchantAuthConfiguration, - ) {} - - async changeAuth(auth: MerchantAuthConfiguration): Promise { - const url = new URL("private/auth", this.baseUrl); - await axios.post(url.href, auth, { - headers: this.makeAuthHeader(), - }); - } - - async deleteInstance(instanceId: string) { - const url = new URL(`management/instances/${instanceId}`, this.baseUrl); - await axios.delete(url.href, { - headers: this.makeAuthHeader(), - }); - } - - async createInstance(req: MerchantInstanceConfig): Promise { - const url = new URL("management/instances", this.baseUrl); - await axios.post(url.href, req, { - headers: this.makeAuthHeader(), - }); - } - - async getInstances(): Promise { - const url = new URL("management/instances", this.baseUrl); - const resp = await axios.get(url.href, { - headers: this.makeAuthHeader(), - }); - return resp.data; - } - - async getInstanceFullDetails(instanceId: string): Promise { - const url = new URL(`management/instances/${instanceId}`, this.baseUrl); - try { - const resp = await axios.get(url.href, { - headers: this.makeAuthHeader(), - }); - return resp.data; - } catch (e) { - throw e; - } - } - - makeAuthHeader(): Record { - switch (this.auth.method) { - case "external": - return {}; - case "token": - return { - Authorization: `Bearer ${this.auth.token}`, - }; - } - } -} - -/** - * FIXME: This should be deprecated in favor of MerchantApiClient - */ -export namespace MerchantPrivateApi { - export async function createOrder( - merchantService: MerchantServiceInterface, - instanceName: string, - req: PostOrderRequest, - withAuthorization: WithAuthorization = {}, - ): Promise { - const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); - let url = new URL("private/orders", baseUrl); - const resp = await axios.post(url.href, req, { - headers: withAuthorization as Record, - }); - return codecForPostOrderResponse().decode(resp.data); - } - - export async function queryPrivateOrderStatus( - merchantService: MerchantServiceInterface, - query: PrivateOrderStatusQuery, - withAuthorization: WithAuthorization = {}, - ): Promise { - const reqUrl = new URL( - `private/orders/${query.orderId}`, - merchantService.makeInstanceBaseUrl(query.instance), - ); - if (query.sessionId) { - reqUrl.searchParams.set("session_id", query.sessionId); - } - const resp = await axios.get(reqUrl.href, { - headers: withAuthorization as Record, - }); - return codecForMerchantOrderPrivateStatusResponse().decode(resp.data); - } - - export async function giveRefund( - merchantService: MerchantServiceInterface, - r: { - instance: string; - orderId: string; - amount: string; - justification: string; - }, - ): Promise<{ talerRefundUri: string }> { - const reqUrl = new URL( - `private/orders/${r.orderId}/refund`, - merchantService.makeInstanceBaseUrl(r.instance), - ); - const resp = await axios.post(reqUrl.href, { - refund: r.amount, - reason: r.justification, - }); - return { - talerRefundUri: resp.data.taler_refund_uri, - }; - } - - export async function createTippingReserve( - merchantService: MerchantServiceInterface, - instance: string, - req: CreateMerchantTippingReserveRequest, - ): Promise { - const reqUrl = new URL( - `private/reserves`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await axios.post(reqUrl.href, req); - // FIXME: validate - return resp.data; - } - - export async function queryTippingReserves( - merchantService: MerchantServiceInterface, - instance: string, - ): Promise { - const reqUrl = new URL( - `private/reserves`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await axios.get(reqUrl.href); - // FIXME: validate - return resp.data; - } - - export async function giveTip( - merchantService: MerchantServiceInterface, - instance: string, - req: TipCreateRequest, - ): Promise { - const reqUrl = new URL( - `private/tips`, - merchantService.makeInstanceBaseUrl(instance), - ); - const resp = await axios.post(reqUrl.href, req); - // FIXME: validate - return resp.data; - } -} - -export interface CreateMerchantTippingReserveRequest { - // Amount that the merchant promises to put into the reserve - initial_balance: AmountString; - - // Exchange the merchant intends to use for tipping - exchange_url: string; - - // Desired wire method, for example "iban" or "x-taler-bank" - wire_method: string; -} - -export interface CreateMerchantTippingReserveConfirmation { - // Public key identifying the reserve - reserve_pub: string; - - // Wire account of the exchange where to transfer the funds - payto_uri: string; -} - -export class MerchantService implements MerchantServiceInterface { - static fromExistingConfig(gc: GlobalTestState, name: string) { - const cfgFilename = gc.testDir + `/merchant-${name}.conf`; - const config = Configuration.load(cfgFilename); - const mc: MerchantConfig = { - currency: config.getString("taler", "currency").required(), - database: config.getString("merchantdb-postgres", "config").required(), - httpPort: config.getNumber("merchant", "port").required(), - name, - }; - return new MerchantService(gc, mc, cfgFilename); - } - - proc: ProcessWrapper | undefined; - - constructor( - private globalState: GlobalTestState, - private merchantConfig: MerchantConfig, - private configFilename: string, - ) {} - - private currentTimetravel: Duration | undefined; - - private isRunning(): boolean { - return !!this.proc; - } - - setTimetravel(t: Duration | undefined): void { - if (this.isRunning()) { - throw Error("can't set time travel while the exchange is running"); - } - this.currentTimetravel = t; - } - - private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { - // Convert to microseconds - return `--timetravel=+${this.currentTimetravel.d_ms * 1000}`; - } - return undefined; - } - - /** - * Return an empty array if no time travel is set, - * and an array with the time travel command line argument - * otherwise. - */ - private get timetravelArgArr(): string[] { - const tta = this.timetravelArg; - if (tta) { - return [tta]; - } - return []; - } - - get port(): number { - return this.merchantConfig.httpPort; - } - - get name(): string { - return this.merchantConfig.name; - } - - async stop(): Promise { - const httpd = this.proc; - if (httpd) { - httpd.proc.kill("SIGTERM"); - await httpd.wait(); - this.proc = undefined; - } - } - - async start(): Promise { - await exec(`taler-merchant-dbinit -c "${this.configFilename}"`); - - this.proc = this.globalState.spawnService( - "taler-merchant-httpd", - [ - "taler-merchant-httpd", - "-LDEBUG", - "-c", - this.configFilename, - ...this.timetravelArgArr, - ], - `merchant-${this.merchantConfig.name}`, - ); - } - - static async create( - gc: GlobalTestState, - mc: MerchantConfig, - ): Promise { - const config = new Configuration(); - config.setString("taler", "currency", mc.currency); - - const cfgFilename = gc.testDir + `/merchant-${mc.name}.conf`; - setTalerPaths(config, gc.testDir + "/talerhome"); - config.setString("merchant", "serve", "tcp"); - config.setString("merchant", "port", `${mc.httpPort}`); - config.setString( - "merchant", - "keyfile", - "${TALER_DATA_HOME}/merchant/merchant.priv", - ); - config.setString("merchantdb-postgres", "config", mc.database); - config.write(cfgFilename); - - return new MerchantService(gc, mc, cfgFilename); - } - - addExchange(e: ExchangeServiceInterface): void { - const config = Configuration.load(this.configFilename); - config.setString( - `merchant-exchange-${e.name}`, - "exchange_base_url", - e.baseUrl, - ); - config.setString( - `merchant-exchange-${e.name}`, - "currency", - this.merchantConfig.currency, - ); - config.setString(`merchant-exchange-${e.name}`, "master_key", e.masterPub); - config.write(this.configFilename); - } - - async addDefaultInstance(): Promise { - return await this.addInstance({ - id: "default", - name: "Default Instance", - paytoUris: [getPayto("merchant-default")], - auth: { - method: "external", - }, - }); - } - - async addInstance( - instanceConfig: PartialMerchantInstanceConfig, - ): Promise { - if (!this.proc) { - throw Error("merchant must be running to add instance"); - } - logger.info("adding instance"); - const url = `http://localhost:${this.merchantConfig.httpPort}/management/instances`; - const auth = instanceConfig.auth ?? { method: "external" }; - - const body: MerchantInstanceConfig = { - auth, - payto_uris: instanceConfig.paytoUris, - id: instanceConfig.id, - name: instanceConfig.name, - address: instanceConfig.address ?? {}, - jurisdiction: instanceConfig.jurisdiction ?? {}, - default_max_wire_fee: - instanceConfig.defaultMaxWireFee ?? - `${this.merchantConfig.currency}:1.0`, - default_wire_fee_amortization: - instanceConfig.defaultWireFeeAmortization ?? 3, - default_max_deposit_fee: - instanceConfig.defaultMaxDepositFee ?? - `${this.merchantConfig.currency}:1.0`, - default_wire_transfer_delay: - instanceConfig.defaultWireTransferDelay ?? - Duration.toTalerProtocolDuration( - Duration.fromSpec({ - days: 1, - }), - ), - default_pay_delay: - instanceConfig.defaultPayDelay ?? - Duration.toTalerProtocolDuration(Duration.getForever()), - }; - await axios.post(url, body); - } - - makeInstanceBaseUrl(instanceName?: string): string { - if (instanceName === undefined || instanceName === "default") { - return `http://localhost:${this.merchantConfig.httpPort}/`; - } else { - return `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/`; - } - } - - async pingUntilAvailable(): Promise { - const url = `http://localhost:${this.merchantConfig.httpPort}/config`; - await pingProc(this.proc, url, `merchant (${this.merchantConfig.name})`); - } -} - -export interface MerchantAuthConfiguration { - method: "external" | "token"; - token?: string; -} - -export interface PartialMerchantInstanceConfig { - auth?: MerchantAuthConfiguration; - id: string; - name: string; - paytoUris: string[]; - address?: unknown; - jurisdiction?: unknown; - defaultMaxWireFee?: string; - defaultMaxDepositFee?: string; - defaultWireFeeAmortization?: number; - defaultWireTransferDelay?: TalerProtocolDuration; - defaultPayDelay?: TalerProtocolDuration; -} - -export interface MerchantInstanceConfig { - auth: MerchantAuthConfiguration; - id: string; - name: string; - payto_uris: string[]; - address: unknown; - jurisdiction: unknown; - default_max_wire_fee: string; - default_max_deposit_fee: string; - default_wire_fee_amortization: number; - default_wire_transfer_delay: TalerProtocolDuration; - default_pay_delay: TalerProtocolDuration; -} - -type TestStatus = "pass" | "fail" | "skip"; - -export interface TestRunResult { - /** - * Name of the test. - */ - name: string; - - /** - * How long did the test run? - */ - timeSec: number; - - status: TestStatus; - - reason?: string; -} - -export async function runTestWithState( - gc: GlobalTestState, - testMain: (t: GlobalTestState) => Promise, - testName: string, - linger: boolean = false, -): Promise { - const startMs = new Date().getTime(); - - const p = openPromise(); - let status: TestStatus; - - const handleSignal = (s: string) => { - logger.warn( - `**** received fatal process event, terminating test ${testName}`, - ); - gc.shutdownSync(); - process.exit(1); - }; - - process.on("SIGINT", handleSignal); - process.on("SIGTERM", handleSignal); - process.on("unhandledRejection", handleSignal); - process.on("uncaughtException", handleSignal); - - try { - logger.info("running test in directory", gc.testDir); - await Promise.race([testMain(gc), p.promise]); - status = "pass"; - if (linger) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: true, - }); - await new Promise((resolve, reject) => { - rl.question("Press enter to shut down test.", () => { - logger.error("Requested shutdown"); - resolve(); - }); - }); - rl.close(); - } - } catch (e) { - console.error("FATAL: test failed with exception", e); - if (e instanceof TalerError) { - console.error(`error detail: ${j2s(e.errorDetail)}`); - } - status = "fail"; - } finally { - await gc.shutdown(); - } - const afterMs = new Date().getTime(); - return { - name: testName, - timeSec: (afterMs - startMs) / 1000, - status, - }; -} - -function shellWrap(s: string) { - return "'" + s.replace("\\", "\\\\").replace("'", "\\'") + "'"; -} - -export interface WalletCliOpts { - cryptoWorkerType?: "sync" | "node-worker-thread"; -} - -export class WalletCli { - private currentTimetravel: Duration | undefined; - private _client: WalletCoreApiClient; - - setTimetravel(d: Duration | undefined) { - this.currentTimetravel = d; - } - - private get timetravelArg(): string | undefined { - if (this.currentTimetravel && this.currentTimetravel.d_ms !== "forever") { - // Convert to microseconds - return `--timetravel=${this.currentTimetravel.d_ms * 1000}`; - } - return undefined; - } - - constructor( - private globalTestState: GlobalTestState, - private name: string = "default", - cliOpts: WalletCliOpts = {}, - ) { - const self = this; - this._client = { - async call(op: any, payload: any): Promise { - logger.info( - `calling wallet with timetravel arg ${j2s(self.timetravelArg)}`, - ); - const cryptoWorkerArg = cliOpts.cryptoWorkerType - ? `--crypto-worker=${cliOpts.cryptoWorkerType}` - : ""; - const resp = await sh( - self.globalTestState, - `wallet-${self.name}`, - `taler-wallet-cli ${ - self.timetravelArg ?? "" - } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${ - self.dbfile - }' api '${op}' ${shellWrap(JSON.stringify(payload))}`, - ); - logger.info("--- wallet core response ---"); - logger.info(resp); - logger.info("--- end of response ---"); - let ar: any; - try { - ar = JSON.parse(resp) as CoreApiResponse; - } catch (e) { - throw new Error("wallet CLI did not return a proper JSON response"); - } - if (ar.type === "error") { - throw TalerError.fromUncheckedDetail(ar.error); - } - return ar.result; - }, - }; - } - - get dbfile(): string { - return this.globalTestState.testDir + `/walletdb-${this.name}.json`; - } - - deleteDatabase() { - fs.unlinkSync(this.dbfile); - } - - private get timetravelArgArr(): string[] { - const tta = this.timetravelArg; - if (tta) { - return [tta]; - } - return []; - } - - get client(): WalletCoreApiClient { - return this._client; - } - - async runUntilDone(args: { maxRetries?: number } = {}): Promise { - await runCommand( - this.globalTestState, - `wallet-${this.name}`, - "taler-wallet-cli", - [ - "--no-throttle", - ...this.timetravelArgArr, - "-LTRACE", - "--skip-defaults", - "--wallet-db", - this.dbfile, - "run-until-done", - ...(args.maxRetries ? ["--max-retries", `${args.maxRetries}`] : []), - ], - ); - } - - async runPending(): Promise { - await runCommand( - this.globalTestState, - `wallet-${this.name}`, - "taler-wallet-cli", - [ - "--no-throttle", - "--skip-defaults", - "-LTRACE", - ...this.timetravelArgArr, - "--wallet-db", - this.dbfile, - "advanced", - "run-pending", - ], - ); - } -} - -export function getRandomIban(salt: string | null = null): string { - function getBban(salt: string | null): string { - if (!salt) return Math.random().toString().substring(2, 6); - let hashed = hash(stringToBytes(salt)); - let ret = ""; - for (let i = 0; i < hashed.length; i++) { - ret += hashed[i].toString(); - } - return ret.substring(0, 4); - } - - let cc_no_check = "131400"; // == DE00 - let bban = getBban(salt); - let check_digits = ( - 98 - - (Number.parseInt(`${bban}${cc_no_check}`) % 97) - ).toString(); - if (check_digits.length == 1) { - check_digits = `0${check_digits}`; - } - return `DE${check_digits}${bban}`; -} - -export function getWireMethodForTest(): string { - if (useLibeufinBank) return "iban"; - return "x-taler-bank"; -} - -/** - * Generate a payto address, whose authority depends - * on whether the banking is served by euFin or Pybank. - */ -export function getPayto(label: string): string { - if (useLibeufinBank) - return `payto://iban/SANDBOXX/${getRandomIban( - label, - )}?receiver-name=${label}`; - return `payto://x-taler-bank/localhost/${label}`; -} - -function waitMs(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/packages/taler-wallet-cli/src/harness/helpers.ts b/packages/taler-wallet-cli/src/harness/helpers.ts deleted file mode 100644 index affaccd61..000000000 --- a/packages/taler-wallet-cli/src/harness/helpers.ts +++ /dev/null @@ -1,444 +0,0 @@ -/* - 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 - */ - -/** - * Helpers to create typical test environments. - * - * @author Florian Dold - */ - -/** - * Imports - */ -import { - AmountString, - ConfirmPayResultType, - MerchantContractTerms, - Duration, - PreparePayResultType, -} from "@gnu-taler/taler-util"; -import { - BankAccessApi, - BankApi, - HarnessExchangeBankAccount, - WalletApiOperation, -} from "@gnu-taler/taler-wallet-core"; -import { CoinConfig, defaultCoinConfig } from "./denomStructures.js"; -import { - FaultInjectedExchangeService, - FaultInjectedMerchantService, -} from "./faultInjection.js"; -import { - BankService, - DbInfo, - ExchangeService, - ExchangeServiceInterface, - getPayto, - GlobalTestState, - MerchantPrivateApi, - MerchantService, - MerchantServiceInterface, - setupDb, - WalletCli, - WithAuthorization, -} from "./harness.js"; - -export interface SimpleTestEnvironment { - commonDb: DbInfo; - bank: BankService; - exchange: ExchangeService; - exchangeBankAccount: HarnessExchangeBankAccount; - merchant: MerchantService; - wallet: WalletCli; -} - -export interface EnvOptions { - /** - * If provided, enable age restrictions with the specified age mask string. - */ - ageMaskSpec?: string; - - mixedAgeRestriction?: boolean; -} - -/** - * Run a test case with a simple TESTKUDOS Taler environment, consisting - * of one exchange, one bank and one merchant. - */ -export async function createSimpleTestkudosEnvironment( - t: GlobalTestState, - coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), - opts: EnvOptions = {}, -): Promise { - 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 ageMaskSpec = opts.ageMaskSpec; - - if (ageMaskSpec) { - exchange.enableAgeRestrictions(ageMaskSpec); - // Enable age restriction for all coins. - exchange.addCoinConfigList( - coinConfig.map((x) => ({ - ...x, - name: `${x.name}-age`, - ageRestricted: true, - })), - ); - // For mixed age restrictions, we also offer coins without age restrictions - if (opts.mixedAgeRestriction) { - exchange.addCoinConfigList( - coinConfig.map((x) => ({ ...x, ageRestricted: false })), - ); - } - } else { - 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")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); - - await merchant.addInstance({ - id: "minst1", - name: "minst1", - paytoUris: [getPayto("minst1")], - defaultWireTransferDelay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 1 }), - ), - }); - - console.log("setup done!"); - - const wallet = new WalletCli(t); - - return { - commonDb: db, - exchange, - merchant, - wallet, - bank, - exchangeBankAccount, - }; -} - -export interface FaultyMerchantTestEnvironment { - commonDb: DbInfo; - bank: BankService; - exchange: ExchangeService; - faultyExchange: FaultInjectedExchangeService; - exchangeBankAccount: HarnessExchangeBankAccount; - merchant: MerchantService; - faultyMerchant: FaultInjectedMerchantService; - wallet: WalletCli; -} - -/** - * Run a test case with a simple TESTKUDOS Taler environment, consisting - * of one exchange, one bank and one merchant. - */ -export async function createFaultInjectedMerchantTestkudosEnvironment( - t: GlobalTestState, -): Promise { - 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(); - - merchant.addExchange(faultyExchange); - - 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, - }; -} - -/** - * Start withdrawing into the wallet. - * - * Only starts the operation, does not wait for it to finish. - */ -export async function startWithdrawViaBank( - t: GlobalTestState, - p: { - wallet: WalletCli; - bank: BankService; - exchange: ExchangeServiceInterface; - amount: AmountString; - restrictAge?: number; - }, -): Promise { - const { wallet, bank, exchange, amount } = p; - - const user = await BankApi.createRandomBankUser(bank); - const wop = await BankAccessApi.createWithdrawalOperation(bank, user, amount); - - // Hand it to the wallet - - await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { - talerWithdrawUri: wop.taler_withdraw_uri, - restrictAge: p.restrictAge, - }); - - await wallet.runPending(); - - // Withdraw (AKA select) - - await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { - exchangeBaseUrl: exchange.baseUrl, - talerWithdrawUri: wop.taler_withdraw_uri, - restrictAge: p.restrictAge, - }); - - // Confirm it - - await BankApi.confirmWithdrawalOperation(bank, user, wop); - - // We do *not* call runPending / runUntilDone on the wallet here. - // Some tests rely on the final withdraw failing. -} - -/** - * Withdraw balance. - */ -export async function withdrawViaBank( - t: GlobalTestState, - p: { - wallet: WalletCli; - bank: BankService; - exchange: ExchangeServiceInterface; - amount: AmountString; - restrictAge?: number; - }, -): Promise { - const { wallet } = p; - - await startWithdrawViaBank(t, p); - - await wallet.runUntilDone(); - - // Check balance - - await wallet.client.call(WalletApiOperation.GetBalances, {}); -} - -export async function applyTimeTravel( - timetravelDuration: Duration, - s: { - exchange?: ExchangeService; - merchant?: MerchantService; - wallet?: WalletCli; - }, -): Promise { - 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) { - s.wallet.setTimetravel(timetravelDuration); - } -} - -/** - * Make a simple payment and check that it succeeded. - */ -export async function makeTestPayment( - t: GlobalTestState, - args: { - merchant: MerchantServiceInterface; - wallet: WalletCli; - order: Partial; - instance?: string; - }, - auth: WithAuthorization = {}, -): Promise { - // Set up order. - - const { wallet, merchant } = args; - const instance = args.instance ?? "default"; - - const orderResp = await MerchantPrivateApi.createOrder( - merchant, - instance, - { - order: args.order, - }, - auth, - ); - - let orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - }, - auth, - ); - - 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 r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { - proposalId: preparePayResult.proposalId, - }); - - t.assertTrue(r2.type === ConfirmPayResultType.Done); - - // Check if payment was successful. - - orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( - merchant, - { - orderId: orderResp.order_id, - instance, - }, - auth, - ); - - t.assertTrue(orderStatus.order_status === "paid"); -} diff --git a/packages/taler-wallet-cli/src/harness/libeufin-apis.ts b/packages/taler-wallet-cli/src/harness/libeufin-apis.ts deleted file mode 100644 index f55275927..000000000 --- a/packages/taler-wallet-cli/src/harness/libeufin-apis.ts +++ /dev/null @@ -1,872 +0,0 @@ -/** - * This file defines most of the API calls offered - * by Nexus and Sandbox. They don't have state, - * therefore got moved away from libeufin.ts where - * the services get actually started and managed. - */ - -import axiosImp from "axios"; -const axios = axiosImp.default; -import { Logger, URL } from "@gnu-taler/taler-util"; - -export interface LibeufinSandboxServiceInterface { - baseUrl: string; -} - -export interface LibeufinNexusServiceInterface { - baseUrl: string; -} - -export interface CreateEbicsSubscriberRequest { - hostID: string; - userID: string; - partnerID: string; - systemID?: string; -} - -export interface BankAccountInfo { - iban: string; - bic: string; - name: string; - label: string; -} - -export interface CreateEbicsBankConnectionRequest { - name: string; // connection name. - ebicsURL: string; - hostID: string; - userID: string; - partnerID: string; - systemID?: string; -} - -export interface UpdateNexusUserRequest { - newPassword: string; -} - -export interface NexusAuth { - auth: { - username: string; - password: string; - }; -} - -export interface PostNexusTaskRequest { - name: string; - cronspec: string; - type: string; // fetch | submit - params: - | { - level: string; // report | statement | all - rangeType: string; // all | since-last | previous-days | latest - } - | {}; -} - -export interface CreateNexusUserRequest { - username: string; - password: string; -} - -export interface PostNexusPermissionRequest { - action: "revoke" | "grant"; - permission: { - subjectType: string; - subjectId: string; - resourceType: string; - resourceId: string; - permissionName: string; - }; -} - -export interface CreateAnastasisFacadeRequest { - name: string; - connectionName: string; - accountName: string; - currency: string; - reserveTransferLevel: "report" | "statement" | "notification"; -} - -export interface CreateTalerWireGatewayFacadeRequest { - name: string; - connectionName: string; - accountName: string; - currency: string; - reserveTransferLevel: "report" | "statement" | "notification"; -} - -export interface SandboxAccountTransactions { - payments: { - accountLabel: string; - creditorIban: string; - creditorBic?: string; - creditorName: string; - debtorIban: string; - debtorBic: string; - debtorName: string; - amount: string; - currency: string; - subject: string; - date: string; - creditDebitIndicator: "debit" | "credit"; - accountServicerReference: string; - }[]; -} - -export interface DeleteBankConnectionRequest { - bankConnectionId: string; -} - -export interface SimulateIncomingTransactionRequest { - debtorIban: string; - debtorBic: string; - debtorName: string; - - /** - * Subject / unstructured remittance info. - */ - subject: string; - - /** - * Decimal amount without currency. - */ - amount: string; -} - -export interface CreateEbicsBankAccountRequest { - subscriber: { - hostID: string; - partnerID: string; - userID: string; - systemID?: string; - }; - // IBAN - iban: string; - // BIC - bic: string; - // human name - name: string; - label: string; -} - -export interface LibeufinSandboxAddIncomingRequest { - creditorIban: string; - creditorBic: string; - creditorName: string; - debtorIban: string; - debtorBic: string; - debtorName: string; - subject: string; - amount: string; - currency: string; - uid: string; - direction: string; -} - -function getRandomString(): string { - return Math.random().toString(36).substring(2); -} - -/** - * APIs spread accross Legacy and Access, it is therefore - * the "base URL" relative to which API every call addresses. - */ -export namespace LibeufinSandboxApi { - // Need Access API base URL. - export async function demobankAccountInfo( - username: string, - password: string, - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ) { - let url = new URL( - `accounts/${accountLabel}`, - libeufinSandboxService.baseUrl, - ); - return await axios.get(url.href, { - auth: { - username: username, - password: password, - }, - }); - } - - // Creates one bank account via the Access API. - // Need the /demobanks/$id/access-api as the base URL - export async function createDemobankAccount( - username: string, - password: string, - libeufinSandboxService: LibeufinSandboxServiceInterface, - iban: string|null = null, - ) { - let url = new URL( - "testing/register", - libeufinSandboxService.baseUrl - ); - await axios.post(url.href, { - username: username, - password: password, - iban: iban - }); - } - // Need /demobanks/$id as the base URL - export async function createDemobankEbicsSubscriber( - req: CreateEbicsSubscriberRequest, - demobankAccountLabel: string, - libeufinSandboxService: LibeufinSandboxServiceInterface, - username: string = "admin", - password: string = "secret", - ) { - // baseUrl should already be pointed to one demobank. - let url = new URL( - "ebics/subscribers", - libeufinSandboxService.baseUrl - ); - await axios.post( - url.href, - { - userID: req.userID, - hostID: req.hostID, - partnerID: req.partnerID, - demobankAccountLabel: demobankAccountLabel, - }, - { - auth: { - username: "admin", - password: "secret", - }, - }, - ); - } - - export async function rotateKeys( - libeufinSandboxService: LibeufinSandboxServiceInterface, - hostID: string, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL(`admin/ebics/hosts/${hostID}/rotate-keys`, baseUrl); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "secret", - }, - }, - ); - } - export async function createEbicsHost( - libeufinSandboxService: LibeufinSandboxServiceInterface, - hostID: string, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/ebics/hosts", baseUrl); - await axios.post( - url.href, - { - hostID, - ebicsVersion: "2.5", - }, - { - auth: { - username: "admin", - password: "secret", - }, - }, - ); - } - - export async function createBankAccount( - libeufinSandboxService: LibeufinSandboxServiceInterface, - req: BankAccountInfo, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL(`admin/bank-accounts/${req.label}`, baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - /** - * This function is useless. It creates a Ebics subscriber - * but never gives it a bank account. To be removed - */ - export async function createEbicsSubscriber( - libeufinSandboxService: LibeufinSandboxServiceInterface, - req: CreateEbicsSubscriberRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/ebics/subscribers", baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - /** - * Create a new bank account and associate it to - * a existing EBICS subscriber. - */ - export async function createEbicsBankAccount( - libeufinSandboxService: LibeufinSandboxServiceInterface, - req: CreateEbicsBankAccountRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/ebics/bank-accounts", baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function simulateIncomingTransaction( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - req: SimulateIncomingTransactionRequest, - ) { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL( - `admin/bank-accounts/${accountLabel}/simulate-incoming-transaction`, - baseUrl, - ); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "secret", - }, - }); - } - - export async function getAccountTransactions( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ): Promise { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL( - `admin/bank-accounts/${accountLabel}/transactions`, - baseUrl, - ); - const res = await axios.get(url.href, { - auth: { - username: "admin", - password: "secret", - }, - }); - return res.data as SandboxAccountTransactions; - } - - export async function getCamt053( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ): Promise { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL("admin/payments/camt", baseUrl); - return await axios.post( - url.href, - { - bankaccount: accountLabel, - type: 53, - }, - { - auth: { - username: "admin", - password: "secret", - }, - }, - ); - } - - export async function getAccountInfoWithBalance( - libeufinSandboxService: LibeufinSandboxServiceInterface, - accountLabel: string, - ): Promise { - const baseUrl = libeufinSandboxService.baseUrl; - let url = new URL(`admin/bank-accounts/${accountLabel}`, baseUrl); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "secret", - }, - }); - } -} - -export namespace LibeufinNexusApi { - export async function getAllConnections( - nexus: LibeufinNexusServiceInterface, - ): Promise { - let url = new URL("bank-connections", nexus.baseUrl); - const res = await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - return res; - } - - export async function deleteBankConnection( - libeufinNexusService: LibeufinNexusServiceInterface, - req: DeleteBankConnectionRequest, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("bank-connections/delete-connection", baseUrl); - return await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function createEbicsBankConnection( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateEbicsBankConnectionRequest, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("bank-connections", baseUrl); - await axios.post( - url.href, - { - source: "new", - type: "ebics", - name: req.name, - data: { - ebicsURL: req.ebicsURL, - hostID: req.hostID, - userID: req.userID, - partnerID: req.partnerID, - systemID: req.systemID, - }, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function getBankAccount( - libeufinNexusService: LibeufinNexusServiceInterface, - accountName: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`bank-accounts/${accountName}`, baseUrl); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function submitInitiatedPayment( - libeufinNexusService: LibeufinNexusServiceInterface, - accountName: string, - paymentId: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-accounts/${accountName}/payment-initiations/${paymentId}/submit`, - baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function fetchAccounts( - libeufinNexusService: LibeufinNexusServiceInterface, - connectionName: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-connections/${connectionName}/fetch-accounts`, - baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function importConnectionAccount( - libeufinNexusService: LibeufinNexusServiceInterface, - connectionName: string, - offeredAccountId: string, - nexusBankAccountId: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `bank-connections/${connectionName}/import-account`, - baseUrl, - ); - await axios.post( - url.href, - { - offeredAccountId, - nexusBankAccountId, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function connectBankConnection( - libeufinNexusService: LibeufinNexusServiceInterface, - connectionName: string, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`bank-connections/${connectionName}/connect`, baseUrl); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function getPaymentInitiations( - libeufinNexusService: LibeufinNexusServiceInterface, - accountName: string, - username: string = "admin", - password: string = "test", - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${accountName}/payment-initiations`, - baseUrl, - ); - let response = await axios.get(url.href, { - auth: { - username: username, - password: password, - }, - }); - console.log( - `Payment initiations of: ${accountName}`, - JSON.stringify(response.data, null, 2), - ); - } - - export async function getConfig( - libeufinNexusService: LibeufinNexusServiceInterface, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/config`, baseUrl); - let response = await axios.get(url.href); - } - - // Uses the Anastasis API to get a list of transactions. - export async function getAnastasisTransactions( - libeufinNexusService: LibeufinNexusServiceInterface, - anastasisBaseUrl: string, - params: {}, // of the request: {delta: 5, ..} - username: string = "admin", - password: string = "test", - ): Promise { - let url = new URL("history/incoming", anastasisBaseUrl); - let response = await axios.get(url.href, { - params: params, - auth: { - username: username, - password: password, - }, - }); - return response; - } - - // FIXME: this function should return some structured - // object that represents a history. - export async function getAccountTransactions( - libeufinNexusService: LibeufinNexusServiceInterface, - accountName: string, - username: string = "admin", - password: string = "test", - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/bank-accounts/${accountName}/transactions`, baseUrl); - let response = await axios.get(url.href, { - auth: { - username: username, - password: password, - }, - }); - return response; - } - - export async function fetchTransactions( - libeufinNexusService: LibeufinNexusServiceInterface, - accountName: string, - rangeType: string = "all", - level: string = "report", - username: string = "admin", - password: string = "test", - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${accountName}/fetch-transactions`, - baseUrl, - ); - return await axios.post( - url.href, - { - rangeType: rangeType, - level: level, - }, - { - auth: { - username: username, - password: password, - }, - }, - ); - } - - export async function changePassword( - libeufinNexusService: LibeufinNexusServiceInterface, - username: string, - req: UpdateNexusUserRequest, - auth: NexusAuth, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/users/${username}/password`, baseUrl); - await axios.post(url.href, req, auth); - } - - export async function getUser( - libeufinNexusService: LibeufinNexusServiceInterface, - auth: NexusAuth, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/user`, baseUrl); - return await axios.get(url.href, auth); - } - - export async function createUser( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateNexusUserRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/users`, baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function getAllPermissions( - libeufinNexusService: LibeufinNexusServiceInterface, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/permissions`, baseUrl); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function postPermission( - libeufinNexusService: LibeufinNexusServiceInterface, - req: PostNexusPermissionRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/permissions`, baseUrl); - await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function getTasks( - libeufinNexusService: LibeufinNexusServiceInterface, - bankAccountName: string, - // When void, the request returns the list of all the - // tasks under this bank account. - taskName: string | void, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); - if (taskName) url = new URL(taskName, `${url.href}/`); - - // It's caller's responsibility to interpret the response. - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function deleteTask( - libeufinNexusService: LibeufinNexusServiceInterface, - bankAccountName: string, - taskName: string, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${bankAccountName}/schedule/${taskName}`, - baseUrl, - ); - await axios.delete(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function postTask( - libeufinNexusService: LibeufinNexusServiceInterface, - bankAccountName: string, - req: PostNexusTaskRequest, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`/bank-accounts/${bankAccountName}/schedule`, baseUrl); - return await axios.post(url.href, req, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function deleteFacade( - libeufinNexusService: LibeufinNexusServiceInterface, - facadeName: string, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL(`facades/${facadeName}`, baseUrl); - return await axios.delete(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function getAllFacades( - libeufinNexusService: LibeufinNexusServiceInterface, - ): Promise { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("facades", baseUrl); - return await axios.get(url.href, { - auth: { - username: "admin", - password: "test", - }, - }); - } - - export async function createAnastasisFacade( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateAnastasisFacadeRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("facades", baseUrl); - await axios.post( - url.href, - { - name: req.name, - type: "anastasis", - config: { - bankAccount: req.accountName, - bankConnection: req.connectionName, - currency: req.currency, - reserveTransferLevel: req.reserveTransferLevel, - }, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function createTwgFacade( - libeufinNexusService: LibeufinNexusServiceInterface, - req: CreateTalerWireGatewayFacadeRequest, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL("facades", baseUrl); - await axios.post( - url.href, - { - name: req.name, - type: "taler-wire-gateway", - config: { - bankAccount: req.accountName, - bankConnection: req.connectionName, - currency: req.currency, - reserveTransferLevel: req.reserveTransferLevel, - }, - }, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } - - export async function submitAllPaymentInitiations( - libeufinNexusService: LibeufinNexusServiceInterface, - accountId: string, - ) { - const baseUrl = libeufinNexusService.baseUrl; - let url = new URL( - `/bank-accounts/${accountId}/submit-all-payment-initiations`, - baseUrl, - ); - await axios.post( - url.href, - {}, - { - auth: { - username: "admin", - password: "test", - }, - }, - ); - } -} diff --git a/packages/taler-wallet-cli/src/harness/libeufin.ts b/packages/taler-wallet-cli/src/harness/libeufin.ts deleted file mode 100644 index 638c8ed90..000000000 --- a/packages/taler-wallet-cli/src/harness/libeufin.ts +++ /dev/null @@ -1,910 +0,0 @@ -/* - 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 - */ - -/** - * This file defines euFin test logic that needs state - * and that depends on the main harness.ts. The other - * definitions - mainly helper functions to call RESTful - * APIs - moved to libeufin-apis.ts. That enables harness.ts - * to depend on such API calls, in contrast to the previous - * situation where harness.ts had to include this file causing - * a circular dependency. */ - -/** - * Imports. - */ -import axios from "axios"; -import { URL, Logger } from "@gnu-taler/taler-util"; -import { - GlobalTestState, - DbInfo, - pingProc, - ProcessWrapper, - runCommand, - setupDb, - sh, - getRandomIban, -} from "../harness/harness.js"; -import { - LibeufinSandboxApi, - LibeufinNexusApi, - CreateEbicsBankAccountRequest, - LibeufinSandboxServiceInterface, - CreateTalerWireGatewayFacadeRequest, - SimulateIncomingTransactionRequest, - SandboxAccountTransactions, - DeleteBankConnectionRequest, - CreateEbicsBankConnectionRequest, - UpdateNexusUserRequest, - NexusAuth, - CreateAnastasisFacadeRequest, - PostNexusTaskRequest, - PostNexusPermissionRequest, - CreateNexusUserRequest, -} from "../harness/libeufin-apis.js"; - - -const logger = new Logger("libeufin.ts"); - -export { LibeufinSandboxApi, LibeufinNexusApi }; - -export interface LibeufinServices { - libeufinSandbox: LibeufinSandboxService; - libeufinNexus: LibeufinNexusService; - commonDb: DbInfo; -} - -export interface LibeufinSandboxConfig { - httpPort: number; - databaseJdbcUri: string; -} - -export interface LibeufinNexusConfig { - httpPort: number; - databaseJdbcUri: string; -} - -interface LibeufinNexusMoneyMovement { - amount: string; - creditDebitIndicator: string; - details: { - debtor: { - name: string; - }; - debtorAccount: { - iban: string; - }; - debtorAgent: { - bic: string; - }; - creditor: { - name: string; - }; - creditorAccount: { - iban: string; - }; - creditorAgent: { - bic: string; - }; - endToEndId: string; - unstructuredRemittanceInformation: string; - }; -} - -interface LibeufinNexusBatches { - batchTransactions: Array; -} - -interface LibeufinNexusTransaction { - amount: string; - creditDebitIndicator: string; - status: string; - bankTransactionCode: string; - valueDate: string; - bookingDate: string; - accountServicerRef: string; - batches: Array; -} - -interface LibeufinNexusTransactions { - transactions: Array; -} - -export interface LibeufinCliDetails { - nexusUrl: string; - sandboxUrl: string; - nexusDatabaseUri: string; - sandboxDatabaseUri: string; - nexusUser: LibeufinNexusUser; -} - -export interface LibeufinEbicsSubscriberDetails { - hostId: string; - partnerId: string; - userId: string; -} - -export interface LibeufinEbicsConnectionDetails { - subscriberDetails: LibeufinEbicsSubscriberDetails; - ebicsUrl: string; - connectionName: string; -} - -export interface LibeufinBankAccountDetails { - currency: string; - iban: string; - bic: string; - personName: string; - accountName: string; -} - -export interface LibeufinNexusUser { - username: string; - password: string; -} - -export interface LibeufinBackupFileDetails { - passphrase: string; - outputFile: string; - connectionName: string; -} - -export interface LibeufinKeyLetterDetails { - outputFile: string; - connectionName: string; -} - -export interface LibeufinBankAccountImportDetails { - offeredBankAccountName: string; - nexusBankAccountName: string; - connectionName: string; -} - -export interface LibeufinPreparedPaymentDetails { - creditorIban: string; - creditorBic: string; - creditorName: string; - subject: string; - amount: string; - currency: string; - nexusBankAccountName: string; -} - -export class LibeufinSandboxService implements LibeufinSandboxServiceInterface { - static async create( - gc: GlobalTestState, - sandboxConfig: LibeufinSandboxConfig, - ): Promise { - return new LibeufinSandboxService(gc, sandboxConfig); - } - - sandboxProc: ProcessWrapper | undefined; - globalTestState: GlobalTestState; - - constructor( - gc: GlobalTestState, - private sandboxConfig: LibeufinSandboxConfig, - ) { - this.globalTestState = gc; - } - - get baseUrl(): string { - return `http://localhost:${this.sandboxConfig.httpPort}/`; - } - - async start(): Promise { - await sh( - this.globalTestState, - "libeufin-sandbox-config", - "libeufin-sandbox config default", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - }, - ); - - this.sandboxProc = this.globalTestState.spawnService( - "libeufin-sandbox", - ["serve", "--port", `${this.sandboxConfig.httpPort}`], - "libeufin-sandbox", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", - }, - ); - } - - async c53tick(): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-sandbox-c53tick", - "libeufin-sandbox camt053tick", - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - }, - ); - return stdout; - } - - async makeTransaction( - debit: string, - credit: string, - amount: string, // $currency:x.y - subject: string, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-sandbox-maketransfer", - `libeufin-sandbox make-transaction --debit-account=${debit} --credit-account=${credit} ${amount} "${subject}"`, - { - ...process.env, - LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri, - }, - ); - return stdout; - } - - async pingUntilAvailable(): Promise { - const url = this.baseUrl; - await pingProc(this.sandboxProc, url, "libeufin-sandbox"); - } -} - -export class LibeufinNexusService { - static async create( - gc: GlobalTestState, - nexusConfig: LibeufinNexusConfig, - ): Promise { - return new LibeufinNexusService(gc, nexusConfig); - } - - nexusProc: ProcessWrapper | undefined; - globalTestState: GlobalTestState; - - constructor(gc: GlobalTestState, private nexusConfig: LibeufinNexusConfig) { - this.globalTestState = gc; - } - - get baseUrl(): string { - return `http://localhost:${this.nexusConfig.httpPort}/`; - } - - async start(): Promise { - await runCommand( - this.globalTestState, - "libeufin-nexus-superuser", - "libeufin-nexus", - ["superuser", "admin", "--password", "test"], - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, - }, - ); - - this.nexusProc = this.globalTestState.spawnService( - "libeufin-nexus", - ["serve", "--port", `${this.nexusConfig.httpPort}`], - "libeufin-nexus", - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, - }, - ); - } - - async pingUntilAvailable(): Promise { - const url = `${this.baseUrl}config`; - await pingProc(this.nexusProc, url, "libeufin-nexus"); - } - - async createNexusSuperuser(details: LibeufinNexusUser): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-nexus", - `libeufin-nexus superuser ${details.username} --password=${details.password}`, - { - ...process.env, - LIBEUFIN_NEXUS_DB_CONNECTION: this.nexusConfig.databaseJdbcUri, - }, - ); - console.log(stdout); - } -} - -export interface TwgAddIncomingRequest { - amount: string; - reserve_pub: string; - debit_account: string; -} - -/** - * The bundle aims at minimizing the amount of input - * data that is required to initialize a new user + Ebics - * connection. - */ -export class NexusUserBundle { - userReq: CreateNexusUserRequest; - connReq: CreateEbicsBankConnectionRequest; - anastasisReq: CreateAnastasisFacadeRequest; - twgReq: CreateTalerWireGatewayFacadeRequest; - twgTransferPermission: PostNexusPermissionRequest; - twgHistoryPermission: PostNexusPermissionRequest; - twgAddIncomingPermission: PostNexusPermissionRequest; - localAccountName: string; - remoteAccountName: string; - - constructor(salt: string, ebicsURL: string) { - this.userReq = { - username: `username-${salt}`, - password: `password-${salt}`, - }; - - this.connReq = { - name: `connection-${salt}`, - ebicsURL: ebicsURL, - hostID: `ebicshost,${salt}`, - partnerID: `ebicspartner,${salt}`, - userID: `ebicsuser,${salt}`, - }; - - this.twgReq = { - currency: "EUR", - name: `twg-${salt}`, - reserveTransferLevel: "report", - accountName: `local-account-${salt}`, - connectionName: `connection-${salt}`, - }; - this.anastasisReq = { - currency: "EUR", - name: `anastasis-${salt}`, - reserveTransferLevel: "report", - accountName: `local-account-${salt}`, - connectionName: `connection-${salt}`, - }; - this.remoteAccountName = `remote-account-${salt}`; - this.localAccountName = `local-account-${salt}`; - this.twgTransferPermission = { - action: "grant", - permission: { - subjectId: `username-${salt}`, - subjectType: "user", - resourceType: "facade", - resourceId: `twg-${salt}`, - permissionName: "facade.talerWireGateway.transfer", - }, - }; - this.twgHistoryPermission = { - action: "grant", - permission: { - subjectId: `username-${salt}`, - subjectType: "user", - resourceType: "facade", - resourceId: `twg-${salt}`, - permissionName: "facade.talerWireGateway.history", - }, - }; - } -} - -/** - * The bundle aims at minimizing the amount of input - * data that is required to initialize a new Sandbox - * customer, associating their bank account with a Ebics - * subscriber. - */ -export class SandboxUserBundle { - ebicsBankAccount: CreateEbicsBankAccountRequest; - constructor(salt: string) { - this.ebicsBankAccount = { - bic: "BELADEBEXXX", - iban: getRandomIban(), - label: `remote-account-${salt}`, - name: `Taler Exchange: ${salt}`, - subscriber: { - hostID: `ebicshost,${salt}`, - partnerID: `ebicspartner,${salt}`, - userID: `ebicsuser,${salt}`, - }, - }; - } -} - -export class LibeufinCli { - cliDetails: LibeufinCliDetails; - globalTestState: GlobalTestState; - - constructor(gc: GlobalTestState, cd: LibeufinCliDetails) { - this.globalTestState = gc; - this.cliDetails = cd; - } - - env(): any { - return { - ...process.env, - LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl, - LIBEUFIN_SANDBOX_USERNAME: "admin", - LIBEUFIN_SANDBOX_PASSWORD: "secret", - }; - } - - async checkSandbox(): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-checksandbox", - "libeufin-cli sandbox check", - this.env(), - ); - } - - async registerBankCustomer(username: string, password: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-registercustomer", - "libeufin-cli sandbox demobank register --name='Test Customer'", - { - ...process.env, - LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl + "/demobanks/default", - LIBEUFIN_SANDBOX_USERNAME: username, - LIBEUFIN_SANDBOX_PASSWORD: password, - }, - ); - console.log(stdout); - } - - async createEbicsHost(hostId: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicshost", - `libeufin-cli sandbox ebicshost create --host-id=${hostId}`, - this.env(), - ); - console.log(stdout); - } - - async createEbicsSubscriber( - details: LibeufinEbicsSubscriberDetails, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicssubscriber", - "libeufin-cli sandbox ebicssubscriber create" + - ` --host-id=${details.hostId}` + - ` --partner-id=${details.partnerId}` + - ` --user-id=${details.userId}`, - this.env(), - ); - console.log(stdout); - } - - async createEbicsBankAccount( - sd: LibeufinEbicsSubscriberDetails, - bankAccountDetails: LibeufinBankAccountDetails, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicsbankaccount", - "libeufin-cli sandbox ebicsbankaccount create" + - ` --iban=${bankAccountDetails.iban}` + - ` --bic=${bankAccountDetails.bic}` + - ` --person-name='${bankAccountDetails.personName}'` + - ` --account-name=${bankAccountDetails.accountName}` + - ` --ebics-host-id=${sd.hostId}` + - ` --ebics-partner-id=${sd.partnerId}` + - ` --ebics-user-id=${sd.userId}`, - this.env(), - ); - console.log(stdout); - } - - async generateTransactions(accountName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-generatetransactions", - `libeufin-cli sandbox bankaccount generate-transactions ${accountName}`, - this.env(), - ); - console.log(stdout); - } - - async showSandboxTransactions(accountName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-showsandboxtransactions", - `libeufin-cli sandbox bankaccount transactions ${accountName}`, - this.env(), - ); - console.log(stdout); - } - - async createEbicsConnection( - connectionDetails: LibeufinEbicsConnectionDetails, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createebicsconnection", - `libeufin-cli connections new-ebics-connection` + - ` --ebics-url=${connectionDetails.ebicsUrl}` + - ` --host-id=${connectionDetails.subscriberDetails.hostId}` + - ` --partner-id=${connectionDetails.subscriberDetails.partnerId}` + - ` --ebics-user-id=${connectionDetails.subscriberDetails.userId}` + - ` ${connectionDetails.connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async createBackupFile(details: LibeufinBackupFileDetails): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createbackupfile", - `libeufin-cli connections export-backup` + - ` --passphrase=${details.passphrase}` + - ` --output-file=${details.outputFile}` + - ` ${details.connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async createKeyLetter(details: LibeufinKeyLetterDetails): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-createkeyletter", - `libeufin-cli connections get-key-letter` + - ` ${details.connectionName} ${details.outputFile}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async connect(connectionName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-connect", - `libeufin-cli connections connect ${connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async downloadBankAccounts(connectionName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-downloadbankaccounts", - `libeufin-cli connections download-bank-accounts ${connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async listOfferedBankAccounts(connectionName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-listofferedbankaccounts", - `libeufin-cli connections list-offered-bank-accounts ${connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async importBankAccount( - importDetails: LibeufinBankAccountImportDetails, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-importbankaccount", - "libeufin-cli connections import-bank-account" + - ` --offered-account-id=${importDetails.offeredBankAccountName}` + - ` --nexus-bank-account-id=${importDetails.nexusBankAccountName}` + - ` ${importDetails.connectionName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async fetchTransactions(bankAccountName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-fetchtransactions", - `libeufin-cli accounts fetch-transactions ${bankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async transactions(bankAccountName: string): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-transactions", - `libeufin-cli accounts transactions ${bankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async preparePayment(details: LibeufinPreparedPaymentDetails): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-preparepayment", - `libeufin-cli accounts prepare-payment` + - ` --creditor-iban=${details.creditorIban}` + - ` --creditor-bic=${details.creditorBic}` + - ` --creditor-name='${details.creditorName}'` + - ` --payment-subject='${details.subject}'` + - ` --payment-amount=${details.currency}:${details.amount}` + - ` ${details.nexusBankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async submitPayment( - details: LibeufinPreparedPaymentDetails, - paymentUuid: string, - ): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-submitpayments", - `libeufin-cli accounts submit-payments` + - ` --payment-uuid=${paymentUuid}` + - ` ${details.nexusBankAccountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async newAnastasisFacade(req: NewAnastasisFacadeReq): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-new-anastasis-facade", - `libeufin-cli facades new-anastasis-facade` + - ` --currency ${req.currency}` + - ` --facade-name ${req.facadeName}` + - ` ${req.connectionName} ${req.accountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async newTalerWireGatewayFacade(req: NewTalerWireGatewayReq): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-new-taler-wire-gateway-facade", - `libeufin-cli facades new-taler-wire-gateway-facade` + - ` --currency ${req.currency}` + - ` --facade-name ${req.facadeName}` + - ` ${req.connectionName} ${req.accountName}`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } - - async listFacades(): Promise { - const stdout = await sh( - this.globalTestState, - "libeufin-cli-facades-list", - `libeufin-cli facades list`, - { - ...process.env, - LIBEUFIN_NEXUS_URL: this.cliDetails.nexusUrl, - LIBEUFIN_NEXUS_USERNAME: this.cliDetails.nexusUser.username, - LIBEUFIN_NEXUS_PASSWORD: this.cliDetails.nexusUser.password, - }, - ); - console.log(stdout); - } -} - -interface NewAnastasisFacadeReq { - facadeName: string; - connectionName: string; - accountName: string; - currency: string; -} - -interface NewTalerWireGatewayReq { - facadeName: string; - connectionName: string; - accountName: string; - currency: string; -} - -/** - * Launch Nexus and Sandbox AND creates users / facades / bank accounts / - * .. all that's required to start making bank traffic. - */ -export async function launchLibeufinServices( - t: GlobalTestState, - nexusUserBundle: NexusUserBundle[], - sandboxUserBundle: SandboxUserBundle[] = [], - withFacades: string[] = [], // takes only "twg" and/or "anastasis" -): Promise { - 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(); - console.log("Libeufin services launched!"); - - for (let sb of sandboxUserBundle) { - await LibeufinSandboxApi.createEbicsHost( - libeufinSandbox, - sb.ebicsBankAccount.subscriber.hostID, - ); - await LibeufinSandboxApi.createEbicsSubscriber( - libeufinSandbox, - sb.ebicsBankAccount.subscriber, - ); - await LibeufinSandboxApi.createDemobankAccount( - sb.ebicsBankAccount.label, - "password-unused", - { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/access-api/" } - ); - await LibeufinSandboxApi.createDemobankEbicsSubscriber( - sb.ebicsBankAccount.subscriber, - sb.ebicsBankAccount.label, - { baseUrl: libeufinSandbox.baseUrl + "/demobanks/default/" } - ); - } - console.log("Sandbox user(s) / account(s) / subscriber(s): created"); - - for (let nb of nexusUserBundle) { - await LibeufinNexusApi.createEbicsBankConnection(libeufinNexus, nb.connReq); - await LibeufinNexusApi.connectBankConnection( - libeufinNexus, - nb.connReq.name, - ); - await LibeufinNexusApi.fetchAccounts(libeufinNexus, nb.connReq.name); - await LibeufinNexusApi.importConnectionAccount( - libeufinNexus, - nb.connReq.name, - nb.remoteAccountName, - nb.localAccountName, - ); - await LibeufinNexusApi.createUser(libeufinNexus, nb.userReq); - for (let facade of withFacades) { - switch (facade) { - case "twg": - await LibeufinNexusApi.createTwgFacade(libeufinNexus, nb.twgReq); - await LibeufinNexusApi.postPermission( - libeufinNexus, - nb.twgTransferPermission, - ); - await LibeufinNexusApi.postPermission( - libeufinNexus, - nb.twgHistoryPermission, - ); - break; - case "anastasis": - await LibeufinNexusApi.createAnastasisFacade( - libeufinNexus, - nb.anastasisReq, - ); - } - } - } - console.log( - "Nexus user(s) / connection(s) / facade(s) / permission(s): created", - ); - - return { - commonDb: db, - libeufinNexus: libeufinNexus, - libeufinSandbox: libeufinSandbox, - }; -} - -/** - * Helper function that searches a payment among - * a list, as returned by Nexus. The key is just - * the payment subject. - */ -export function findNexusPayment( - key: string, - payments: LibeufinNexusTransactions, -): LibeufinNexusMoneyMovement | void { - let transactions = payments["transactions"]; - for (let i = 0; i < transactions.length; i++) { - let batches = transactions[i]["batches"]; - for (let y = 0; y < batches.length; y++) { - let movements = batches[y]["batchTransactions"]; - for (let z = 0; z < movements.length; z++) { - let movement = movements[z]; - if (movement["details"]["unstructuredRemittanceInformation"] == key) - return movement; - } - } - } -} diff --git a/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts b/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts deleted file mode 100644 index 2a59b0160..000000000 --- a/packages/taler-wallet-cli/src/harness/merchantApiTypes.ts +++ /dev/null @@ -1,337 +0,0 @@ -/* - 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 - */ - -/** - * Test harness for various GNU Taler components. - * Also provides a fault-injection proxy. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { - MerchantContractTerms, - Duration, - Codec, - buildCodecForObject, - codecForString, - codecOptional, - codecForConstString, - codecForBoolean, - codecForNumber, - codecForMerchantContractTerms, - codecForAny, - buildCodecForUnion, - AmountString, - AbsoluteTime, - CoinPublicKeyString, - EddsaPublicKeyString, - codecForAmountString, - TalerProtocolDuration, - codecForTimestamp, - TalerProtocolTimestamp, -} from "@gnu-taler/taler-util"; - -export interface PostOrderRequest { - // The order must at least contain the minimal - // order detail, but can override all - order: Partial; - - // if set, the backend will then set the refund deadline to the current - // time plus the specified delay. - refund_delay?: TalerProtocolDuration; - - // specifies the payment target preferred by the client. Can be used - // to select among the various (active) wire methods supported by the instance. - payment_target?: string; - - // FIXME: some fields are missing - - // Should a token for claiming the order be generated? - // False can make sense if the ORDER_ID is sufficiently - // high entropy to prevent adversarial claims (like it is - // if the backend auto-generates one). Default is 'true'. - create_token?: boolean; -} - -export type ClaimToken = string; - -export interface PostOrderResponse { - order_id: string; - token?: ClaimToken; -} - -export const codecForPostOrderResponse = (): Codec => - buildCodecForObject() - .property("order_id", codecForString()) - .property("token", codecOptional(codecForString())) - .build("PostOrderResponse"); - - -export const codecForRefundDetails = (): Codec => - buildCodecForObject() - .property("reason", codecForString()) - .property("pending", codecForBoolean()) - .property("amount", codecForString()) - .property("timestamp", codecForTimestamp) - .build("PostOrderResponse"); - -export const codecForCheckPaymentPaidResponse = - (): Codec => - buildCodecForObject() - .property("order_status_url", codecForString()) - .property("order_status", codecForConstString("paid")) - .property("refunded", codecForBoolean()) - .property("wired", codecForBoolean()) - .property("deposit_total", codecForAmountString()) - .property("exchange_ec", codecForNumber()) - .property("exchange_hc", codecForNumber()) - .property("refund_amount", codecForAmountString()) - .property("contract_terms", codecForMerchantContractTerms()) - // FIXME: specify - .property("wire_details", codecForAny()) - .property("wire_reports", codecForAny()) - .property("refund_details", codecForAny()) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentUnpaidResponse = - (): Codec => - buildCodecForObject() - .property("order_status", codecForConstString("unpaid")) - .property("taler_pay_uri", codecForString()) - .property("order_status_url", codecForString()) - .property("already_paid_order_id", codecOptional(codecForString())) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentClaimedResponse = - (): Codec => - buildCodecForObject() - .property("order_status", codecForConstString("claimed")) - .property("contract_terms", codecForMerchantContractTerms()) - .build("CheckPaymentClaimedResponse"); - -export const codecForMerchantOrderPrivateStatusResponse = - (): Codec => - buildCodecForUnion() - .discriminateOn("order_status") - .alternative("paid", codecForCheckPaymentPaidResponse()) - .alternative("unpaid", codecForCheckPaymentUnpaidResponse()) - .alternative("claimed", codecForCheckPaymentClaimedResponse()) - .build("MerchantOrderPrivateStatusResponse"); - -export type MerchantOrderPrivateStatusResponse = - | CheckPaymentPaidResponse - | CheckPaymentUnpaidResponse - | CheckPaymentClaimedResponse; - -export interface CheckPaymentClaimedResponse { - // Wallet claimed the order, but didn't pay yet. - order_status: "claimed"; - - contract_terms: MerchantContractTerms; -} - -export interface CheckPaymentPaidResponse { - // did the customer pay for this contract - order_status: "paid"; - - // Was the payment refunded (even partially) - refunded: boolean; - - // Did the exchange wire us the funds - wired: boolean; - - // Total amount the exchange deposited into our bank account - // for this contract, excluding fees. - deposit_total: AmountString; - - // Numeric error code indicating errors the exchange - // encountered tracking the wire transfer for this purchase (before - // we even got to specific coin issues). - // 0 if there were no issues. - exchange_ec: number; - - // HTTP status code returned by the exchange when we asked for - // information to track the wire transfer for this purchase. - // 0 if there were no issues. - exchange_hc: number; - - // Total amount that was refunded, 0 if refunded is false. - refund_amount: AmountString; - - // Contract terms - contract_terms: MerchantContractTerms; - - // Ihe wire transfer status from the exchange for this order if available, otherwise empty array - wire_details: TransactionWireTransfer[]; - - // Reports about trouble obtaining wire transfer details, empty array if no trouble were encountered. - wire_reports: TransactionWireReport[]; - - // The refund details for this order. One entry per - // refunded coin; empty array if there are no refunds. - refund_details: RefundDetails[]; - - order_status_url: string; -} - -export interface CheckPaymentUnpaidResponse { - order_status: "unpaid"; - - // URI that the wallet must process to complete the payment. - taler_pay_uri: string; - - order_status_url: string; - - // Alternative order ID which was paid for already in the same session. - // Only given if the same product was purchased before in the same session. - already_paid_order_id?: string; - - // We do we NOT return the contract terms here because they may not - // exist in case the wallet did not yet claim them. -} - -export interface RefundDetails { - // Reason given for the refund - reason: string; - - // when was the refund approved - timestamp: TalerProtocolTimestamp; - - // has not been taken yet - pending: boolean; - - // Total amount that was refunded (minus a refund fee). - amount: AmountString; -} - -export interface TransactionWireTransfer { - // Responsible exchange - exchange_url: string; - - // 32-byte wire transfer identifier - wtid: string; - - // execution time of the wire transfer - execution_time: AbsoluteTime; - - // Total amount that has been wire transferred - // to the merchant - amount: AmountString; - - // Was this transfer confirmed by the merchant via the - // POST /transfers API, or is it merely claimed by the exchange? - confirmed: boolean; -} - -export interface TransactionWireReport { - // Numerical error code - code: number; - - // Human-readable error description - hint: string; - - // Numerical error code from the exchange. - exchange_ec: number; - - // HTTP status code received from the exchange. - exchange_hc: number; - - // Public key of the coin for which we got the exchange error. - coin_pub: CoinPublicKeyString; -} - -export interface TippingReserveStatus { - // Array of all known reserves (possibly empty!) - reserves: ReserveStatusEntry[]; -} - -export interface ReserveStatusEntry { - // Public key of the reserve - reserve_pub: string; - - // Timestamp when it was established - creation_time: AbsoluteTime; - - // Timestamp when it expires - expiration_time: AbsoluteTime; - - // Initial amount as per reserve creation call - merchant_initial_amount: AmountString; - - // Initial amount as per exchange, 0 if exchange did - // not confirm reserve creation yet. - exchange_initial_amount: AmountString; - - // Amount picked up so far. - pickup_amount: AmountString; - - // Amount approved for tips that exceeds the pickup_amount. - committed_amount: AmountString; - - // Is this reserve active (false if it was deleted but not purged) - active: boolean; -} - -export interface TipCreateConfirmation { - // Unique tip identifier for the tip that was created. - tip_id: string; - - // taler://tip URI for the tip - taler_tip_uri: string; - - // URL that will directly trigger processing - // the tip when the browser is redirected to it - tip_status_url: string; - - // when does the tip expire - tip_expiration: AbsoluteTime; -} - -export interface TipCreateRequest { - // Amount that the customer should be tipped - amount: AmountString; - - // Justification for giving the tip - justification: string; - - // URL that the user should be directed to after tipping, - // will be included in the tip_token. - next_url: string; -} - -export interface MerchantInstancesResponse { - // List of instances that are present in the backend (see Instance) - instances: MerchantInstanceDetail[]; -} - -export interface MerchantInstanceDetail { - // Merchant name corresponding to this instance. - name: string; - - // Merchant instance this response is about ($INSTANCE) - id: string; - - // Public key of the merchant/instance, in Crockford Base32 encoding. - merchant_pub: EddsaPublicKeyString; - - // List of the payment targets supported by this instance. Clients can - // specify the desired payment target in /order requests. Note that - // front-ends do not have to support wallets selecting payment targets. - payment_targets: string[]; -} diff --git a/packages/taler-wallet-cli/src/harness/sync.ts b/packages/taler-wallet-cli/src/harness/sync.ts deleted file mode 100644 index a9e8de412..000000000 --- a/packages/taler-wallet-cli/src/harness/sync.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - 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 - */ - -/** - * Imports. - */ -import { URL } from "@gnu-taler/taler-util"; -import * as fs from "fs"; -import * as util from "util"; -import { - GlobalTestState, - pingProc, - ProcessWrapper, -} from "../harness/harness.js"; -import { Configuration } from "@gnu-taler/taler-util"; -import * as child_process from "child_process"; - -const exec = util.promisify(child_process.exec); - -export interface SyncConfig { - /** - * Human-readable name used in the test harness logs. - */ - name: string; - - httpPort: number; - - /** - * Database connection string (only postgres is supported). - */ - database: string; - - annualFee: string; - - currency: string; - - uploadLimitMb: number; - - /** - * Fulfillment URL used for contract terms related to - * sync. - */ - fulfillmentUrl: string; - - paymentBackendUrl: string; -} - -function setSyncPaths(config: Configuration, home: string) { - config.setString("paths", "sync_home", home); - // We need to make sure that the path of taler_runtime_dir isn't too long, - // as it contains unix domain sockets (108 character limit). - const runDir = fs.mkdtempSync("/tmp/taler-test-"); - config.setString("paths", "sync_runtime_dir", runDir); - config.setString("paths", "sync_data_home", "$SYNC_HOME/.local/share/sync/"); - config.setString("paths", "sync_config_home", "$SYNC_HOME/.config/sync/"); - config.setString("paths", "sync_cache_home", "$SYNC_HOME/.config/sync/"); -} - -export class SyncService { - static async create( - gc: GlobalTestState, - sc: SyncConfig, - ): Promise { - const config = new Configuration(); - - const cfgFilename = gc.testDir + `/sync-${sc.name}.conf`; - setSyncPaths(config, gc.testDir + "/synchome"); - config.setString("taler", "currency", sc.currency); - config.setString("sync", "serve", "tcp"); - config.setString("sync", "port", `${sc.httpPort}`); - config.setString("sync", "db", "postgres"); - config.setString("syncdb-postgres", "config", sc.database); - config.setString("sync", "payment_backend_url", sc.paymentBackendUrl); - config.setString("sync", "upload_limit_mb", `${sc.uploadLimitMb}`); - config.write(cfgFilename); - - return new SyncService(gc, sc, cfgFilename); - } - - proc: ProcessWrapper | undefined; - - get baseUrl(): string { - return `http://localhost:${this.syncConfig.httpPort}/`; - } - - async start(): Promise { - await exec(`sync-dbinit -c "${this.configFilename}"`); - - this.proc = this.globalState.spawnService( - "sync-httpd", - ["-LDEBUG", "-c", this.configFilename], - `sync-${this.syncConfig.name}`, - ); - } - - async pingUntilAvailable(): Promise { - const url = new URL("config", this.baseUrl).href; - await pingProc(this.proc, url, "sync"); - } - - constructor( - private globalState: GlobalTestState, - private syncConfig: SyncConfig, - private configFilename: string, - ) { } -} diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 8fad3bdbf..90dc2fdd1 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -17,20 +17,13 @@ /** * Imports. */ -import { deepStrictEqual } from "assert"; -import fs from "fs"; -import os from "os"; -import path from "path"; -// Polyfill for encoding which isn't present globally in older nodejs versions import { addPaytoQueryParams, AgeRestriction, - Amounts, classifyTalerUri, clk, codecForList, codecForString, - Configuration, decodeCrock, encodeCrock, getRandomBytes, @@ -39,12 +32,11 @@ import { parsePaytoUri, PreparePayResultType, RecoveryMergeStrategy, - rsaBlind, setDangerousTimetravel, setGlobalLogLevelFromString, TalerUriType, - parseDevExperimentUri, } from "@gnu-taler/taler-util"; +import type { TalerCryptoInterface } from "@gnu-taler/taler-wallet-core"; import { CryptoDispatcher, getDefaultNodeWallet, @@ -60,20 +52,8 @@ import { WalletCoreApiClient, walletCoreDebugFlags, } from "@gnu-taler/taler-wallet-core"; -import type { TalerCryptoInterface } from "@gnu-taler/taler-wallet-core"; -import { TextDecoder, TextEncoder } from "util"; -import { runBench1 } from "./bench1.js"; -import { runBench2 } from "./bench2.js"; -import { runBench3 } from "./bench3.js"; -import { runEnv1 } from "./env1.js"; -import { GlobalTestState, runTestWithState } from "./harness/harness.js"; -import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; -import { lintExchangeDeployment } from "./lint.js"; -import { runEnvFull } from "./env-full.js"; -// @ts-ignore -global.TextEncoder = TextEncoder; -// @ts-ignore -global.TextDecoder = TextDecoder; +import fs from "fs"; +import os from "os"; // This module also serves as the entry point for the crypto // thread worker, and thus must expose these two handlers. @@ -894,75 +874,6 @@ advancedCli wallet.stop(); }); -advancedCli - .subcommand("bench1", "bench1", { - help: "Run the 'bench1' benchmark", - }) - .requiredOption("configJson", ["--config-json"], clk.STRING) - .action(async (args) => { - let config: any; - try { - config = JSON.parse(args.bench1.configJson); - } catch (e) { - console.log("Could not parse config JSON"); - } - await runBench1(config); - }); - -advancedCli - .subcommand("bench2", "bench2", { - help: "Run the 'bench2' benchmark", - }) - .requiredOption("configJson", ["--config-json"], clk.STRING) - .action(async (args) => { - let config: any; - try { - config = JSON.parse(args.bench2.configJson); - } catch (e) { - console.log("Could not parse config JSON"); - } - await runBench2(config); - }); - -advancedCli - .subcommand("bench3", "bench3", { - help: "Run the 'bench3' benchmark", - }) - .requiredOption("configJson", ["--config-json"], clk.STRING) - .action(async (args) => { - let config: any; - try { - config = JSON.parse(args.bench3.configJson); - } catch (e) { - console.log("Could not parse config JSON"); - } - await runBench3(config); - }); - -advancedCli - .subcommand("envFull", "env-full", { - help: "Run a test environment for bench1", - }) - .action(async (args) => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env-full-")); - const testState = new GlobalTestState({ - testDir, - }); - await runTestWithState(testState, runEnvFull, "env-full", true); - }); - -advancedCli - .subcommand("env1", "env1", { - help: "Run a test environment for bench1", - }) - .action(async (args) => { - const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-")); - const testState = new GlobalTestState({ - testDir, - }); - await runTestWithState(testState, runEnv1, "env1", true); - }); - advancedCli .subcommand("withdrawFakebank", "withdraw-fakebank", { help: "Withdraw via a fakebank.", @@ -992,7 +903,7 @@ advancedCli }) .action((args) => { const enc = fs.readFileSync(0, "utf8"); - fs.writeFileSync(1, decodeCrock(enc.trim())); + console.log(decodeCrock(enc.trim())); }); advancedCli @@ -1207,92 +1118,6 @@ advancedCli }); }); -const deploymentCli = walletCli.subcommand("deploymentArgs", "deployment", { - help: "Subcommands for handling GNU Taler deployments.", -}); - -deploymentCli - .subcommand("lintExchange", "lint-exchange", { - help: "Run checks on the exchange deployment.", - }) - .flag("cont", ["--continue"], { - help: "Continue after errors if possible", - }) - .flag("debug", ["--debug"], { - help: "Output extra debug info", - }) - .action(async (args) => { - await lintExchangeDeployment( - args.lintExchange.debug, - args.lintExchange.cont, - ); - }); - -deploymentCli - .subcommand("coincfg", "gen-coin-config", { - help: "Generate a coin/denomination configuration for the exchange.", - }) - .requiredOption("minAmount", ["--min-amount"], clk.STRING, { - help: "Smallest denomination", - }) - .requiredOption("maxAmount", ["--max-amount"], clk.STRING, { - help: "Largest denomination", - }) - .action(async (args) => { - let out = ""; - - const stamp = Math.floor(new Date().getTime() / 1000); - - const min = Amounts.parseOrThrow(args.coincfg.minAmount); - const max = Amounts.parseOrThrow(args.coincfg.maxAmount); - if (min.currency != max.currency) { - console.error("currency mismatch"); - process.exit(1); - } - const currency = min.currency; - let x = min; - let n = 1; - - out += "# Coin configuration for the exchange.\n"; - out += '# Should be placed in "/etc/taler/conf.d/exchange-coins.conf".\n'; - out += "\n"; - - while (Amounts.cmp(x, max) < 0) { - out += `[COIN-${currency}-n${n}-t${stamp}]\n`; - out += `VALUE = ${Amounts.stringify(x)}\n`; - out += `DURATION_WITHDRAW = 7 days\n`; - out += `DURATION_SPEND = 2 years\n`; - out += `DURATION_LEGAL = 6 years\n`; - out += `FEE_WITHDRAW = ${currency}:0\n`; - out += `FEE_DEPOSIT = ${Amounts.stringify(min)}\n`; - out += `FEE_REFRESH = ${currency}:0\n`; - out += `FEE_REFUND = ${currency}:0\n`; - out += `RSA_KEYSIZE = 2048\n`; - out += "\n"; - x = Amounts.add(x, x).amount; - n++; - } - - console.log(out); - }); - -const deploymentConfigCli = deploymentCli.subcommand("configArgs", "config", { - help: "Subcommands the Taler configuration.", -}); - -deploymentConfigCli - .subcommand("show", "show") - .flag("diagnostics", ["-d", "--diagnostics"]) - .maybeArgument("cfgfile", clk.STRING, {}) - .action(async (args) => { - const cfg = Configuration.load(args.show.cfgfile); - console.log( - cfg.stringify({ - diagnostics: args.show.diagnostics, - }), - ); - }); - const testCli = walletCli.subcommand("testingArgs", "testing", { help: "Subcommands for testing.", }); @@ -1426,102 +1251,12 @@ testCli.subcommand("logtest", "logtest").action(async (args) => { logger.error("This is an error message."); }); -testCli - .subcommand("listIntegrationtests", "list-integrationtests") - .action(async (args) => { - for (const t of getTestInfo()) { - let s = t.name; - if (t.suites.length > 0) { - s += ` (suites: ${t.suites.join(",")})`; - } - if (t.excludeByDefault) { - s += ` [excluded by default]`; - } - console.log(s); - } - }); - -testCli - .subcommand("runIntegrationtests", "run-integrationtests") - .maybeArgument("pattern", clk.STRING, { - help: "Glob pattern to select which tests to run", - }) - .maybeOption("suites", ["--suites"], clk.STRING, { - help: "Only run selected suites (comma-separated list)", - }) - .flag("dryRun", ["--dry"], { - help: "Only print tests that will be selected to run.", - }) - .flag("quiet", ["--quiet"], { - help: "Produce less output.", - }) - .action(async (args) => { - await runTests({ - includePattern: args.runIntegrationtests.pattern, - suiteSpec: args.runIntegrationtests.suites, - dryRun: args.runIntegrationtests.dryRun, - verbosity: args.runIntegrationtests.quiet ? 0 : 1, - }); - }); - async function read(stream: NodeJS.ReadStream) { const chunks = []; for await (const chunk of stream) chunks.push(chunk); return Buffer.concat(chunks).toString("utf8"); } -testCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => { - const data = await read(process.stdin); - - const lines = data.match(/[^\r\n]+/g); - - if (!lines) { - throw Error("can't split lines"); - } - - const vals: Record = {}; - - let inBlindSigningSection = false; - - for (const line of lines) { - if (line === "blind signing:") { - inBlindSigningSection = true; - continue; - } - if (line[0] !== " ") { - inBlindSigningSection = false; - continue; - } - if (inBlindSigningSection) { - const m = line.match(/ (\w+) (\w+)/); - if (!m) { - console.log("bad format"); - process.exit(2); - } - vals[m[1]] = m[2]; - } - } - - console.log(vals); - - const req = (k: string) => { - if (!vals[k]) { - throw Error(`no value for ${k}`); - } - return decodeCrock(vals[k]); - }; - - const myBm = rsaBlind( - req("message_hash"), - req("blinding_key_secret"), - req("rsa_public_key"), - ); - - deepStrictEqual(req("blinded_message"), myBm); - - console.log("check passed!"); -}); - testCli .subcommand("cryptoworker", "cryptoworker") .maybeOption("impl", ["--impl"], clk.STRING) diff --git a/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts b/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts deleted file mode 100644 index ea05de8e9..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/scenario-prompt-payment.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts b/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts deleted file mode 100644 index ff589dd79..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-merchant.ts +++ /dev/null @@ -1,201 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-age-restrictions-mixed-merchant.ts b/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-mixed-merchant.ts deleted file mode 100644 index 8bf71b63d..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-mixed-merchant.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-age-restrictions-peer.ts b/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-peer.ts deleted file mode 100644 index af5b4df52..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-age-restrictions-peer.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-bank-api.ts b/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts deleted file mode 100644 index c7a23d3ce..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-bank-api.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-claim-loop.ts b/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts deleted file mode 100644 index a509e3b19..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-claim-loop.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-clause-schnorr.ts b/packages/taler-wallet-cli/src/integrationtests/test-clause-schnorr.ts deleted file mode 100644 index bf42dc4c6..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-clause-schnorr.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-denom-unoffered.ts b/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts deleted file mode 100644 index b5ecbee4a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-denom-unoffered.ts +++ /dev/null @@ -1,126 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-deposit.ts b/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts deleted file mode 100644 index 07382c43e..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-deposit.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-exchange-management.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts deleted file mode 100644 index 6b63c3741..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-management.ts +++ /dev/null @@ -1,285 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - // 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-wallet-cli/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts deleted file mode 100644 index 074126e9f..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-exchange-timetravel.ts +++ /dev/null @@ -1,240 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - 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-wallet-cli/src/integrationtests/test-fee-regression.ts b/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts deleted file mode 100644 index 8c5a5bea4..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-fee-regression.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - 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-wallet-cli/src/integrationtests/test-forced-selection.ts b/packages/taler-wallet-cli/src/integrationtests/test-forced-selection.ts deleted file mode 100644 index 91be11a82..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-forced-selection.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts deleted file mode 100644 index c3cbc0608..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankaccount.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts deleted file mode 100644 index 912b7b2ac..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-bankconnection.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts deleted file mode 100644 index a1da9e0da..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade-bad-request.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts deleted file mode 100644 index 946c565d4..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-facade.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts deleted file mode 100644 index f8f2d7d80..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-permissions.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts deleted file mode 100644 index cb85c1ffc..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-camt.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts deleted file mode 100644 index 24fd9d3ef..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-sandbox-transactions.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts deleted file mode 100644 index 95f4bfaa0..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-scheduling.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-api-users.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts deleted file mode 100644 index bc3103c7e..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-api-users.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts deleted file mode 100644 index 53aacca84..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-bad-gateway.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts deleted file mode 100644 index 94fd76683..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-basic.ts +++ /dev/null @@ -1,308 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - 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 = { - 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-wallet-cli/src/integrationtests/test-libeufin-c5x.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts deleted file mode 100644 index 2ba29656a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-c5x.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts deleted file mode 100644 index 1ed258c3a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-facade-anastasis.ts +++ /dev/null @@ -1,169 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts deleted file mode 100644 index 21bf07de2..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-keyrotation.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts deleted file mode 100644 index 850b0f1d9..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-nexus-balance.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts deleted file mode 100644 index 245f34331..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund-multiple-users.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-refund.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts deleted file mode 100644 index 9d90121a0..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-refund.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts deleted file mode 100644 index e56cb3d68..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-sandbox-wire-transfer-cli.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts b/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts deleted file mode 100644 index 7bc067cfe..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-libeufin-tutorial.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts deleted file mode 100644 index 30ab1cd4b..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-exchange-confusion.ts +++ /dev/null @@ -1,243 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - 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-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts deleted file mode 100644 index 09231cdd8..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-delete.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts deleted file mode 100644 index a4e44c7f3..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances-urls.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-merchant-instances.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts deleted file mode 100644 index 3efe83241..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts +++ /dev/null @@ -1,184 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-merchant-longpolling.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts deleted file mode 100644 index 4b9f53f05..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-longpolling.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-merchant-refund-api.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts deleted file mode 100644 index 5d9b23fa7..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-refund-api.ts +++ /dev/null @@ -1,303 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - 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 { - 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-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts deleted file mode 100644 index 70edaaf0c..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts +++ /dev/null @@ -1,620 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - 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 { - 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-wallet-cli/src/integrationtests/test-pay-paid.ts b/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts deleted file mode 100644 index 2ef91e4a8..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-pay-paid.ts +++ /dev/null @@ -1,222 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-payment-claim.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts deleted file mode 100644 index e93d2c44c..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-claim.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-payment-fault.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts deleted file mode 100644 index dea538e35..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-fault.ts +++ /dev/null @@ -1,222 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-payment-forgettable.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts deleted file mode 100644 index 3bdd6bef3..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-forgettable.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-payment-idempotency.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts deleted file mode 100644 index 1099a8188..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-idempotency.ts +++ /dev/null @@ -1,121 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-payment-multiple.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts deleted file mode 100644 index 46325c05f..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-multiple.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-payment-on-demo.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts deleted file mode 100644 index 737620ce7..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-on-demo.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-payment-transient.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts deleted file mode 100644 index b57b355c6..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-transient.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-payment-zero.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts deleted file mode 100644 index c38b8b382..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment-zero.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-payment.ts b/packages/taler-wallet-cli/src/integrationtests/test-payment.ts deleted file mode 100644 index 66d10f996..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-payment.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-paywall-flow.ts b/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts deleted file mode 100644 index a9601c625..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts +++ /dev/null @@ -1,252 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts deleted file mode 100644 index 211f20494..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-pull.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts deleted file mode 100644 index 4aaeca624..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-peer-to-peer-push.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-refund-auto.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts deleted file mode 100644 index 4c2a2f94a..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-auto.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-refund-gone.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts deleted file mode 100644 index b6cefda86..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-gone.ts +++ /dev/null @@ -1,124 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-refund-incremental.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts deleted file mode 100644 index 8d1f6e873..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund-incremental.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-refund.ts b/packages/taler-wallet-cli/src/integrationtests/test-refund.ts deleted file mode 100644 index b63dad590..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-refund.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-revocation.ts b/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts deleted file mode 100644 index 0fbb4960e..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-revocation.ts +++ /dev/null @@ -1,215 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - const { wallet, exchange, merchant } = req; - const coinDump = await wallet.client.call(WalletApiOperation.DumpCoins, {}); - console.log(coinDump); - const usedDenomHashes = new Set(); - 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 { - 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-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts deleted file mode 100644 index 54b66e0b2..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-autorefresh.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - 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-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts deleted file mode 100644 index 9335af9f5..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-timetravel-withdraw.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-tipping.ts b/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts deleted file mode 100644 index d31e0c06b..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-tipping.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - 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 - */ - -/** - * 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 => { - 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-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts deleted file mode 100644 index fc2f3335d..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-basic.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts deleted file mode 100644 index 8b52260e9..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-backup-doublespend.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-wallet-balance.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-balance.ts deleted file mode 100644 index f5226c6c0..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-balance.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-wallet-cryptoworker.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-cryptoworker.ts deleted file mode 100644 index a9f1c4d80..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-cryptoworker.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-wallet-dbless.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts deleted file mode 100644 index 269a8b240..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallet-dbless.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-wallettesting.ts b/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts deleted file mode 100644 index 03c446db3..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-wallettesting.ts +++ /dev/null @@ -1,233 +0,0 @@ -/* - 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 - */ - -/** - * 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 { - 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-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts deleted file mode 100644 index bf2dc0133..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-abort-bank.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts deleted file mode 100644 index dc7298e5d..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-bank-integrated.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts deleted file mode 100644 index ec6e54e6c..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-fakebank.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-withdrawal-high.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-high.ts deleted file mode 100644 index deb0e6dde..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-high.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/test-withdrawal-manual.ts b/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts deleted file mode 100644 index b691ae508..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/test-withdrawal-manual.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - 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 - */ - -/** - * 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-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts deleted file mode 100644 index 4b1c28bde..000000000 --- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts +++ /dev/null @@ -1,496 +0,0 @@ -/* - 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 - */ - -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; - 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 | 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 = 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); - }); -} diff --git a/packages/taler-wallet-cli/src/lint.ts b/packages/taler-wallet-cli/src/lint.ts deleted file mode 100644 index 49fb9dc86..000000000 --- a/packages/taler-wallet-cli/src/lint.ts +++ /dev/null @@ -1,534 +0,0 @@ -/* - 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 - */ - -/** - * The deployment linter implements checks for a deployment - * of the GNU Taler exchange. It is meant to help sysadmins - * when setting up an exchange. - * - * The linter does checks in the configuration and uses - * various tools of the exchange in test mode (-t). - * - * To be able to run the tools as the right user, the linter should be - * run as root. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { - codecForExchangeKeysJson, - codecForKeysManagementResponse, - Configuration, - decodeCrock, -} from "@gnu-taler/taler-util"; -import { - NodeHttpLib, - readSuccessResponseJsonOrThrow, -} from "@gnu-taler/taler-wallet-core"; -import { URL } from "url"; -import { spawn } from "child_process"; -import { delayMs } from "./harness/harness.js"; - -interface BasicConf { - mainCurrency: string; -} - -interface PubkeyConf { - masterPublicKey: string; -} - -const httpLib = new NodeHttpLib(); - -interface ShellResult { - stdout: string; - stderr: string; - status: number; -} - -interface LintContext { - /** - * Be more verbose. - */ - verbose: boolean; - - /** - * Always continue even after errors. - */ - cont: boolean; - - cfg: Configuration; - - numErr: number; -} - -/** - * Run a shell command, return stdout. - */ -export async function sh( - context: LintContext, - command: string, - env: { [index: string]: string | undefined } = process.env, -): Promise { - if (context.verbose) { - console.log("executing command:", command); - } - return new Promise((resolve, reject) => { - const stdoutChunks: Buffer[] = []; - const stderrChunks: Buffer[] = []; - const proc = spawn(command, { - stdio: ["inherit", "pipe", "pipe"], - shell: true, - env: env, - }); - proc.stdout.on("data", (x) => { - if (x instanceof Buffer) { - stdoutChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - proc.stderr.on("data", (x) => { - if (x instanceof Buffer) { - stderrChunks.push(x); - } else { - throw Error("unexpected data chunk type"); - } - }); - proc.on("exit", (code, signal) => { - if (code != 0 && context.verbose) { - console.log(`child process exited (${code} / ${signal})`); - } - const bOut = Buffer.concat(stdoutChunks).toString("utf-8"); - const bErr = Buffer.concat(stderrChunks).toString("utf-8"); - resolve({ - status: code ?? -1, - stderr: bErr, - stdout: bOut, - }); - }); - proc.on("error", () => { - reject(Error("Child process had error")); - }); - }); -} - -function checkBasicConf(context: LintContext): BasicConf { - const cfg = context.cfg; - const currencyEntry = cfg.getString("taler", "currency"); - let mainCurrency: string | undefined; - - if (!currencyEntry.value) { - context.numErr++; - console.log("error: currency not defined in section TALER option CURRENCY"); - console.log("Aborting further checks."); - process.exit(1); - } else { - mainCurrency = currencyEntry.value.toUpperCase(); - } - - if (mainCurrency === "KUDOS") { - console.log( - "warning: section TALER option CURRENCY contains toy currency value KUDOS", - ); - } - - const roundUnit = cfg.getAmount("taler", "currency_round_unit"); - const ru = roundUnit.required(); - if (ru.currency.toLowerCase() != mainCurrency.toLowerCase()) { - context.numErr++; - console.log( - "error: [TALER]/CURRENCY_ROUND_UNIT: currency does not match main currency", - ); - } - return { mainCurrency }; -} - -function checkCoinConfig(context: LintContext, basic: BasicConf): void { - const cfg = context.cfg; - const coinPrefix1 = "COIN_"; - const coinPrefix2 = "COIN-"; - let numCoins = 0; - - for (const secName of cfg.getSectionNames()) { - if (!(secName.startsWith(coinPrefix1) || secName.startsWith(coinPrefix2))) { - continue; - } - numCoins++; - - // FIXME: check that section is well-formed - } - - if (numCoins == 0) { - context.numErr++; - console.log( - "error: no coin denomination configured, please configure [coin-*] sections", - ); - } -} - -async function checkWireConfig(context: LintContext): Promise { - const cfg = context.cfg; - const accountPrefix = "EXCHANGE-ACCOUNT-"; - const accountCredentialsPrefix = "EXCHANGE-ACCOUNTCREDENTIALS-"; - - let accounts = new Set(); - let credentials = new Set(); - - for (const secName of cfg.getSectionNames()) { - if (secName.startsWith(accountPrefix)) { - accounts.add(secName.slice(accountPrefix.length)); - // FIXME: check settings - } - - if (secName.startsWith(accountCredentialsPrefix)) { - credentials.add(secName.slice(accountCredentialsPrefix.length)); - // FIXME: check settings - } - } - - if (accounts.size === 0) { - context.numErr++; - console.log( - "error: No accounts configured (no sections EXCHANGE-ACCOUNT-*).", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - for (const acc of accounts) { - if (!credentials.has(acc)) { - console.log( - `warning: no credentials configured for exchange-account-${acc}`, - ); - } - } - - for (const acc of accounts) { - // test credit history - { - const res = await sh( - context, - "su -l --shell /bin/sh " + - `-c 'taler-exchange-wire-gateway-client -s exchange-accountcredentials-${acc} --credit-history' ` + - "taler-exchange-wire", - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log( - "error: Could not run taler-exchange-wire-gateway-client. Please review logs above.", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - } - - // TWG client - { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-wirewatch -t' taler-exchange-wire`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run wirewatch. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - - // Wirewatch - { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-wirewatch -t' taler-exchange-wire`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run wirewatch. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - - // Closer - { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run closer. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } -} - -async function checkAggregatorConfig(context: LintContext) { - const res = await sh( - context, - "su -l --shell /bin/sh -c 'taler-exchange-aggregator -t' taler-exchange-aggregator", - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run aggregator. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } -} - -async function checkCloserConfig(context: LintContext) { - const res = await sh( - context, - `su -l --shell /bin/sh -c 'taler-exchange-closer -t' taler-exchange-closer`, - ); - if (res.status != 0) { - context.numErr++; - console.log(res.stdout); - console.log(res.stderr); - console.log("error: Could not run closer. Please review logs above."); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } -} - -function checkMasterPublicKeyConfig(context: LintContext): PubkeyConf { - const cfg = context.cfg; - const pub = cfg.getString("exchange", "master_public_key"); - - const pubDecoded = decodeCrock(pub.required()); - - if (pubDecoded.length != 32) { - context.numErr++; - console.log("error: invalid master public key"); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - return { - masterPublicKey: pub.required(), - }; -} - -export async function checkExchangeHttpd( - context: LintContext, - pubConf: PubkeyConf, -): Promise { - const cfg = context.cfg; - const baseUrlEntry = cfg.getString("exchange", "base_url"); - - if (!baseUrlEntry.isDefined) { - context.numErr++; - console.log( - "error: configuration needs to specify section EXCHANGE option BASE_URL", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - const baseUrl = baseUrlEntry.required(); - - if (!baseUrl.startsWith("http")) { - context.numErr++; - console.log( - "error: section EXCHANGE option BASE_URL needs to be an http or https URL", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - if (!baseUrl.endsWith("/")) { - context.numErr++; - console.log( - "error: section EXCHANGE option BASE_URL needs to end with a slash", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - - if (!baseUrl.startsWith("https://")) { - console.log( - "warning: section EXCHANGE option BASE_URL: it is recommended to serve the exchange via HTTPS", - ); - } - - { - const mgmtUrl = new URL("management/keys", baseUrl); - const resp = await httpLib.get(mgmtUrl.href); - - const futureKeys = await readSuccessResponseJsonOrThrow( - resp, - codecForKeysManagementResponse(), - ); - - if (futureKeys.future_denoms.length > 0) { - console.log( - `warning: exchange has denomination keys that need to be signed by the offline signing procedure`, - ); - } - - if (futureKeys.future_signkeys.length > 0) { - console.log( - `warning: exchange has signing keys that need to be signed by the offline signing procedure`, - ); - } - } - - // Check if we can use /keys already - { - const keysUrl = new URL("keys", baseUrl); - - const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); - - if (!resp) { - context.numErr++; - console.log( - "error: request to /keys timed out. " + - "Make sure to sign and upload denomination and signing keys " + - "with taler-exchange-offline.", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } else { - const keys = await readSuccessResponseJsonOrThrow( - resp, - codecForExchangeKeysJson(), - ); - - if (keys.master_public_key !== pubConf.masterPublicKey) { - context.numErr++; - console.log( - "error: master public key of exchange does not match public key of live exchange", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } - } - } - - // Check /wire - { - const keysUrl = new URL("wire", baseUrl); - - const resp = await Promise.race([httpLib.get(keysUrl.href), delayMs(2000)]); - - if (!resp) { - context.numErr++; - console.log( - "error: request to /wire timed out. " + - "Make sure to sign and upload accounts and wire fees " + - "using the taler-exchange-offline tool.", - ); - if (!context.cont) { - console.log("Aborting further checks."); - process.exit(1); - } - } else { - if (resp.status !== 200) { - console.log( - "error: Can't access exchange /wire. Please check " + - "the logs of taler-exchange-httpd for further information.", - ); - } - } - } -} - -/** - * Do some basic checks in the configuration of a Taler deployment. - */ -export async function lintExchangeDeployment( - verbose: boolean, - cont: boolean, -): Promise { - if (process.getuid!() != 0) { - console.log( - "warning: the exchange deployment linter is designed to be run as root", - ); - } - - const cfg = Configuration.load(); - - const context: LintContext = { - cont, - verbose, - cfg, - numErr: 0, - }; - - const basic = checkBasicConf(context); - - checkCoinConfig(context, basic); - - await checkWireConfig(context); - - await checkAggregatorConfig(context); - - await checkCloserConfig(context); - - const pubConf = checkMasterPublicKeyConfig(context); - - await checkExchangeHttpd(context, pubConf); - - if (context.numErr == 0) { - console.log("Linting completed without errors."); - process.exit(0); - } else { - console.log(`Linting completed with ${context.numErr} errors.`); - process.exit(1); - } -} diff --git a/packages/taler-wallet-embedded/package.json b/packages/taler-wallet-embedded/package.json index d8cd5db29..01a2378fd 100644 --- a/packages/taler-wallet-embedded/package.json +++ b/packages/taler-wallet-embedded/package.json @@ -17,7 +17,8 @@ "compile": "tsc && rollup -c", "pretty": "prettier --write src", "coverage": "tsc && nyc ava", - "clean": "rimraf lib dist tsconfig.tsbuildinfo" + "clean": "rimraf lib dist tsconfig.tsbuildinfo", + "deps": "pnpm install --frozen-lockfile --filter @gnu-taler/taler-wallet-embedded..." }, "files": [ "AUTHORS", @@ -32,7 +33,7 @@ "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-node-resolve": "^13.3.0", "@rollup/plugin-replace": "^4.0.0", - "@types/node": "^18.8.5", + "@types/node": "^18.11.17", "prettier": "^2.5.1", "rimraf": "^3.0.2", "rollup": "^2.79.0", @@ -44,7 +45,6 @@ "@gnu-taler/idb-bridge": "workspace:*", "@gnu-taler/taler-util": "workspace:*", "@gnu-taler/taler-wallet-core": "workspace:*", - "@gnu-taler/taler-wallet-embedded": "link:", "tslib": "^2.4.0" } } diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json index 5c6983bfc..8a6c19406 100644 --- a/packages/taler-wallet-webextension/package.json +++ b/packages/taler-wallet-webextension/package.json @@ -43,13 +43,13 @@ } }, "devDependencies": { - "@gnu-taler/web-util": "workspace:*", "@babel/core": "7.18.9", "@babel/plugin-transform-modules-commonjs": "7.18.6", "@babel/plugin-transform-react-jsx-source": "7.18.6", "@babel/preset-typescript": "7.18.6", "@babel/runtime": "7.18.9", "@gnu-taler/pogen": "workspace:*", + "@gnu-taler/web-util": "workspace:*", "@linaria/babel-preset": "3.0.0-beta.22", "@linaria/core": "3.0.0-beta.22", "@linaria/react": "3.0.0-beta.22", @@ -58,7 +58,7 @@ "@types/chrome": "0.0.197", "@types/history": "^4.7.8", "@types/mocha": "^9.0.0", - "@types/node": "^18.8.5", + "@types/node": "^18.11.17", "babel-loader": "^8.2.3", "babel-plugin-transform-react-jsx": "^6.24.1", "chai": "^4.3.6", diff --git a/packages/web-util/package.json b/packages/web-util/package.json index ad87304fe..e6f8195c0 100644 --- a/packages/web-util/package.json +++ b/packages/web-util/package.json @@ -26,7 +26,7 @@ "devDependencies": { "@gnu-taler/taler-util": "workspace:*", "@types/express": "^4.17.14", - "@types/node": "^18.11.9", + "@types/node": "^18.11.17", "@types/web": "^0.0.82", "@types/ws": "^8.5.3", "axios": "^1.2.1", -- cgit v1.2.3