commit fcc64886961bef05f44ddb158212fe5112d1eb31
parent 46fbf8c1fb2943f48c0737f461812bd12f370486
Author: Florian Dold <dold@taler.net>
Date: Tue, 28 Jul 2026 16:46:31 +0200
harness: check that HEAD reaches the paivana paywall page
Covers the request path with the whole site behind the paywall, where a HEAD
that is not recognised as a template request redirects to itself forever.
Issue: https://bugs.taler.net/n/11640
Diffstat:
2 files changed, 93 insertions(+), 0 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-paivana-head.ts b/packages/taler-harness/src/integrationtests/test-paivana-head.ts
@@ -0,0 +1,91 @@
+/*
+ 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.
+
+ 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 { Logger } from "@gnu-taler/taler-util";
+import * as http from "node:http";
+import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js";
+import { GlobalTestState } from "../harness/harness.js";
+
+const logger = new Logger("test-paivana-head.ts");
+
+interface RawResponse {
+ status: number;
+ headers: http.IncomingHttpHeaders;
+}
+
+/**
+ * Issue a single request without following redirects.
+ *
+ * Uses node's HTTP client directly because the harness HTTP library does not
+ * offer the HEAD method.
+ */
+function request(url: string, method: "GET" | "HEAD"): Promise<RawResponse> {
+ return new Promise<RawResponse>((resolve, reject) => {
+ const req = http.request(url, { method }, (resp) => {
+ // Drain the body so the socket is released.
+ resp.resume();
+ resp.on("end", () =>
+ resolve({ status: resp.statusCode ?? 0, headers: resp.headers }),
+ );
+ });
+ req.on("error", reject);
+ req.end();
+ });
+}
+
+/**
+ * Check that HEAD requests are answered like GET requests all the way to the
+ * paywall page, instead of being redirected back to the paywall URL forever.
+ */
+export async function runPaivanaHeadTest(t: GlobalTestState) {
+ const { paivana } = await createSimpleTestkudosEnvironmentV3(t, undefined, {
+ // Every page except the site root is behind the paywall, so that the URL
+ // the paywall page itself is served from is covered too: a request that is
+ // wrongly subjected to the paywall check there redirects to itself. The
+ // root has to stay outside, because the harness readiness probe for
+ // paivana insists on a 200 and would otherwise never see one.
+ paivanaWebsite: "^https?://[^/]+/.+",
+ });
+
+ const website = `${paivana.baseUrl}index.html`;
+
+ for (const method of ["GET", "HEAD"] as const) {
+ const first = await request(website, method);
+ const location = first.headers["location"];
+ logger.info(`${method} ${website}: ${first.status} -> ${location}`);
+ t.assertDeepEqual(first.status, 302);
+ t.assertTrue(location != null);
+
+ // The fragment carries the website the payment will be for; the browser
+ // does not send it back, so the paywall URL is what paivana sees next.
+ const paywallUrl = location.split("#")[0];
+ t.assertDeepEqual(
+ paywallUrl,
+ `${paivana.baseUrl}.well-known/paivana/templates/paivana`,
+ );
+
+ const second = await request(paywallUrl, method);
+ logger.info(`${method} ${paywallUrl}: ${second.status}`);
+ t.assertDeepEqual(second.status, 402);
+ // The wallet needs the taler://pay-template/ URI from this header.
+ t.assertTrue(second.headers["paivana"] != null);
+ }
+}
+
+runPaivanaHeadTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -137,6 +137,7 @@ import { runMerchantTokenfamiliesTest } from "./test-merchant-tokenfamilies.js";
import { runMerchantWireTest } from "./test-merchant-wire.js";
import { runMultiExchangeTest } from "./test-multiexchange.js";
import { runOtpTest } from "./test-otp.js";
+import { runPaivanaHeadTest } from "./test-paivana-head.js";
import { runPaivanaRepurchaseTest } from "./test-paivana-repurchase.js";
import { runPaivanaTest } from "./test-paivana.js";
import { runPayPaidTest } from "./test-pay-paid.js";
@@ -473,6 +474,7 @@ const allTests: TestMainFunction[] = [
runPreparedTransferTest,
runPaivanaTest,
runPaivanaRepurchaseTest,
+ runPaivanaHeadTest,
runKycFormValidationTest,
runKycMerchantWalletReuseTest,
runWithdrawalShortenTest,