commit 5bdfa4e17b22ae6addecda805e57e6790289e307
parent 57892a485b04987f05854615cceb15054646c654
Author: Florian Dold <dold@taler.net>
Date: Sat, 29 Aug 2026 17:19:19 +0200
taler-harness: cover merchant session status endpoint
Diffstat:
2 files changed, 136 insertions(+), 0 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-sessions.ts b/packages/taler-harness/src/integrationtests/test-merchant-sessions.ts
@@ -0,0 +1,134 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 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.
+*/
+
+import {
+ ConfirmPayResultType,
+ HttpStatusCode,
+ succeedOrThrow,
+ TalerMerchantInstanceHttpClient,
+ TemplateType,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ createSimpleTestkudosEnvironmentV3,
+ withdrawViaBankV3,
+} from "../harness/environments.js";
+import { GlobalTestState, harnessHttpLib } from "../harness/harness.js";
+
+/**
+ * Verify the public session-status endpoint used by Paivana, including its
+ * unknown-to-paid long-poll transition.
+ */
+export async function runMerchantSessionsTest(t: GlobalTestState) {
+ const {
+ bankClient,
+ walletClient,
+ exchange,
+ merchant,
+ merchantAdminAccessToken,
+ } = await createSimpleTestkudosEnvironmentV3(t);
+
+ const withdrawal = await withdrawViaBankV3(t, {
+ walletClient,
+ bankClient,
+ exchange,
+ amount: "TESTKUDOS:10",
+ });
+ await withdrawal.withdrawalFinishedCond;
+
+ const merchantClient = new TalerMerchantInstanceHttpClient(
+ merchant.makeInstanceBaseUrl(),
+ );
+ const sessionId =
+ "4321-6hvIP7UnmNDVjp5Intuf9jFK7MzW0ycEyxf5Mszl3xs=";
+ const fulfillmentUrl = "https://example.com/protected-article";
+
+ const missingFulfillmentUrl = new URL(
+ `sessions/${sessionId}`,
+ merchant.makeInstanceBaseUrl(),
+ );
+ const missingFulfillmentResp = await harnessHttpLib.fetch(
+ missingFulfillmentUrl.href,
+ );
+ t.assertDeepEqual(missingFulfillmentResp.status, HttpStatusCode.BadRequest);
+
+ const unknownSession = await merchantClient.getOrderIdForSessionAndUrl(
+ sessionId,
+ fulfillmentUrl,
+ );
+ t.assertTrue(unknownSession.type === "fail");
+ t.assertDeepEqual(unknownSession.response.status, HttpStatusCode.NotFound);
+
+ succeedOrThrow(
+ await merchantClient.addTemplate(merchantAdminAccessToken, {
+ template_id: "paivana-access",
+ template_description: "Paivana access",
+ template_contract: {
+ template_type: TemplateType.PAIVANA,
+ summary: "Paivana access",
+ website_regex: "https://example.com/.*",
+ choices: [{ amount: "TESTKUDOS:5" }],
+ },
+ }),
+ );
+
+ // Paivana starts polling before the wallet instantiates the template, so
+ // the endpoint must also wake when the session is initially unknown.
+ const paidSessionPromise = merchantClient.getOrderIdForSessionAndUrl(
+ sessionId,
+ fulfillmentUrl,
+ { timeout: 10_000 },
+ );
+ const talerPayTemplateUri =
+ `taler+http://pay-template/localhost:${merchant.port}/paivana-access` +
+ `?session_id=${encodeURIComponent(sessionId)}` +
+ `&fulfillment_url=${encodeURIComponent(fulfillmentUrl)}`;
+
+ const preparePay = await walletClient.call(
+ WalletApiOperation.PreparePayForTemplateV2,
+ { talerPayTemplateUri },
+ );
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: preparePay.transactionId,
+ txState: {
+ major: TransactionMajorState.Dialog,
+ minor: TransactionMinorState.Proposed,
+ },
+ });
+
+ const choices = await walletClient.call(
+ WalletApiOperation.GetChoicesForPayment,
+ { transactionId: preparePay.transactionId },
+ );
+
+ const wrongFulfillment = await merchantClient.getOrderIdForSessionAndUrl(
+ sessionId,
+ "https://example.com/different-article",
+ );
+ t.assertTrue(wrongFulfillment.type === "fail");
+ t.assertDeepEqual(wrongFulfillment.response.status, HttpStatusCode.NotFound);
+
+ const confirmPay = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePay.transactionId,
+ choiceIndex: 0,
+ });
+ t.assertTrue(confirmPay.type === ConfirmPayResultType.Done);
+
+ const paidSession = succeedOrThrow(await paidSessionPromise);
+ t.assertDeepEqual(paidSession.paid, true);
+ t.assertDeepEqual(paidSession.order_id, choices.contractTerms.order_id);
+}
+
+runMerchantSessionsTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -130,6 +130,7 @@ import { runMerchantPaytoReuseTest } from "./test-merchant-payto-reuse.js";
import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js";
import { runMerchantRefundFeesTest } from "./test-merchant-refund-fees.js";
import { runMerchantReportsTest } from "./test-merchant-reports.js";
+import { runMerchantSessionsTest } from "./test-merchant-sessions.js";
import { runMerchantSelfProvisionActivationTest } from "./test-merchant-self-provision-activation.js";
import { runMerchantSelfProvisionInactiveAccountPermissionsTest } from "./test-merchant-self-provision-inactive-account-permissions.js";
import { runMerchantSpecPublicOrdersTest } from "./test-merchant-spec-public-orders.js";
@@ -332,6 +333,7 @@ const allTests: TestMainFunction[] = [
runMerchantInstanceCasingTest,
runMerchantInstancesTest,
runMerchantLongpollingTest,
+ runMerchantSessionsTest,
runMerchantRefundApiTest,
runMerchantSpecPublicOrdersTest,
runPaymentClaimTest,