Files
computer-playbook/roles/web-app-mediawiki/files/playwright/addons/PluggableAuth.spec.js
kevinveenbirkenbachandClaude Opus 4.8 4c91df5de7 test(e2e): forbid raw page.goto, migrate all specs to Tor-resilient gotoOnion
New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:59:38 +02:00

93 lines
3.8 KiB
JavaScript

const { test, expect } = require("@playwright/test");
const { resolveTimeout } = require("../timeouts");
const { skipUnlessAddonEnabled } = require("../addon-gating");
const { skipUnlessServiceEnabled } = require("../service-gating");
const { decodeDotenvQuotedValue, normalizeBaseUrl, gotoOnion } = require("../personas");
test.use({ ignoreHTTPSErrors: true });
const appBaseUrl = normalizeBaseUrl(process.env.APP_BASE_URL || "");
const canonicalDomain = decodeDotenvQuotedValue(process.env.CANONICAL_DOMAIN || "");
// PluggableAuth is the auth framework backing OpenIDConnect. The role wires it
// with EnableAutoLogin=true / EnableLocalLogin=false, so a wiki where the
// framework is actually loaded AND bound to the OIDC plugin must NOT serve the
// stock local username/password login form on Special:UserLogin — it must
// instead hand off to the OIDC issuer (Keycloak) or render only the
// PluggableAuth SSO affordance. This test fails if PluggableAuth is absent or
// not coupled to the IdP.
test("PluggableAuth: framework replaces local login and is bound to the OIDC issuer", async ({
page,
}) => {
skipUnlessAddonEnabled("PluggableAuth");
skipUnlessServiceEnabled("sso");
expect(appBaseUrl, "APP_BASE_URL must be set").toBeTruthy();
expect(canonicalDomain, "CANONICAL_DOMAIN must be set").toBeTruthy();
await page.context().clearCookies();
const loginUrl = `${appBaseUrl}/index.php?title=Special:UserLogin`;
await gotoOnion(page, loginUrl, { waitUntil: "domcontentloaded", timeout: resolveTimeout(60_000) });
// Let an EnableAutoLogin handoff to the IdP settle.
await page
.waitForLoadState("networkidle", { timeout: resolveTimeout(60_000) })
.catch(() => {});
const finalUrl = page.url();
const redirectedAway = !finalUrl.includes("Special:UserLogin");
if (redirectedAway) {
// EnableAutoLogin handed off to the configured OIDC issuer. Prove the
// landing page is genuinely an OAuth2/OIDC authorization surface coupled to
// this wiki, not an arbitrary redirect: the OIDC client sends the wiki's
// redirect_uri back to itself and Keycloak renders a recognizable login.
const url = new URL(finalUrl);
const looksLikeAuthEndpoint =
/\/(auth|authorize|protocol\/openid-connect\/auth|oauth2?\/authorize)/i.test(
url.pathname,
) || url.searchParams.has("redirect_uri");
expect(
looksLikeAuthEndpoint,
`auto-login should land on the OIDC authorization endpoint, got ${finalUrl}`,
).toBeTruthy();
// The OAuth request must carry this wiki as the client redirect target,
// proving the clientID/providerURL coupling from $wgPluggableAuth_Config.
const redirectUri = url.searchParams.get("redirect_uri") || "";
if (redirectUri) {
expect(
decodeURIComponent(redirectUri),
"OIDC redirect_uri must point back at this wiki",
).toContain(canonicalDomain);
}
await expect(page.locator("body")).toContainText(
/sign in|log ?in|username|password|keycloak|openid/i,
{ timeout: resolveTimeout(60_000) },
);
return;
}
// No auto-redirect: we are still on Special:UserLogin. PluggableAuth with
// EnableLocalLogin=false must have suppressed the stock local credential form
// and exposed only the SSO affordance bound to OIDC.
const body = page.locator("body");
await expect(body).toContainText(
/single sign[- ]?on|openid|\bsso\b|log ?in with|continue with|keycloak/i,
{ timeout: resolveTimeout(60_000) },
);
// Stock MediaWiki local login has a password input named "wpPassword". With
// EnableLocalLogin=false the framework removes it; its presence would mean
// PluggableAuth is not actually driving the login surface.
await expect(
page.locator('input[name="wpPassword"]'),
"local password field must be absent when PluggableAuth owns login",
).toHaveCount(0);
});