From 529876c7ca95ce7b6d0c48751ddbbf6dd4342df0 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sun, 18 Oct 2026 15:03:33 +0200 Subject: [PATCH] fix(test-e2e-playwright): judge HSTS on the app's own response, not the SSO page gotoOnion follows the redirect chain, so a front page behind oauth2-proxy returned the Keycloak login page and expectHstsWhenTls read the issuer's headers. On an onion pick the clearnet issuer's HSTS then failed the 'must not emit HSTS over plaintext onion transport' assertion although the onion vhost sent none. Add ownResponseHeaders, which walks redirectedFrom() back to the first response, and use it in the eight specs whose front page redirects to SSO: lam, kix, magento, openproject, pgadmin, phpldapadmin, phpmyadmin and pihole. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../files/personas/utils/tls.js | 17 ++++++++++++++++- .../files/playwright/test-kix-root-tls-hsts.js | 5 ++--- .../files/playwright/playwright.spec.js | 5 ++--- .../files/playwright/playwright.spec.js | 5 ++--- .../files/playwright/test-baseline.js | 5 ++--- .../files/playwright/playwright.spec.js | 5 ++--- .../files/playwright/playwright.spec.js | 5 ++--- .../files/playwright/playwright.spec.js | 5 ++--- .../files/playwright/playwright.spec.js | 5 ++--- 9 files changed, 32 insertions(+), 25 deletions(-) diff --git a/roles/test-e2e-playwright/files/personas/utils/tls.js b/roles/test-e2e-playwright/files/personas/utils/tls.js index f5bb99f122..809fec0b13 100644 --- a/roles/test-e2e-playwright/files/personas/utils/tls.js +++ b/roles/test-e2e-playwright/files/personas/utils/tls.js @@ -30,4 +30,19 @@ function expectHstsWhenTls(headers, baseUrl, label) { } } -module.exports = { isTlsBaseUrl, expectHstsWhenTls }; +/** + * Headers of the first response a navigation received, before any redirect. + * + * @param {import("@playwright/test").Response} response final navigation response + * @returns {Promise>} headers the app's own vhost sent + */ +async function ownResponseHeaders(response) { + let request = response.request(); + while (request.redirectedFrom()) { + request = request.redirectedFrom(); + } + const own = await request.response(); + return (own || response).headers(); +} + +module.exports = { isTlsBaseUrl, expectHstsWhenTls, ownResponseHeaders }; diff --git a/roles/web-app-kix/files/playwright/test-kix-root-tls-hsts.js b/roles/web-app-kix/files/playwright/test-kix-root-tls-hsts.js index 242dcd0bb6..8665989d2b 100644 --- a/roles/web-app-kix/files/playwright/test-kix-root-tls-hsts.js +++ b/roles/web-app-kix/files/playwright/test-kix-root-tls-hsts.js @@ -1,12 +1,11 @@ const { test, expect } = require("@playwright/test"); -const { expectHstsWhenTls, gotoOnion } = require("./personas"); +const { expectHstsWhenTls, ownResponseHeaders, gotoOnion } = require("./personas"); exports.register = function (shared) { test("kix root emits TLS+HSTS", async ({ page }) => { const response = await gotoOnion(page, `${shared.env.appBaseUrl}/`); expect(response, "Expected a response from the KIX root").toBeTruthy(); expect(response.status(), "Expected KIX root status < 500").toBeLessThan(500); - const headers = response.headers(); - expectHstsWhenTls(headers, shared.env.appBaseUrl, "kix"); + expectHstsWhenTls(await ownResponseHeaders(response), shared.env.appBaseUrl, "kix"); }); }; diff --git a/roles/web-app-lam/files/playwright/playwright.spec.js b/roles/web-app-lam/files/playwright/playwright.spec.js index 4a8f07a760..83189b00e2 100644 --- a/roles/web-app-lam/files/playwright/playwright.spec.js +++ b/roles/web-app-lam/files/playwright/playwright.spec.js @@ -1,7 +1,7 @@ const { test, expect } = require("@playwright/test"); const { resolveTimeout } = require("./timeouts"); -const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas"); +const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, ownResponseHeaders, gotoOnion } = require("./personas"); test.use({ ignoreHTTPSErrors: true }); const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || ""); @@ -21,8 +21,7 @@ test("LAM front page is served under canonical domain with TLS", async ({ page } response.url().includes(canonicalDomain), `Expected canonical domain "${canonicalDomain}" to back the LAM URL` ).toBe(true); - const headers = response.headers(); - expectHstsWhenTls(headers, appBaseUrl, "LAM"); + expectHstsWhenTls(await ownResponseHeaders(response), appBaseUrl, "LAM"); }); test("LAM returns HTML content under canonical domain", async ({ request }) => { diff --git a/roles/web-app-magento/files/playwright/playwright.spec.js b/roles/web-app-magento/files/playwright/playwright.spec.js index da95cdfee1..e1bfa5b179 100644 --- a/roles/web-app-magento/files/playwright/playwright.spec.js +++ b/roles/web-app-magento/files/playwright/playwright.spec.js @@ -1,7 +1,7 @@ const { test, expect } = require("@playwright/test"); const { resolveTimeout } = require("./timeouts"); -const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas"); +const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, ownResponseHeaders, gotoOnion } = require("./personas"); test.use({ ignoreHTTPSErrors: true }); const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || ""); @@ -21,8 +21,7 @@ test("Magento front page is served under canonical domain with TLS", async ({ pa response.url().includes(canonicalDomain), `Expected canonical domain "${canonicalDomain}" to back the Magento URL` ).toBe(true); - const headers = response.headers(); - expectHstsWhenTls(headers, appBaseUrl, "Magento"); + expectHstsWhenTls(await ownResponseHeaders(response), appBaseUrl, "Magento"); }); test("Magento returns HTML content under canonical domain", async ({ request }) => { diff --git a/roles/web-app-openproject/files/playwright/test-baseline.js b/roles/web-app-openproject/files/playwright/test-baseline.js index 2cd9c6892c..02f3f8c3aa 100644 --- a/roles/web-app-openproject/files/playwright/test-baseline.js +++ b/roles/web-app-openproject/files/playwright/test-baseline.js @@ -1,7 +1,7 @@ const { test, expect } = require("@playwright/test"); const { resolveTimeout } = require("./timeouts"); -const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas"); +const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, ownResponseHeaders, gotoOnion } = require("./personas"); test.use({ ignoreHTTPSErrors: true }); const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || ""); @@ -21,8 +21,7 @@ test("OpenProject front page is served under canonical domain with TLS", async ( response.url().includes(canonicalDomain), `Expected canonical domain "${canonicalDomain}" to back the OpenProject URL` ).toBe(true); - const headers = response.headers(); - expectHstsWhenTls(headers, appBaseUrl, "OpenProject"); + expectHstsWhenTls(await ownResponseHeaders(response), appBaseUrl, "OpenProject"); }); test("OpenProject returns HTML content under canonical domain", async ({ request }) => { diff --git a/roles/web-app-pgadmin/files/playwright/playwright.spec.js b/roles/web-app-pgadmin/files/playwright/playwright.spec.js index 7ce614fdc0..16feba5ea4 100644 --- a/roles/web-app-pgadmin/files/playwright/playwright.spec.js +++ b/roles/web-app-pgadmin/files/playwright/playwright.spec.js @@ -1,7 +1,7 @@ const { test, expect } = require("@playwright/test"); const { resolveTimeout } = require("./timeouts"); -const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas"); +const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, ownResponseHeaders, gotoOnion } = require("./personas"); test.use({ ignoreHTTPSErrors: true }); const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || ""); @@ -21,8 +21,7 @@ test("pgAdmin front page is served under canonical domain with TLS", async ({ pa response.url().includes(canonicalDomain), `Expected canonical domain "${canonicalDomain}" to back the pgAdmin URL` ).toBe(true); - const headers = response.headers(); - expectHstsWhenTls(headers, appBaseUrl, "pgAdmin"); + expectHstsWhenTls(await ownResponseHeaders(response), appBaseUrl, "pgAdmin"); }); test("pgAdmin returns HTML content under canonical domain", async ({ request }) => { diff --git a/roles/web-app-phpldapadmin/files/playwright/playwright.spec.js b/roles/web-app-phpldapadmin/files/playwright/playwright.spec.js index a1f5c71926..ee4cba4d70 100644 --- a/roles/web-app-phpldapadmin/files/playwright/playwright.spec.js +++ b/roles/web-app-phpldapadmin/files/playwright/playwright.spec.js @@ -1,7 +1,7 @@ const { test, expect } = require("@playwright/test"); const { resolveTimeout } = require("./timeouts"); -const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas"); +const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, ownResponseHeaders, gotoOnion } = require("./personas"); test.use({ ignoreHTTPSErrors: true }); const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || ""); @@ -21,8 +21,7 @@ test("phpLDAPadmin front page is served under canonical domain with TLS", async response.url().includes(canonicalDomain), `Expected canonical domain "${canonicalDomain}" to back the phpLDAPadmin URL` ).toBe(true); - const headers = response.headers(); - expectHstsWhenTls(headers, appBaseUrl, "phpLDAPadmin"); + expectHstsWhenTls(await ownResponseHeaders(response), appBaseUrl, "phpLDAPadmin"); }); test("phpLDAPadmin returns HTML content under canonical domain", async ({ request }) => { diff --git a/roles/web-app-phpmyadmin/files/playwright/playwright.spec.js b/roles/web-app-phpmyadmin/files/playwright/playwright.spec.js index dbaf3020f5..0df8a7c4c7 100644 --- a/roles/web-app-phpmyadmin/files/playwright/playwright.spec.js +++ b/roles/web-app-phpmyadmin/files/playwright/playwright.spec.js @@ -1,7 +1,7 @@ const { test, expect } = require("@playwright/test"); const { resolveTimeout } = require("./timeouts"); -const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, gotoOnion } = require("./personas"); +const { decodeDotenvQuotedValue, normalizeBaseUrl, runAdminFlow, runBiberFlow, runGuestFlow , expectHstsWhenTls, ownResponseHeaders, gotoOnion } = require("./personas"); test.use({ ignoreHTTPSErrors: true }); const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || ""); @@ -21,8 +21,7 @@ test("phpMyAdmin front page is served under canonical domain with TLS", async ({ response.url().includes(canonicalDomain), `Expected canonical domain "${canonicalDomain}" to back the phpMyAdmin URL` ).toBe(true); - const headers = response.headers(); - expectHstsWhenTls(headers, appBaseUrl, "phpMyAdmin"); + expectHstsWhenTls(await ownResponseHeaders(response), appBaseUrl, "phpMyAdmin"); }); test("phpMyAdmin returns HTML content under canonical domain", async ({ request }) => { diff --git a/roles/web-app-pihole/files/playwright/playwright.spec.js b/roles/web-app-pihole/files/playwright/playwright.spec.js index e2e69c7fe9..fbf97e836c 100644 --- a/roles/web-app-pihole/files/playwright/playwright.spec.js +++ b/roles/web-app-pihole/files/playwright/playwright.spec.js @@ -1,7 +1,7 @@ // @ts-check const { test, expect } = require("@playwright/test"); const { resolveTimeout } = require("./timeouts"); -const { expectHstsWhenTls, gotoOnion } = require("./personas"); +const { expectHstsWhenTls, ownResponseHeaders, gotoOnion } = require("./personas"); test.use({ ignoreHTTPSErrors: true }); @@ -31,8 +31,7 @@ test("Pi-hole front page is served under canonical domain with TLS", async ({ pa `Expected canonical domain "${canonicalDomain}" to back the Pi-hole URL` ).toBe(true); } - const headers = response.headers(); - expectHstsWhenTls(headers, piholeBaseUrl, "Pi-hole"); + expectHstsWhenTls(await ownResponseHeaders(response), piholeBaseUrl, "Pi-hole"); }); test("Pi-hole returns HTML content under canonical domain", async ({ request }) => {