From 2f945b2aebf3378496c8be3ef48a16253dde3358 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 12 Aug 2021 21:01:40 +0200 Subject: merchant test cases --- .../test-merchant-spec-public-orders.ts | 332 +++++++++++++++++++++ .../src/integrationtests/test-paywall-flow.ts | 4 +- .../src/integrationtests/testrunner.ts | 2 + .../taler-wallet-core/src/headless/NodeHttpLib.ts | 3 + .../src/operations/transactions.ts | 7 + 5 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts 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 new file mode 100644 index 000000000..98528ada4 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-spec-public-orders.ts @@ -0,0 +1,332 @@ +/* + 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, +} from "@gnu-taler/taler-util"; +import { + encodeCrock, + getRandomBytes, + NodeHttpLib, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState, MerchantPrivateApi, WalletCli } from "./harness"; +import { + createSimpleTestkudosEnvironment, + withdrawViaBank, +} from "./helpers.js"; + +const httpLib = new NodeHttpLib(); + +/** + * 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 { + wallet, + bank, + exchange, + merchant, + } = await createSimpleTestkudosEnvironment(t); + const wallet2 = new WalletCli(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + await withdrawViaBank(t, { + wallet: wallet2, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + // 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 + } + + 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 + { + 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 confirmPayRes2 = await wallet.client.call( + WalletApiOperation.ConfirmPay, + { + proposalId: proposalId, + sessionId: "mysession", + }, + ); + + 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", "mysession"); + 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", "mysession"); + 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"); + } +} + +runMerchantSpecPublicOrdersTest.suites = ["merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts b/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts index 25c8862cc..04eee79e3 100644 --- a/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts +++ b/packages/taler-wallet-cli/src/integrationtests/test-paywall-flow.ts @@ -134,10 +134,10 @@ export async function runPaywallFlowTest(t: GlobalTestState) { console.log(publicOrderStatusResp.data); - if (publicOrderStatusResp.status != 202) { + if (publicOrderStatusResp.status != 200) { console.log(publicOrderStatusResp.data); throw Error( - `expected status 202 (after paying), but got ${publicOrderStatusResp.status}`, + `expected status 200 (after paying), but got ${publicOrderStatusResp.status}`, ); } diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts index 5e0cb9f57..c2e626436 100644 --- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts +++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts @@ -76,6 +76,7 @@ import { runMerchantInstancesDeleteTest } from "./test-merchant-instances-delete import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend"; import { runPaymentForgettableTest } from "./test-payment-forgettable.js"; import { runPaymentZeroTest } from "./test-payment-zero.js"; +import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js"; /** * Test runner. @@ -113,6 +114,7 @@ const allTests: TestMainFunction[] = [ runMerchantInstancesDeleteTest, runMerchantInstancesUrlsTest, runMerchantLongpollingTest, + runMerchantSpecPublicOrdersTest, runMerchantRefundApiTest, runPayAbortTest, runPaymentClaimTest, diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts index 1186ea4d6..9655d363f 100644 --- a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts +++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts @@ -52,6 +52,8 @@ export class NodeHttpLib implements HttpRequestLibrary { const method = opt?.method ?? "GET"; let body = opt?.body; + logger.trace(`Requesting ${method} ${url}`); + const parsedUrl = new URL(url); if (this.throttlingEnabled && this.throttle.applyThrottle(url)) { throw OperationFailedError.fromCode( @@ -79,6 +81,7 @@ export class NodeHttpLib implements HttpRequestLibrary { transformResponse: (x) => x, data: body, timeout, + maxRedirects: 0, }); } catch (e) { throw OperationFailedError.fromCode( diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 52b9819dd..a07551e82 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -403,6 +403,13 @@ export enum TombstoneTag { DeleteRefund = "delete-refund", } +export async function retryTransactionNow( + ws: InternalWalletState, + transactionId: string, +): Promise { + const [type, ...rest] = transactionId.split(":"); +} + /** * Immediately retry the underlying operation * of a transaction. -- cgit v1.2.3