taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Apackages/taler-harness/src/integrationtests/test-paivana-head.ts | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
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,