fix(test-e2e-playwright): gate shared personas on dashboard + oidc service flags

CI run 26089179898 job 76714878217 (Deploy web-app-bluesky, variant 1)
failed at the mailu Playwright `guest: public-landing → auth chain →
never authenticated` test with 60s timeout × 3 retries. Root cause is
two layered defects:

1. Between Round 1 and Round 2 of the matrix deploy the fresh-purge
   sweep removes the web-app-dashboard vhost and container (mandate-
   fresh-purged on first contact with a new VARIANT, see commit
   3f12fbe83). Round 2 in variant 1 deploys only matomo/mailu/keycloak/
   bluesky; the dashboard.infinito.example vhost never comes back.

2. mailu's templates/playwright.env.j2 pins APP_BASE_URL to
   `lookup('tls', 'web-app-dashboard', 'url.base')` unconditionally —
   it has to, because the shared persona helpers route through the
   dashboard main-frame Account menu. In variant 1 (dashboard disabled
   for mailu) that URL is dangling: nginx's default_server falls onto
   the bluesky-pds backend, which serves a text/plain ASCII banner.
   readCspString's `<meta>` fallback in personas/utils/csp.js has no
   explicit getAttribute timeout and waits on the test-level 60s.

The shared persona itself has no way to learn that APP_BASE_URL routes
through a dashboard frame whose service is disabled — biber + admin
sidestepped this only because mailu already blocks them via
PERSONA_BIBER_BLOCKED / PERSONA_ADMINISTRATOR_BLOCKED for an unrelated
silent-SSO escape. guest had no equivalent opt-out and timed out.

Decided by service flag, not by sniffing the response, so the skip
fires before any network I/O:

- roles/test-e2e-playwright/files/personas/guest.js: skip when
  APP_BASE_URL's host starts with "dashboard." AND
  DASHBOARD_SERVICE_ENABLED=false.
- roles/test-e2e-playwright/files/personas/biber.js +
  personas/admin.js: same dashboard-frame skip + a new OIDC-required
  skip (these personas drive the Keycloak round-trip; without OIDC
  they cannot reach an authenticated surface). Skips placed
  immediately before the dependent operation, not at the top, so the
  reason matches the operation that would actually fail. parity-marker
  safeIsEnabled("dashboard") added to the existing oauth2/logout/
  matomo block so the env-gate parity guard recognises the new
  consumption.
- roles/web-app-mailu/templates/playwright.env.j2: emit
  DASHBOARD_SERVICE_ENABLED in the existing OIDC/LOGOUT/MATOMO_SERVICE
  _ENABLED block. The other 14 dashboard-frame roles will pick up
  this declaration when they hit the same variant-disabled failure.

biber + admin remain unconditional-blocked for mailu via the existing
PERSONA_*_BLOCKED flags; the new skips inside them are forward-looking
for the day the silent-SSO escape lifts and those personas are
un-blocked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.7 committed 2026-05-19 17:00:38 +02:00
1 parent 76dfdf78f7
commit 1f6b5f8942
4 files changed
+64

No files matched your search

