integration test for paywall flow

This commit is contained in:
Florian Dold 2020-08-13 00:26:55 +05:30
parent 4891c4c7ce
commit e9ed3b1867
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
11 changed files with 319 additions and 75 deletions

View File

@ -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<MerchantOrderPrivateStatusResponse> {
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<ConfirmPayResult> {
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<void> {
const resp = await this.apiRequest("addExchange", req);
if (resp.type === "response") {

View File

@ -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");

View File

@ -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");
}

View File

@ -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");
});

View File

@ -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");

View File

@ -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");

View File

@ -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");

View File

@ -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 <http://www.gnu.org/licenses/>
*/
/**
* 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);
});

View File

@ -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");

View File

@ -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");

View File

@ -221,6 +221,29 @@ export interface ConfirmPayResultPending {
export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
export const codecForConfirmPayResultPending = (): Codec<
ConfirmPayResultPending
> =>
buildCodecForObject<ConfirmPayResultPending>()
.property("lastError", codecForAny())
.property("type", codecForConstString(ConfirmPayResultType.Pending))
.build("ConfirmPayResultPending");
export const codecForConfirmPayResultDone = (): Codec<
ConfirmPayResultDone
> =>
buildCodecForObject<ConfirmPayResultDone>()
.property("type", codecForConstString(ConfirmPayResultType.Done))
.property("nextUrl", codecForString())
.build("ConfirmPayResultDone");
export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> =>
buildCodecForUnion<ConfirmPayResult>()
.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<PreparePayResult> =>
export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
buildCodecForUnion<PreparePayResult>()
.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