import { readFile } from "node:fs/promises"; import { X509Certificate, createPrivateKey } from "node:crypto"; import type { APIKeyCreationResponse, Certificate } from "../web/src/api/types"; import { expect, login, navigate, screenshot, test } from "./fixtures"; test("login validation, session persistence and logout", async ({ page }) => { await page.goto("/certificates"); await expect(page).toHaveURL("/"); await expect( page.getByLabel("Break-glass administrator token"), ).toBeVisible(); await screenshot(page, "login.png"); await page.getByLabel("Break-glass administrator token").fill("invalid"); await page.getByRole("button", { name: "Sign in", exact: true }).click(); await expect( page.getByText("Error: Invalid token", { exact: true }), ).toBeVisible(); await screenshot(page, "login-error.png"); await login(page); await page.reload(); await expect( page.getByText("Bootstrap administrator", { exact: true }), ).toBeVisible(); await page.getByRole("button", { name: "Sign out" }).click(); await expect( page.getByRole("button", { name: "Sign in", exact: true }), ).toBeVisible(); await page.reload(); expect((await page.request.get("/api/v1/session")).status()).toBe(401); }); for (const endpoint of ["/api/v1/session", "/auth/methods"]) { test(`stalled startup request for ${endpoint} shows a startup error`, async ({ page, }) => { await page.route(`**${endpoint}`, () => {}); await page.goto("/"); if (endpoint === "/api/v1/session") { await expect( page.getByLabel("Break-glass administrator token"), ).toBeVisible({ timeout: 25_000 }); await expect(page.getByRole("alert")).toContainText( "Unable to check your session", ); } else { await expect( page.getByText("Unable to load sign-in options.", { exact: false }), ).toBeVisible({ timeout: 25_000 }); await expect(page.locator(".splash")).toHaveCount(0); } }); } test("certificate list, grid, version history and pending state", async ({ page, }) => { await login(page); await screenshot(page, "certificates.png"); await page.getByRole("button", { name: "Grid", exact: true }).click(); await expect( page.getByRole("button", { name: "Grid", exact: true }), ).toHaveAttribute("aria-pressed", "true"); await screenshot(page, "certificates-grid.png"); await page .getByRole("heading", { name: "homelab-wildcard", exact: true }) .click(); await expect( page.getByText("Previous version", { exact: true }), ).toBeVisible(); await screenshot(page, "certificate-details.png"); await page.getByRole("button", { name: "Close", exact: true }).click(); await page .getByRole("heading", { name: "internal-gateway", exact: true }) .click(); await expect(page.getByText("No certificate versions stored.")).toBeVisible(); await expect(page.locator(".modal a[aria-disabled=true]")).toHaveCount(4); await screenshot(page, "certificate-pending.png"); }); test("renewal persists and downloads real certificate and private key", async ({ page, }) => { await login(page); const card = page.getByRole("article").filter({ has: page.getByRole("heading", { name: "internal-gateway", exact: true }), }); const admissionEvent = page.waitForResponse( (response) => response.url().endsWith("/certificates/internal-gateway/renew") && response.request().method() === "POST", ); await card.getByRole("button", { name: "Renew", exact: true }).click(); const admission = await admissionEvent; expect(admission.status()).toBe(202); const accepted = (await admission.json()) as { job_id: number }; expect(accepted.job_id).toBeGreaterThan(0); await expect(card.getByText("valid", { exact: true })).toBeVisible(); await expect( card.getByRole("button", { name: "Renew", exact: true }), ).toBeEnabled(); const completed = await page.request.get(`/api/v1/jobs/${accepted.job_id}`); expect(completed.status()).toBe(200); expect(((await completed.json()) as { status: string }).status).toBe( "succeeded", ); await page.reload(); await card.getByRole("heading").click(); await expect( page.getByText("Current version", { exact: true }), ).toBeVisible(); const artifacts: Buffer[] = []; for (const name of ["Certificate", "Private key"]) { const downloadEvent = page.waitForEvent("download"); await page .locator(".modal") .getByRole("link", { name, exact: true }) .click(); const download = await downloadEvent; expect(await download.failure()).toBeNull(); artifacts.push(await readFile(await download.path())); } const certificate = new X509Certificate(artifacts[0]); expect(certificate.subjectAltName).toContain("gateway.example.com"); expect( certificate.checkPrivateKey(createPrivateKey(artifacts[1])), ).toBeTruthy(); await page.getByRole("button", { name: "Close", exact: true }).click(); await navigate(page, "history"); await expect( page.getByRole("row").filter({ hasText: "internal-gateway" }), ).toContainText("succeeded"); }); for (const name of ["homelab-wildcard", "internal-gateway"]) { test(`open certificate details refresh after issuance for ${name}`, async ({ page, }) => { await page.clock.install(); await login(page); await page.getByRole("heading", { name, exact: true }).click(); const modal = page.locator(".modal"); await expect(modal.getByText("Loading versions…")).toHaveCount(0); const previousVersionCount = await modal .locator(".version-timeline li") .count(); const initialResponse = await page.request.get( `/api/v1/certificates/${name}`, ); const initial = (await initialResponse.json()) as Certificate; expect( (await page.request.post(`/api/v1/certificates/${name}/renew`)).status(), ).toBe(202); let updated: Certificate = initial; await expect .poll(async () => { const response = await page.request.get(`/api/v1/certificates/${name}`); updated = (await response.json()) as Certificate; return updated.current_version?.id; }) .not.toBe(initial.current_version?.id); await page.clock.fastForward(30_000); await expect( modal.getByText(`Serial: ${updated.current_version!.serial}`, { exact: true, }), ).toBeVisible(); await expect( modal.getByText( `SHA-256: ${updated.current_version!.fingerprint_sha256}`, { exact: true, }, ), ).toBeVisible(); await expect(modal.locator(".version-timeline li")).toHaveCount( previousVersionCount + 1, ); await expect(modal.locator(".version-timeline li.current")).toHaveCount(1); await expect(modal.locator("a[aria-disabled=true]")).toHaveCount(0); }); } test("API key form validation, scoped access, revoke and delete", async ({ page, playwright, baseURL, }) => { await login(page); await navigate(page, "api keys"); await expect(page.locator("tbody tr")).toHaveCount(2); await screenshot(page, "api-keys.png"); await page .getByRole("button", { name: "Create API key", exact: true }) .click(); await screenshot(page, "create-api-key.png"); await page.getByRole("button", { name: "Create key", exact: true }).click(); await expect(page.getByLabel("Name", { exact: true })).toBeFocused(); expect( await page .getByLabel("Name", { exact: true }) .evaluate((input: HTMLInputElement) => input.validity.valueMissing), ).toBeTruthy(); await page.getByLabel("Name", { exact: true }).fill("Caddy deployment"); await page.getByLabel("homelab-wildcard", { exact: true }).check(); await page.getByLabel("Read private keys", { exact: true }).uncheck(); await page.getByRole("button", { name: "Create key", exact: true }).click(); await expect( page.getByRole("heading", { name: "Automatic download", exact: true }), ).toBeVisible(); await expect( page.getByRole("combobox", { name: /^Files/ }).locator("option"), ).toHaveText(["Full chain", "Certificate", "CA chain"]); await expect( page.getByRole("combobox", { name: /^Certificate/ }).locator("option"), ).toHaveText(["homelab-wildcard"]); const token = await page.locator(".token code").innerText(); const machine = await playwright.request.newContext({ baseURL, extraHTTPHeaders: { Authorization: `Bearer ${token}` }, }); try { const certificates = await machine.get("/api/v1/certificates"); expect(certificates.ok()).toBeTruthy(); expect(await certificates.json()).toEqual([ expect.objectContaining({ name: "homelab-wildcard" }), ]); expect( ( await machine.get("/api/v1/certificates/homelab-wildcard/private.key") ).status(), ).toBe(403); expect((await machine.get("/api/v1/api-keys")).status()).toBe(403); await page.reload(); await expect(page.locator(".token")).toHaveCount(0); const row = page.getByRole("row").filter({ hasText: "Caddy deployment" }); await row.getByRole("button", { name: "Revoke", exact: true }).click(); await expect( page.getByRole("heading", { name: "Revoke API key?", exact: true }), ).toBeVisible(); await page.getByRole("button", { name: "Revoke key", exact: true }).click(); await expect( row.getByRole("button", { name: "Delete", exact: true }), ).toBeVisible(); expect((await machine.get("/api/v1/certificates")).status()).toBe(401); await row.getByRole("button", { name: "Delete", exact: true }).click(); await page.getByRole("button", { name: "Delete key", exact: true }).click(); await expect(row).toHaveCount(0); await page.reload(); await expect(page.locator("tbody tr")).toHaveCount(2); } finally { await machine.dispose(); } }); test("install command builder defaults, customization and copying", async ({ page, context, }) => { await login(page); await navigate(page, "api keys"); // Create through the real API, replacing only the displayed random token. // The separate lifecycle test verifies authentication with an unmodified token. const displayToken = "cv_demo_synthetic_install_builder_token"; await page.route("**/api/v1/api-keys", async (route) => { if (route.request().method() !== "POST") return route.continue(); const response = await route.fetch(); expect(response.status()).toBe(201); const result = (await response.json()) as APIKeyCreationResponse; await route.fulfill({ response, json: { ...result, token: displayToken } }); }); await page .getByRole("button", { name: "Create API key", exact: true }) .click(); await page.getByLabel("Name", { exact: true }).fill("Caddy deployment"); await page .getByLabel("Any certificate, including certificates added later") .check(); await page.getByRole("button", { name: "Create key", exact: true }).click(); const helper = page.locator(".api-usage-helper"); const command = helper.locator(".api-command code"); await expect( helper.getByRole("heading", { name: "Automatic download" }), ).toBeVisible(); await expect( helper.getByRole("combobox", { name: /^Certificate/ }), ).toHaveValue("homelab-wildcard"); await expect(helper.getByRole("combobox", { name: /^Files/ })).toHaveValue( "fullchain.crt,private.key", ); await expect(command).toContainText( "curl -fsSL 'http://127.0.0.1:8099/client/install.sh'", ); await expect(command).toContainText(`CERTVAULT_API_KEY='${displayToken}'`); await expect(command).toContainText("--server 'http://127.0.0.1:8099'"); await expect(command).toContainText("--certificate 'homelab-wildcard'"); await expect(command).toContainText("--file 'fullchain.crt'"); await expect(command).toContainText("--file 'private.key'"); await expect(command).toContainText( "--destination '/etc/ssl/certvault/homelab-wildcard'", ); await expect(command).toContainText("--schedule '17 3 * * *'"); await expect(command).not.toContainText("--reload-command"); await screenshot(page, "installation-command-default.png", helper); await helper .getByRole("combobox", { name: /^Certificate/ }) .selectOption("monitoring-services"); await expect(helper.getByLabel("Destination folder on client")).toHaveValue( "/etc/ssl/certvault/monitoring-services", ); await helper .getByLabel("Destination folder on client") .fill("/etc/caddy/certificates"); await helper .getByRole("combobox", { name: /^Certificate/ }) .selectOption("homelab-wildcard"); await expect(helper.getByLabel("Destination folder on client")).toHaveValue( "/etc/caddy/certificates", ); await helper .getByLabel("Output name for fullchain.crt", { exact: true }) .fill("caddy.crt"); await helper .getByLabel("Output name for private.key", { exact: true }) .fill("caddy.key"); await helper .getByRole("combobox", { name: /^Schedule/ }) .selectOption("17 * * * *"); await helper .getByLabel("Command after files change (optional)") .fill("systemctl reload caddy"); await expect(command).toContainText("--file 'fullchain.crt=caddy.crt'"); await expect(command).toContainText("--file 'private.key=caddy.key'"); await expect(command).toContainText( "--destination '/etc/caddy/certificates'", ); await expect(command).toContainText("--schedule '17 * * * *'"); await expect(command).toContainText( "--reload-command 'systemctl reload caddy'", ); await screenshot(page, "installation-command-customized.png", helper); await helper.getByLabel("Installation preset").selectOption("proxmox-ve"); await expect(command).toContainText( "--file 'fullchain.crt=pveproxy-ssl.pem'", ); await expect(command).toContainText("--file 'private.key=pveproxy-ssl.key'"); await expect(command).toContainText("--destination '/etc/pve/local'"); await expect(command).toContainText("--schedule '17 3 * * *'"); await expect(command).toContainText( "--reload-command 'systemctl restart pveproxy'", ); await expect(command).toContainText( `| env CERTVAULT_API_KEY='${displayToken}' sh`, ); await expect(command).not.toContainText("sudo"); await screenshot(page, "installation-command-proxmox.png", helper); await page.setViewportSize({ width: 390, height: 844 }); await expect(helper.getByLabel("Installation preset")).toBeVisible(); expect( await page.evaluate(() => document.documentElement.scrollWidth), ).toBeLessThanOrEqual(390); await page.setViewportSize({ width: 1400, height: 900 }); await helper.getByLabel("Installation preset").selectOption(""); await helper .getByLabel("Installation preset") .selectOption("proxmox-backup-server"); await expect(command).toContainText("--file 'fullchain.crt=proxy.pem'"); await expect(command).toContainText("--file 'private.key=proxy.key'"); await expect(command).toContainText("--destination '/etc/proxmox-backup'"); await expect(command).toContainText("--schedule '17 3 * * *'"); await expect(command).toContainText( "--reload-command 'chown root:backup /etc/proxmox-backup/proxy.pem /etc/proxmox-backup/proxy.key && chmod 640 /etc/proxmox-backup/proxy.pem /etc/proxmox-backup/proxy.key && systemctl reload proxmox-backup-proxy'", ); await expect(command).not.toContainText("sudo"); await screenshot(page, "installation-command-proxmox-backup.png", helper); await page.setViewportSize({ width: 390, height: 844 }); expect( await page.evaluate(() => document.documentElement.scrollWidth), ).toBeLessThanOrEqual(390); await page.setViewportSize({ width: 1400, height: 900 }); await helper.getByLabel("Installation preset").selectOption(""); await expect(command).toContainText("--file 'fullchain.crt'"); await expect(command).not.toContainText("--reload-command"); await expect(command).toContainText("sudo env"); await context.grantPermissions(["clipboard-read", "clipboard-write"]); await expect( helper.getByLabel("Automatically update the client (requires HTTPS)"), ).toBeDisabled(); await expect(command).not.toContainText("--auto-update"); await helper .getByRole("button", { name: "Copy command", exact: true }) .click(); await expect( helper.getByRole("button", { name: "Copied!", exact: true }), ).toBeVisible(); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe( await command.innerText(), ); await helper .getByRole("combobox", { name: /^Files/ }) .selectOption("certificate.crt"); await expect( helper.getByLabel("Output name for private.key", { exact: true }), ).toHaveCount(0); await expect(command).toContainText("--file 'certificate.crt'"); await expect(command).not.toContainText("--file 'private.key"); await helper .getByLabel("Destination folder on client") .fill("/srv/client's certs"); await expect(command).toContainText( `--destination '/srv/client'"'"'s certs'`, ); await page.reload(); await expect(helper).not.toBeVisible(); await page.getByText("Install command", { exact: true }).click(); await expect(command).toContainText("--reuse-key"); await expect(command).toContainText("| sudo sh -s --"); await expect(command).not.toContainText("CERTVAULT_API_KEY="); await helper.getByLabel("Destination folder on client").fill("/srv/redeploy"); await expect(command).toContainText("--destination '/srv/redeploy'"); await helper .getByRole("checkbox", { name: "Update only", exact: true }) .check(); await expect(command).toContainText("--update-client-only"); await expect(command).not.toContainText("--certificate"); await expect(command).not.toContainText("--schedule"); await expect(command).not.toContainText("--reload-command"); await expect(command).not.toContainText("CERTVAULT_API_KEY"); await helper.getByLabel("Use sudo (disable when running as root)").uncheck(); await expect(command).toContainText("| sh -s --"); await expect(helper.getByRole("combobox")).toHaveCount(0); await helper .getByRole("checkbox", { name: "Update only", exact: true }) .uncheck(); await expect(command).toContainText("--reuse-key"); await expect(command).toContainText("--destination '/srv/redeploy'"); }); test("ACME accounts protect current account and delete old registration", async ({ page, }) => { await login(page); await navigate(page, "ACME accounts"); await expect(page.locator("article")).toHaveCount(2); await expect( page.getByText("https://acme.example.com/account/demo", { exact: true }), ).toBeVisible(); await screenshot(page, "acme-accounts.png"); await expect( page .locator("article.current") .getByRole("button", { name: "Delete", exact: true }), ).toHaveCount(0); await page.getByRole("button", { name: "Delete", exact: true }).click(); await screenshot(page, "delete-acme-account.png"); await page.getByRole("button", { name: "Cancel", exact: true }).click(); await expect(page.locator("article")).toHaveCount(2); await page.getByRole("button", { name: "Delete", exact: true }).click(); await page .getByRole("button", { name: "Delete account", exact: true }) .click(); await expect(page.locator("article")).toHaveCount(1); await page.reload(); await expect( page.getByRole("heading", { name: "acme.example.com", exact: true }), ).toBeVisible(); await expect(page.locator("article")).toHaveCount(1); }); test("history filters and pagination persist in the URL", async ({ page }) => { await login(page); await navigate(page, "history"); await expect(page.getByText("12 jobs", { exact: true })).toBeVisible(); await screenshot(page, "history.png"); await page.getByRole("combobox", { name: /^Rows/ }).selectOption("10"); await expect(page.locator("tbody tr")).toHaveCount(10); await page.getByRole("button", { name: "Next", exact: true }).click(); await expect(page.locator("tbody tr")).toHaveCount(2); await expect(page).toHaveURL(/page=2/); await page.getByText("Statuses", { exact: true }).click(); await page.getByLabel("failed", { exact: true }).check(); await page .getByRole("button", { name: "Apply filters", exact: true }) .click(); await expect(page.getByText("1 jobs", { exact: true })).toBeVisible(); await expect( page.getByText("DNS challenge timed out", { exact: true }), ).toBeVisible(); await screenshot(page, "history-filtered.png"); await page.reload(); await expect(page.locator("tbody tr")).toHaveCount(1); await page.goto("/history?status=running"); await expect( page.getByText("No history entries match these filters."), ).toBeVisible(); await screenshot(page, "history-empty.png"); }); test("audit log filters and pagination", async ({ page }) => { await login(page); await navigate(page, "audit logs"); await expect(page.getByText("12 events", { exact: true })).toBeVisible(); expect( new Set(await page.locator("tbody tr td:nth-child(3)").allTextContents()) .size, ).toBe(10); expect(await page.evaluate(() => document.documentElement.scrollHeight)).toBe( 900, ); await screenshot(page, "audit-logs.png"); const main = page.getByRole("main"); await main.evaluate((element) => element.scrollTo(0, element.scrollHeight)); await expect(page.locator(".content-footer")).toBeInViewport(); await expect(page.getByRole("navigation")).toBeInViewport(); expect(await page.evaluate(() => window.scrollY)).toBe(0); await screenshot(page, "audit-logs-bottom.png"); await page.getByRole("combobox", { name: /^Rows/ }).selectOption("10"); await expect(page.locator("tbody tr")).toHaveCount(10); await page.getByRole("button", { name: "Next", exact: true }).click(); await expect(page.locator("tbody tr")).toHaveCount(2); await page.getByText("Actors", { exact: true }).click(); await page.getByLabel("admin@example.com", { exact: true }).check(); await page .getByRole("button", { name: "Apply filters", exact: true }) .click(); await expect(page).toHaveURL(/actor=admin%40example.com/); await expect(page.locator("tbody tr")).toHaveCount(2); await page.reload(); await expect(page.getByText("2 events", { exact: true })).toBeVisible(); await page.getByText("Actions", { exact: true }).click(); await page.getByLabel("renewal.trigger", { exact: true }).check(); await page .getByRole("button", { name: "Apply filters", exact: true }) .click(); await expect(page.locator("tbody tr")).toHaveCount(1); await expect(page.locator("tbody tr")).toContainText("homelab-wildcard"); await screenshot(page, "audit-filtered.png"); await page.goto("/audit-logs?actor=nobody"); await expect( page.getByText("No audit events match these filters."), ).toBeVisible(); await screenshot(page, "audit-empty.png"); }); test("empty console", async ({ page }) => { await login(page); expect( (await page.request.post("/__test/reset?scenario=empty")).ok(), ).toBeTruthy(); await page.reload(); await expect(page.getByRole("status")).toHaveText("Operational"); await expect(page.locator("article")).toHaveCount(0); await screenshot(page, "certificates-empty.png"); await navigate(page, "ACME accounts"); await expect( page.getByText(/No ACME accounts have been created/), ).toBeVisible(); await screenshot(page, "acme-accounts-empty.png"); await navigate(page, "api keys"); await expect(page.locator("tbody tr")).toHaveCount(0); await screenshot(page, "api-keys-empty.png"); }); test("expired session leaves the console and supports signing in again", async ({ page, context, }) => { await login(page); await navigate(page, "history"); await page.clock.install(); await context.clearCookies(); await page.clock.fastForward(30_000); await expect(page.getByRole("alert")).toHaveText( "Your session expired. Sign in again.", ); await expect(page).toHaveURL("/"); await expect(page.getByRole("navigation")).toHaveCount(0); await expect(page.locator("article")).toHaveCount(0); await page .getByLabel("Break-glass administrator token") .fill("certvault-e2e-admin"); await page.getByRole("button", { name: "Sign in", exact: true }).click(); await expect(page.getByRole("navigation")).toBeVisible(); await expect(page.getByRole("status")).toHaveText("Operational"); await expect(page.getByRole("alert")).toHaveCount(0); }); test("unauthorized page requests also expire the session", async ({ page }) => { await login(page); await page.route("**/api/v1/jobs/history?*", (route) => route.fulfill({ status: 401, json: { detail: "Authentication required" } }), ); await page .getByRole("navigation") .getByRole("link", { name: "history", exact: true }) .click(); await expect(page.getByRole("alert")).toHaveText( "Your session expired. Sign in again.", ); await expect(page.getByRole("navigation")).toHaveCount(0); }); test("certificate polling reports stale data and recovers automatically", async ({ page, }) => { await page.clock.install(); await login(page); await page.route("**/api/v1/certificates", (route) => route.fulfill({ status: 503, json: { detail: "Temporarily unavailable" } }), ); await page.clock.fastForward(30_000); await expect(page.locator("main > .error")).toContainText( "displayed data may be outdated", ); await expect(page.locator("main > .error")).toContainText( "Temporarily unavailable", ); await expect(page.getByRole("status")).toHaveText("Warning"); await expect(page.locator("article")).toHaveCount(3); expect((await page.request.get("/api/v1/health")).ok()).toBeTruthy(); expect((await page.request.get("/api/v1/ready")).ok()).toBeTruthy(); await page.unroute("**/api/v1/certificates"); await page.clock.fastForward(30_000); await expect(page.locator("main > .error")).toHaveCount(0); await expect(page.getByRole("status")).toHaveText("Operational"); }); test("renewal request failures are visible and can be retried", async ({ page, }) => { await login(page); await page.route("**/api/v1/certificates/internal-gateway/renew", (route) => route.fulfill({ status: 503, json: { detail: "Renewal service unavailable" }, }), ); const card = page.getByRole("article").filter({ has: page.getByRole("heading", { name: "internal-gateway", exact: true }), }); await card.getByRole("button", { name: "Renew", exact: true }).click(); await expect(page.getByRole("alert")).toContainText( "Unable to renew internal-gateway: Error: Renewal service unavailable", ); await expect( card.getByRole("button", { name: "Renew", exact: true }), ).toBeEnabled(); await page.unroute("**/api/v1/certificates/internal-gateway/renew"); await card.getByRole("button", { name: "Renew", exact: true }).click(); await expect(page.getByRole("alert")).toHaveCount(0); await expect(card.getByText("valid", { exact: true })).toBeVisible(); await expect( card.getByRole("button", { name: "Renew", exact: true }), ).toBeEnabled(); }); test("ACME account polling failures recover independently of certificate polling", async ({ page, }) => { await page.clock.install({ time: new Date("2026-08-16T12:00:00Z") }); await login(page); await page.route("**/api/v1/acme-accounts", (route) => route.fulfill({ status: 503, json: { detail: "Account storage unavailable" }, }), ); await navigate(page, "ACME accounts"); await expect(page.locator("main > .error")).toContainText( "Unable to refresh ACME accounts", ); await expect(page.getByRole("status")).toHaveText("Warning"); const certificateRefresh = page.waitForResponse("**/api/v1/certificates"); await navigate(page, "certificates"); expect((await certificateRefresh).ok()).toBeTruthy(); await expect(page.getByRole("status")).toHaveText("Warning"); await page.unroute("**/api/v1/acme-accounts"); await navigate(page, "ACME accounts"); await expect(page.locator("main > .error")).toHaveCount(0); await expect(page.getByRole("status")).toHaveText("Operational"); }); test("API failure is visible and reload recovers", async ({ page }) => { await login(page); await page.route("**/api/v1/certificates", (route) => route.fulfill({ status: 503, json: { detail: "Temporarily unavailable" } }), ); await page.reload(); await expect(page.locator("main > .error")).toContainText( "Error: Temporarily unavailable", ); await expect(page.locator("main > .error")).toBeVisible(); await expect(page.getByRole("status")).toHaveText("Warning"); const banner = await page.locator("main > .error").boundingBox(); const stats = await page.locator(".stats").boundingBox(); expect(banner).not.toBeNull(); expect(stats).not.toBeNull(); expect(stats!.y - (banner!.y + banner!.height)).toBeGreaterThanOrEqual(24); await screenshot(page, "certificates-error.png"); await page.unroute("**/api/v1/certificates"); await page.reload(); await expect(page.locator("article")).toHaveCount(3); await expect(page.locator("main > .error")).toHaveCount(0); await page.route("**/api/v1/certificates/*/versions", (route) => route.fulfill({ status: 503, json: { detail: "Unable to load versions" } }), ); await page .getByRole("heading", { name: "homelab-wildcard", exact: true }) .click(); await expect( page.getByText("Unable to load versions", { exact: true }), ).toBeVisible(); expect(await page.locator(".modal").boundingBox()).toEqual({ x: 0, y: 0, width: 1400, height: 900, }); expect(await page.evaluate(() => document.documentElement.scrollHeight)).toBe( 900, ); await screenshot(page, "certificate-versions-error.png"); // A short viewport must keep the entire dialog reachable without exposing // or scrolling the content behind its backdrop. await page.setViewportSize({ width: 1400, height: 500 }); const dialog = page.locator(".modal section"); await dialog.evaluate((element) => element.scrollTo(0, element.scrollHeight)); await expect( page.getByText("Unable to load versions", { exact: true }), ).toBeInViewport(); expect(await page.evaluate(() => document.documentElement.scrollHeight)).toBe( 500, ); await dialog.evaluate((element) => element.scrollTo(0, 0)); await page.getByRole("button", { name: "Close", exact: true }).click(); await expect(page.locator(".modal")).toHaveCount(0); }); test("deduplicated queued renewal clears when the same job finishes", async ({ page, }) => { let state: "idle" | "queued" | "succeeded" = "idle"; await page.route("**/api/v1/certificates", async (route) => { const response = await route.fetch(); const certificates = (await response.json()) as Certificate[]; const certificate = certificates.find( (item) => item.name === "internal-gateway", )!; certificate.latest_job = { id: 777, error: "", certificate_name: certificate.name, kind: "manual", status: state === "queued" ? "queued" : "succeeded", started_at: "2026-08-16T12:00:00Z", ...(state === "queued" ? {} : { finished_at: "2026-08-16T12:01:00Z" }), }; await route.fulfill({ response, json: certificates }); }); await page.route( "**/api/v1/certificates/internal-gateway/renew", async (route) => { state = "queued"; await route.fulfill({ status: 202, json: { job_id: 777, status: "queued" }, }); }, ); await login(page); const card = page.getByRole("article").filter({ has: page.getByRole("heading", { name: "internal-gateway", exact: true }), }); await card.getByRole("button", { name: "Renew", exact: true }).click(); await expect(card.locator(".status-badge-running")).toHaveText("Queued"); await expect( card.getByRole("button", { name: "Queued", exact: true }), ).toBeDisabled(); state = "succeeded"; await expect( card.getByRole("button", { name: "Renew", exact: true }), ).toBeEnabled(); await expect(card.getByText("Queued", { exact: true })).toHaveCount(0); }); test("idle polling pauses off-page and does not overlap slow requests", async ({ page, }) => { await page.clock.install(); await login(page); let calls = 0; let release: (() => void) | undefined; await page.route("**/api/v1/certificates", async (route) => { calls++; if (calls === 1) await new Promise((resolve) => { release = resolve; }); await route.continue(); }); expect(calls).toBe(0); await page.clock.fastForward(2_000); expect(calls).toBe(0); await page.clock.fastForward(28_000); await expect.poll(() => calls).toBe(1); await page.clock.fastForward(10_000); expect(calls).toBe(1); release!(); await expect(page.getByRole("status")).toHaveText("Operational"); await navigate(page, "history"); const previous = calls; await page.clock.fastForward(60_000); expect(calls).toBe(previous); await navigate(page, "certificates"); await expect.poll(() => calls).toBe(previous + 1); }); test("superseded certificate response cannot overwrite mutation refresh", async ({ page, }) => { await page.clock.install(); await login(page); let calls = 0; let release: (() => void) | undefined; await page.route("**/api/v1/certificates", async (route) => { calls++; const response = await route.fetch(); const certificates = (await response.json()) as Certificate[]; if (calls === 1) { certificates[0].domains = ["stale.example.com"]; if (certificates[0].current_version) certificates[0].current_version.domains = ["stale.example.com"]; await new Promise((resolve) => { release = resolve; }); } await route.fulfill({ response, json: certificates }); }); await page.clock.fastForward(30_000); await expect.poll(() => calls).toBe(1); await page .getByRole("article") .filter({ has: page.getByRole("heading", { name: "internal-gateway", exact: true }), }) .getByRole("button", { name: "Renew", exact: true }) .click(); await expect.poll(() => calls).toBeGreaterThan(1); release!(); await expect(page.getByText("stale.example.com")).toHaveCount(0); await expect(page.getByRole("status")).toHaveText("Operational"); }); test("hidden console pauses polling and refreshes immediately on return", async ({ page, }) => { await page.clock.install(); await login(page); let calls = 0; await page.route("**/api/v1/certificates", async (route) => { calls++; await route.continue(); }); expect(calls).toBe(0); await page.evaluate(() => { Object.defineProperty(document, "hidden", { configurable: true, value: true, }); document.dispatchEvent(new Event("visibilitychange")); }); await page.clock.fastForward(60_000); expect(calls).toBe(0); await page.evaluate(() => { Object.defineProperty(document, "hidden", { configurable: true, value: false, }); document.dispatchEvent(new Event("visibilitychange")); }); await expect.poll(() => calls).toBe(1); });