From e9ed3b18672af919efa12364b97fd2b7efe21cd9 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 13 Aug 2020 00:26:55 +0530 Subject: integration test for paywall flow --- packages/taler-integrationtests/src/harness.ts | 32 +++- .../src/scenario-prompt-payment.ts | 7 +- .../src/scenario-rerun-payment-multiple.ts | 14 +- .../src/test-payment-fault.ts | 14 +- .../src/test-payment-idempotency.ts | 14 +- .../src/test-payment-multiple.ts | 14 +- .../taler-integrationtests/src/test-payment.ts | 14 +- .../src/test-paywall-flow.ts | 206 +++++++++++++++++++++ .../src/test-refund-incremental.ts | 21 +-- packages/taler-integrationtests/src/test-refund.ts | 14 +- .../taler-wallet-core/src/types/walletTypes.ts | 44 ++++- 11 files changed, 319 insertions(+), 75 deletions(-) create mode 100644 packages/taler-integrationtests/src/test-paywall-flow.ts (limited to 'packages') diff --git a/packages/taler-integrationtests/src/harness.ts b/packages/taler-integrationtests/src/harness.ts index 0cf769163..8f9c540f6 100644 --- a/packages/taler-integrationtests/src/harness.ts +++ b/packages/taler-integrationtests/src/harness.ts @@ -50,6 +50,9 @@ import { GetWithdrawalDetailsForUriRequest, WithdrawUriInfoResponse, codecForWithdrawUriInfoResponse, + ConfirmPayRequest, + ConfirmPayResult, + codecForConfirmPayResult, } from "taler-wallet-core"; import { URL } from "url"; import axios from "axios"; @@ -58,6 +61,7 @@ import { codecForPostOrderResponse, PostOrderRequest, PostOrderResponse, + MerchantOrderPrivateStatusResponse, } from "./merchantApiTypes"; import { EddsaKeyPair, @@ -886,6 +890,13 @@ export interface MerchantConfig { database: string; } + +export interface PrivateOrderStatusQuery { + instance?: string, + orderId: string, + sessionId?: string, +} + export class MerchantService { static fromExistingConfig(gc: GlobalTestState, name: string) { const cfgFilename = gc.testDir + `/merchant-${name}.conf`; @@ -982,17 +993,20 @@ export class MerchantService { }); } - async queryPrivateOrderStatus(instanceName: string, orderId: string) { + async queryPrivateOrderStatus(query: PrivateOrderStatusQuery): Promise { const reqUrl = new URL( - `private/orders/${orderId}`, - this.makeInstanceBaseUrl(instanceName), + `private/orders/${query.orderId}`, + this.makeInstanceBaseUrl(query.instance), ); + if (query.sessionId) { + reqUrl.searchParams.set("session_id", query.sessionId); + } const resp = await axios.get(reqUrl.href); return codecForMerchantOrderPrivateStatusResponse().decode(resp.data); } - makeInstanceBaseUrl(instanceName: string): string { - if (instanceName === "default") { + makeInstanceBaseUrl(instanceName?: string): string { + if (instanceName === undefined || instanceName === "default") { return `http://localhost:${this.merchantConfig.httpPort}/`; } else { return `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceName}/`; @@ -1177,6 +1191,14 @@ export class WalletCli { throw new OperationFailedError(resp.error); } + async confirmPay(req: ConfirmPayRequest): Promise { + const resp = await this.apiRequest("confirmPay", req); + if (resp.type === "response") { + return codecForConfirmPayResult().decode(resp.result); + } + throw new OperationFailedError(resp.error); + } + async addExchange(req: AddExchangeRequest): Promise { const resp = await this.apiRequest("addExchange", req); if (resp.type === "response") { diff --git a/packages/taler-integrationtests/src/scenario-prompt-payment.ts b/packages/taler-integrationtests/src/scenario-prompt-payment.ts index f60c6704d..3e4bfc6c2 100644 --- a/packages/taler-integrationtests/src/scenario-prompt-payment.ts +++ b/packages/taler-integrationtests/src/scenario-prompt-payment.ts @@ -47,10 +47,9 @@ runTest(async (t: GlobalTestState) => { }, }); - let orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + let orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); diff --git a/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts b/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts index a755aa939..525ba9a25 100644 --- a/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts +++ b/packages/taler-integrationtests/src/scenario-rerun-payment-multiple.ts @@ -60,10 +60,9 @@ async function withdrawAndPay( }, }); - let orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + let orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -82,10 +81,9 @@ async function withdrawAndPay( // Check if payment was successful. - orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "paid"); } diff --git a/packages/taler-integrationtests/src/test-payment-fault.ts b/packages/taler-integrationtests/src/test-payment-fault.ts index 2ee5c7055..4babdc500 100644 --- a/packages/taler-integrationtests/src/test-payment-fault.ts +++ b/packages/taler-integrationtests/src/test-payment-fault.ts @@ -153,10 +153,9 @@ runTest(async (t: GlobalTestState) => { }, }); - let orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + let orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -196,10 +195,9 @@ runTest(async (t: GlobalTestState) => { // Check if payment was successful. - orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "paid"); }); diff --git a/packages/taler-integrationtests/src/test-payment-idempotency.ts b/packages/taler-integrationtests/src/test-payment-idempotency.ts index 4d6727715..bc641a356 100644 --- a/packages/taler-integrationtests/src/test-payment-idempotency.ts +++ b/packages/taler-integrationtests/src/test-payment-idempotency.ts @@ -49,10 +49,9 @@ runTest(async (t: GlobalTestState) => { }, }); - let orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + let orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -85,10 +84,9 @@ runTest(async (t: GlobalTestState) => { // Check if payment was successful. - orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "paid"); diff --git a/packages/taler-integrationtests/src/test-payment-multiple.ts b/packages/taler-integrationtests/src/test-payment-multiple.ts index 80092a9a3..00b3c0b69 100644 --- a/packages/taler-integrationtests/src/test-payment-multiple.ts +++ b/packages/taler-integrationtests/src/test-payment-multiple.ts @@ -130,10 +130,9 @@ runTest(async (t: GlobalTestState) => { }, }); - let orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + let orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -152,10 +151,9 @@ runTest(async (t: GlobalTestState) => { // Check if payment was successful. - orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "paid"); diff --git a/packages/taler-integrationtests/src/test-payment.ts b/packages/taler-integrationtests/src/test-payment.ts index 77645909c..12b4267b7 100644 --- a/packages/taler-integrationtests/src/test-payment.ts +++ b/packages/taler-integrationtests/src/test-payment.ts @@ -48,10 +48,9 @@ runTest(async (t: GlobalTestState) => { }, }); - let orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + let orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -71,10 +70,9 @@ runTest(async (t: GlobalTestState) => { // Check if payment was successful. - orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "paid"); diff --git a/packages/taler-integrationtests/src/test-paywall-flow.ts b/packages/taler-integrationtests/src/test-paywall-flow.ts new file mode 100644 index 000000000..b329a9c6d --- /dev/null +++ b/packages/taler-integrationtests/src/test-paywall-flow.ts @@ -0,0 +1,206 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more 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 { runTest, GlobalTestState } from "./harness"; +import { createSimpleTestkudosEnvironment, withdrawViaBank } from "./helpers"; +import { + PreparePayResultType, + codecForMerchantOrderStatusUnpaid, + ConfirmPayResultType, +} from "taler-wallet-core"; +import axios from "axios"; + +/** + * Run test for basic, bank-integrated withdrawal. + */ +runTest(async (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 merchant.createOrder("default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/article42", + }, + }); + + const firstOrderId = orderResp.order_id; + + let orderStatus = await merchant.queryPrivateOrderStatus({ + 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 pubUnpaidStatusResp = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + console.log(pubUnpaidStatusResp); + + let preparePayResp = await wallet.preparePay({ + talerPayUri: pubUnpaidStatusResp.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}`, + ); + } + + let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + const confirmPayRes = await wallet.confirmPay({ + proposalId: proposalId, + }); + + t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); + + publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 410) { + throw Error( + `expected status 410 (after paying), but got ${publicOrderStatusResp.status}`, + ); + } + + /** + * ========================================================================= + * Now change up the session ID! + * ========================================================================= + */ + + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + sessionId: "mysession-two", + }); + + // Should be unpaid because of a new session ID + t.assertTrue(orderStatus.order_status === "unpaid"); + + publicOrderStatusUrl = orderStatus.order_status_url; + + // Pay with new taler://pay URI, which should + // have the new session ID! + // Wallet should now automatically re-play payment. + preparePayResp = await wallet.preparePay({ + talerPayUri: orderStatus.taler_pay_uri, + }); + + t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); + t.assertTrue(preparePayResp.paid); + + /** + * ========================================================================= + * Now we test re-purchase detection. + * ========================================================================= + */ + + orderResp = await merchant.createOrder("default", { + order: { + summary: "Buy me!", + amount: "TESTKUDOS:5", + // Same fulfillment URL as previously! + fulfillment_url: "https://example.com/article42", + }, + }); + + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + sessionId: "mysession-three", + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + t.assertTrue(orderStatus.already_paid_order_id === undefined); + publicOrderStatusUrl = 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.preparePay({ + talerPayUri: orderStatus.taler_pay_uri, + }); + + t.assertTrue(preparePayResp.status === PreparePayResultType.AlreadyConfirmed); + t.assertTrue(preparePayResp.paid); + + // Ask the order status of the claimed-but-unpaid order + publicOrderStatusResp = await axios.get(publicOrderStatusUrl, { + validateStatus: () => true, + }); + + if (publicOrderStatusResp.status != 403) { + throw Error( + `expected status 403, but got ${publicOrderStatusResp.status}`, + ); + } + + pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode( + publicOrderStatusResp.data, + ); + + t.assertTrue(pubUnpaidStatusResp.already_paid_order_id === firstOrderId); +}); diff --git a/packages/taler-integrationtests/src/test-refund-incremental.ts b/packages/taler-integrationtests/src/test-refund-incremental.ts index 0667b10d8..59a36b942 100644 --- a/packages/taler-integrationtests/src/test-refund-incremental.ts +++ b/packages/taler-integrationtests/src/test-refund-incremental.ts @@ -47,10 +47,9 @@ runTest(async (t: GlobalTestState) => { }, }); - let orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + let orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -69,10 +68,9 @@ runTest(async (t: GlobalTestState) => { // Check if payment was successful. - orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "paid"); @@ -103,10 +101,9 @@ runTest(async (t: GlobalTestState) => { }); console.log(r); - orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "paid"); diff --git a/packages/taler-integrationtests/src/test-refund.ts b/packages/taler-integrationtests/src/test-refund.ts index e1fdbfc50..d0d0a0a05 100644 --- a/packages/taler-integrationtests/src/test-refund.ts +++ b/packages/taler-integrationtests/src/test-refund.ts @@ -47,10 +47,9 @@ runTest(async (t: GlobalTestState) => { }, }); - let orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + let orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "unpaid"); @@ -69,10 +68,9 @@ runTest(async (t: GlobalTestState) => { // Check if payment was successful. - orderStatus = await merchant.queryPrivateOrderStatus( - "default", - orderResp.order_id, - ); + orderStatus = await merchant.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); t.assertTrue(orderStatus.order_status === "paid"); diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts index 6f6340520..8521af3ff 100644 --- a/packages/taler-wallet-core/src/types/walletTypes.ts +++ b/packages/taler-wallet-core/src/types/walletTypes.ts @@ -221,6 +221,29 @@ export interface ConfirmPayResultPending { export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending; +export const codecForConfirmPayResultPending = (): Codec< + ConfirmPayResultPending +> => + buildCodecForObject() + .property("lastError", codecForAny()) + .property("type", codecForConstString(ConfirmPayResultType.Pending)) + .build("ConfirmPayResultPending"); + +export const codecForConfirmPayResultDone = (): Codec< + ConfirmPayResultDone +> => + buildCodecForObject() + .property("type", codecForConstString(ConfirmPayResultType.Done)) + .property("nextUrl", codecForString()) + .build("ConfirmPayResultDone"); + +export const codecForConfirmPayResult = (): Codec => + buildCodecForUnion() + .discriminateOn("type") + .alternative(ConfirmPayResultType.Pending, codecForConfirmPayResultPending()) + .alternative(ConfirmPayResultType.Done, codecForConfirmPayResultDone()) + .build("ConfirmPayResult"); + /** * Information about all sender wire details known to the wallet, * as well as exchanges that accept these wire types. @@ -400,13 +423,22 @@ export const codecForPreparePayResultAlreadyConfirmed = (): Codec< .property("contractTerms", codecForAny()) .build("PreparePayResultAlreadyConfirmed"); -export const codecForPreparePayResult = (): Codec => +export const codecForPreparePayResult = (): Codec => buildCodecForUnion() - .discriminateOn("status") - .alternative(PreparePayResultType.AlreadyConfirmed, codecForPreparePayResultAlreadyConfirmed()) - .alternative(PreparePayResultType.InsufficientBalance, codecForPreparePayResultInsufficientBalance()) - .alternative(PreparePayResultType.PaymentPossible, codecForPreparePayResultPaymentPossible()) - .build("PreparePayResult"); + .discriminateOn("status") + .alternative( + PreparePayResultType.AlreadyConfirmed, + codecForPreparePayResultAlreadyConfirmed(), + ) + .alternative( + PreparePayResultType.InsufficientBalance, + codecForPreparePayResultInsufficientBalance(), + ) + .alternative( + PreparePayResultType.PaymentPossible, + codecForPreparePayResultPaymentPossible(), + ) + .build("PreparePayResult"); export type PreparePayResult = | PreparePayResultInsufficientBalance -- cgit v1.2.3