e2e/ui.spec.ts
Rama master · réplica de lectura del repositorio original.
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<void>((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<void>((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);
});