@@ -60,6 +60,7 @@ async function runAdminFlow(page, opts = {}) {
safeIsEnabled("oauth2");
safeIsEnabled("logout");
safeIsEnabled("matomo");
safeIsEnabled("dashboard");
const canonicalDomain = readEnv("CANONICAL_DOMAIN");
const appBaseUrl = normalizeUrl(process.env.APP_BASE_URL);
@@ -81,6 +82,30 @@ async function runAdminFlow(page, opts = {}) {
const oidcEnabled = safeIsEnabled("oidc");
// The administrator journey relies on the OIDC login chain to
// authenticate; when OIDC is off, no Keycloak round-trip happens
// and the persona cannot reach an authenticated surface.
if (!oidcEnabled) {
test.skip(
true,
"administrator persona requires OIDC, but OIDC_SERVICE_ENABLED=false for this role.",
);
return;
}
// Dashboard vhost is purged between variants; skip before any I/O
// when APP_BASE_URL routes through a disabled dashboard frame.
if (new URL(appBaseUrl).hostname.toLowerCase().startsWith("dashboard.")) {
const dashboardEnabled = (process.env.DASHBOARD_SERVICE_ENABLED || "").toLowerCase();
if (dashboardEnabled === "false") {
test.skip(
true,
"administrator persona collapsed: APP_BASE_URL routes via the dashboard frame but DASHBOARD_SERVICE_ENABLED=false for this variant.",
);
return;
}
}
// Direct-app entry: bookmark-style navigation. The OAuth2-Proxy gate
// fires on the first request, redirecting unauthenticated requests
// to Keycloak; the auth chain is the same regardless of how the user
@@ -60,6 +60,7 @@ async function runBiberFlow(page, opts = {}) {
safeIsEnabled("oauth2");
safeIsEnabled("logout");
safeIsEnabled("matomo");
safeIsEnabled("dashboard");
const canonicalDomain = readEnv("CANONICAL_DOMAIN");
const appBaseUrl = normalizeUrl(process.env.APP_BASE_URL);
@@ -80,6 +81,19 @@ async function runBiberFlow(page, opts = {}) {
await page.context().clearCookies();
// Dashboard vhost is purged between variants; skip before any I/O
// when APP_BASE_URL routes through a disabled dashboard frame.
if (new URL(appBaseUrl).hostname.toLowerCase().startsWith("dashboard.")) {
const dashboardEnabled = (process.env.DASHBOARD_SERVICE_ENABLED || "").toLowerCase();
if (dashboardEnabled === "false") {
test.skip(
true,
"biber persona collapsed: APP_BASE_URL routes via the dashboard frame but DASHBOARD_SERVICE_ENABLED=false for this variant.",
);
return;
}
}
// Direct-app entry: bookmark-style navigation. The OAuth2-Proxy gate
// fires on the first request, redirecting unauthenticated requests
// to Keycloak; the auth chain is the same regardless of how the user
@@ -88,6 +102,17 @@ async function runBiberFlow(page, opts = {}) {
const oidcEnabled = safeIsEnabled("oidc");
// The biber journey relies on the OIDC login chain to authenticate;
// when OIDC is off, no Keycloak round-trip happens and the persona
// cannot reach an authenticated surface.
if (!oidcEnabled) {
test.skip(
true,
"biber persona requires OIDC, but OIDC_SERVICE_ENABLED=false for this role.",
);
return;
}
// Two auth shapes share a single login step:
// * oauth2-proxy gate: the goto is intercepted and the page lands
// directly on the Keycloak auth endpoint; perform Keycloak login.
@@ -42,6 +42,19 @@ async function runGuestFlow(page = {}) {
return;
}
// Dashboard vhost is purged between variants; skip before any I/O
// when APP_BASE_URL routes through a disabled dashboard frame.
if (new URL(appBaseUrl).hostname.toLowerCase().startsWith("dashboard.")) {
const dashboardEnabled = (process.env.DASHBOARD_SERVICE_ENABLED || "").toLowerCase();
if (dashboardEnabled === "false") {
test.skip(
true,
"guest persona collapsed: APP_BASE_URL routes via the dashboard frame but DASHBOARD_SERVICE_ENABLED=false for this variant.",
);
return;
}
}
test.setTimeout(60_000);
const startUrl = appBaseUrl;
@@ -26,3 +26,4 @@ MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }}
OIDC_SERVICE_ENABLED={{ lookup('config', application_id, 'services.oidc.enabled') | string | lower }}
LOGOUT_SERVICE_ENABLED={{ lookup('config', application_id, 'services.logout.enabled') | string | lower }}
MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }}
DASHBOARD_SERVICE_ENABLED={{ lookup('config', application_id, 'services.dashboard.enabled') | string | lower }}