mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
Three assertions in these specs passed whether or not the thing they name happened. The native login test navigates to `/admin/login` and then asserted that the URL contains `/admin`, which it already did before anyone typed a password, next to a check that `body` is visible, which is true the moment the document parses. A failed login scored green. It now waits for the URL to leave `/login` and for the password field it just filled to be gone. The SSO login test asserted `page.url()` once, immediately after a settle whose result is discarded, so whether it saw the finished redirect was luck. Its two sibling tests in the same file poll for the Pi-hole base URL and wait out `/oauth2/callback` first; this one never did. It does now, and it also rejects being parked on the callback, which nothing checked before. The denial test counted `/oauth2/callback` and the auth endpoint as evidence of denial. Both appear mid-handoff on a login that is about to SUCCEED, so an authorization regression could pass here unnoticed. The check becomes a poll over three distinct states: still handing off keeps polling, back at the auth endpoint or a forbidden body is denial, anything else fails and reports where the browser actually landed. A transient no longer reads as a pass. The obvious form of that fix — requiring the URL to leave `/oauth2/callback` before judging — is avoided on purpose: oauth2-proxy may render its 403 on the callback path itself, and the test would then time out on a correct denial. Judging the state rather than the path does not care. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
48 lines
2.1 KiB
JavaScript
48 lines
2.1 KiB
JavaScript
// @ts-check
|
|
// Scenario: Native admin login (Keycloak not enabled)
|
|
const { test, expect } = require("@playwright/test");
|
|
const { resolveTimeout } = require("./timeouts");
|
|
const { gotoOnion } = require("./personas");
|
|
|
|
test.use({ ignoreHTTPSErrors: true });
|
|
|
|
function decodeDotenvQuotedValue(value) {
|
|
if (typeof value !== "string" || value.length < 2) return value;
|
|
if (!(value.startsWith('"') && value.endsWith('"'))) return value;
|
|
const encoded = value.slice(1, -1);
|
|
try { return JSON.parse(`"${encoded}"`).replace(/\$\$/g, "$"); }
|
|
catch { return encoded.replace(/\$\$/g, "$"); }
|
|
}
|
|
|
|
const oidcIssuerUrl = decodeDotenvQuotedValue(process.env.OIDC_ISSUER_URL);
|
|
const piholeBaseUrl = decodeDotenvQuotedValue(process.env.PIHOLE_BASE_URL);
|
|
const adminPassword = decodeDotenvQuotedValue(process.env.ADMIN_PASSWORD);
|
|
|
|
test.beforeEach(() => {
|
|
test.skip(!!oidcIssuerUrl, "SSO is enabled — skipping native login tests");
|
|
expect(piholeBaseUrl, "PIHOLE_BASE_URL must be set").toBeTruthy();
|
|
expect(adminPassword, "ADMIN_PASSWORD must be set").toBeTruthy();
|
|
});
|
|
|
|
test("administrator: can log in natively to pihole", async ({ page }) => {
|
|
const expectedPiholeBaseUrl = piholeBaseUrl.replace(/\/$/, "");
|
|
|
|
await gotoOnion(page, `${expectedPiholeBaseUrl}/admin/login`);
|
|
await page.waitForLoadState("networkidle", { timeout: resolveTimeout(30_000) }).catch(() => {});
|
|
|
|
const passwordInput = page.getByRole("textbox", { name: /password/i });
|
|
await passwordInput.waitFor({ state: "visible", timeout: resolveTimeout(30_000) });
|
|
await passwordInput.fill(adminPassword);
|
|
// Pi-hole v6 login button text is "Log in (uses cookie)"
|
|
await page.getByRole("button", { name: /log in|sign in/i }).click({ timeout: resolveTimeout(30_000) });
|
|
|
|
await page.waitForLoadState("networkidle", { timeout: resolveTimeout(30_000) }).catch(() => {});
|
|
await expect
|
|
.poll(() => page.url(), {
|
|
timeout: resolveTimeout(30_000),
|
|
message: `native login must leave the login form, still at ${page.url()}`,
|
|
})
|
|
.not.toContain("/login");
|
|
await expect(passwordInput).toBeHidden({ timeout: resolveTimeout(30_000) });
|
|
});
|