Files
computer-playbook/roles/web-app-joomla/files/playwright/test-oidc-fallback.js
kevinveenbirkenbachandClaude Opus 4.8 07c6ad8e7c test(e2e): scale every nav click + timeout literal for onion targets
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.

Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).

Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:35:53 +02:00

54 lines
2.7 KiB
JavaScript

const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("./timeouts");
const { skipUnlessServiceEnabled } = require("./service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, gotoOnion } = require("./personas");
const joomlaBaseUrl = normalizeBaseUrl(process.env.JOOMLA_BASE_URL);
const adminUsername = decodeDotenvQuotedValue(process.env.ADMIN_USERNAME);
const adminPassword = decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD);
test.use({ ignoreHTTPSErrors: true });
async function performJoomlaAdminFormLogin(page, baseUrl, username, password) {
// Local Joomla form-login at /administrator?fallback=local. The
// `?fallback=local` query short-circuits the plg_system_keycloak
// redirect so the operator has an emergency hatch when Keycloak is
// unavailable (per the documented Modus 3 contract).
await gotoOnion(page, `${baseUrl}/administrator?fallback=local`, { waitUntil: "domcontentloaded" });
const usernameField = page.locator("input[name='username']");
const passwordField = page.locator("input[name='passwd']");
await usernameField.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
await usernameField.fill(username);
await passwordField.fill(password);
await Promise.all([
page.waitForLoadState("domcontentloaded"),
page.locator("button[type='submit'], input[type='submit']").first().click({ timeout: resolveTimeout(30_000) }),
]);
}
test("OIDC: /administrator?fallback=local hatch bypasses Keycloak and accepts the local Joomla form (Modus 3 emergency path)", async ({ page }) => {
// The local form-login fallback at /administrator?fallback=local
// is the operationally-mandated hatch when Keycloak is unavailable
// (per the documented Modus 3 contract). It MUST NOT redirect to the IdP,
// and the local form MUST accept the bootstrap administrator
// credentials.
expect(joomlaBaseUrl, "JOOMLA_BASE_URL must be set in the Playwright env file").toBeTruthy();
expect(adminUsername, "ADMIN_USERNAME must be set in the Playwright env file").toBeTruthy();
expect(adminPassword, "ADMIN_PASSWORD must be set in the Playwright env file").toBeTruthy();
await page.context().clearCookies();
skipUnlessServiceEnabled("sso");
const expectedJoomlaBaseUrl = joomlaBaseUrl.replace(/\/$/, "");
await performJoomlaAdminFormLogin(page, expectedJoomlaBaseUrl, adminUsername, adminPassword);
// Joomla 6 uses `body.com_cpanel` on the admin home; the broader
// fallback set covers future template tweaks.
const controlPanelMarker = page
.locator("body.com_cpanel, #sidebarmenu, nav[aria-label='Main menu'], a[href*='option=com_cpanel']")
.first();
await controlPanelMarker.waitFor({ state: "visible", timeout: resolveTimeout(60_000) });
});