/*
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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
BankService,
ExchangeService,
GlobalTestState,
MerchantApiClient,
MerchantService,
harnessHttpLib,
} from "../harness/harness.js";
import {
createSimpleTestkudosEnvironmentV2,
createWalletDaemonWithClient,
withdrawViaBankV2,
} from "../harness/helpers.js";
interface Context {
merchant: MerchantService;
merchantBaseUrl: string;
bank: BankService;
exchange: ExchangeService;
}
const httpLib = harnessHttpLib;
async function testWithClaimToken(
t: GlobalTestState,
c: Context,
): Promise {
const { walletClient } = await createWalletDaemonWithClient(t, {
name: "wct",
});
const { bank, exchange } = c;
const { merchant, merchantBaseUrl } = c;
const wres = await withdrawViaBankV2(t, {
walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
});
await wres.withdrawalFinishedCond;
const sessionId = "mysession";
const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
const orderResp = await merchantClient.createOrder({
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.fetch(
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.fetch(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.fetch(url.href, {
headers: {
Accept: "text/html",
},
});
const r = await httpResp.text();
t.assertDeepEqual(httpResp.status, 402);
console.log(r);
}
const preparePayResp = await walletClient.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.fetch(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.fetch(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.fetch(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.fetch(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.fetch(url.href);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(httpResp.status, 202);
}
const confirmPayRes = await walletClient.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.fetch(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.fetch(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.fetch(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.fetch(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.fetch(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.fetch(url.href, {
headers: { Accept: "text/html" },
});
t.assertDeepEqual(httpResp.status, 200);
}
const confirmPayRes2 = await walletClient.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 merchantClient.createOrder({
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.fetch(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.fetch(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.fetch(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 { walletClient } = await createWalletDaemonWithClient(t, {
name: "wnoct",
});
const sessionId = "mysession2";
const { bank, exchange } = c;
const { merchant, merchantBaseUrl } = c;
const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
const wres = await withdrawViaBankV2(t, {
walletClient,
bank,
exchange,
amount: "TESTKUDOS:20",
});
await wres.withdrawalFinishedCond;
const orderResp = await merchantClient.createOrder({
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.fetch(
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.fetch(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.fetch(url.href, {
headers: {
Accept: "text/html",
},
});
const r = await httpResp.text();
t.assertDeepEqual(httpResp.status, 402);
console.log(r);
}
const preparePayResp = await walletClient.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.fetch(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.fetch(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.fetch(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.fetch(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.fetch(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 walletClient.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.fetch(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.fetch(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.fetch(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.fetch(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.fetch(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.fetch(url.href, {
headers: { Accept: "text/html" },
});
t.assertDeepEqual(httpResp.status, 200);
}
const confirmPayRes2 = await walletClient.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 merchantClient.createOrder({
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.fetch(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.fetch(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.fetch(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 createSimpleTestkudosEnvironmentV2(
t,
);
// Base URL for the default instance.
const merchantBaseUrl = merchant.makeInstanceBaseUrl();
{
const httpResp = await httpLib.fetch(
new URL("config", merchantBaseUrl).href,
);
const r = await httpResp.json();
console.log(r);
t.assertDeepEqual(r.currency, "TESTKUDOS");
}
{
const httpResp = await httpLib.fetch(
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.fetch(
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"];