117 lines
4.3 KiB
TypeScript
117 lines
4.3 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { OPERATOR, VIEWER } from "./credentials";
|
|
import { collectBrowserIssues, login } from "./helpers";
|
|
|
|
test.describe("authentication and authorization", () => {
|
|
test("rejects invalid credentials without creating a session", async ({ page }) => {
|
|
await page.goto("/login.html");
|
|
await page.locator("#user").fill("not-a-real-user");
|
|
await page.locator("#pass").fill("not-the-password");
|
|
await page.locator("#login-form button[type='submit']").click();
|
|
|
|
await expect(page.locator("#error")).toContainText("Invalid credentials");
|
|
await expect(page).toHaveURL(/\/login\.html$/);
|
|
const me = await page.request.get("/api/auth/me");
|
|
expect(me.status()).toBe(401);
|
|
});
|
|
|
|
test("serves login instead of protected content when unauthenticated", async ({ page }) => {
|
|
const response = await page.goto("/settings");
|
|
expect(response?.status()).toBe(200);
|
|
await expect(page.locator("#login-form")).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "Settings" })).toHaveCount(0);
|
|
});
|
|
|
|
test("logout revokes the browser session", async ({ page }) => {
|
|
await login(page);
|
|
page.once("dialog", (dialog) => dialog.accept());
|
|
await page.locator("#user-actions-btn").click();
|
|
await Promise.all([
|
|
page.waitForURL((url) => url.pathname === "/"),
|
|
page.locator("#logout-btn").click(),
|
|
]);
|
|
await expect(page.locator("#login-form")).toBeVisible();
|
|
|
|
const me = await page.request.get("/api/auth/me");
|
|
expect(me.status()).toBe(401);
|
|
await page.goto("/users");
|
|
await expect(page.locator("#login-form")).toBeVisible();
|
|
});
|
|
|
|
test("viewer navigation and direct routes enforce read-only access", async ({ page }) => {
|
|
await login(page, VIEWER);
|
|
await expect(page.locator("#role-badge")).toContainText("Viewer");
|
|
|
|
for (const id of [
|
|
"users-link",
|
|
"build-link",
|
|
"plugins-link",
|
|
"scripts-link",
|
|
"file-share-link",
|
|
"notifications-link",
|
|
"enrollment-link",
|
|
]) {
|
|
await expect(page.locator(`#${id}`)).toBeHidden();
|
|
}
|
|
|
|
const denied = await page.goto("/users");
|
|
expect(denied?.status()).toBe(403);
|
|
await expect(page.locator("body")).toContainText("Admin access required");
|
|
|
|
const allowed = await page.goto("/metrics");
|
|
expect(allowed?.status()).toBe(200);
|
|
await expect(page.getByRole("heading", { name: "Metrics Dashboard" })).toBeVisible();
|
|
});
|
|
|
|
test("operator can use operational pages but not admin pages", async ({ page }) => {
|
|
await login(page, OPERATOR);
|
|
await expect(page.locator("#role-badge")).toContainText("Operator");
|
|
await expect(page.locator("#build-link")).not.toHaveClass(/hidden/);
|
|
await expect(page.locator("#users-link")).toBeHidden();
|
|
|
|
const build = await page.goto("/build");
|
|
expect(build?.status()).toBe(200);
|
|
await expect(page.locator("#build-form")).toBeVisible();
|
|
|
|
const users = await page.goto("/users");
|
|
expect(users?.status()).toBe(403);
|
|
});
|
|
});
|
|
|
|
test("admin page sweep loads without browser errors", async ({ page }) => {
|
|
// This test validates each page's static/HTTP initialization. Opening two
|
|
// long-lived sockets for every rapid hard navigation would only exercise the
|
|
// server's WebSocket rate limiter; socket lifecycle is covered separately.
|
|
await page.routeWebSocket(/.*/, (socket) => socket.close());
|
|
const issues = collectBrowserIssues(page);
|
|
await login(page);
|
|
|
|
const routes = [
|
|
["/", "#grid"],
|
|
["/metrics", "h1"],
|
|
["/graph", "h1"],
|
|
["/settings", "#prefs-form"],
|
|
["/logs", "#page-info"],
|
|
["/notifications", "#notification-table"],
|
|
["/users", "#users-table-body"],
|
|
["/build", "#build-form"],
|
|
["/sol-publish", "#rpc-endpoint-list"],
|
|
["/plugins", "#plugin-list"],
|
|
["/scripts", "#saved-scripts-list"],
|
|
["/deploy", "#client-list"],
|
|
["/socks5-manager", "body"],
|
|
["/file-share", "#upload-form"],
|
|
["/purgatory", "#enrollment-table"],
|
|
] as const;
|
|
|
|
for (const [path, selector] of routes) {
|
|
const response = await page.goto(path);
|
|
expect(response?.status(), `${path} should return 200`).toBe(200);
|
|
await expect(page.locator("#top-nav"), `${path} should mount navigation`).toBeVisible();
|
|
await expect(page.locator(selector).first(), `${path} should render ${selector}`).toBeVisible();
|
|
await page.waitForTimeout(250);
|
|
}
|
|
|
|
expect(issues).toEqual([]);
|
|
});
|