Files
ITWorx_MCP_Hub-Public/apps/web/tests/e2e/rbac.spec.ts
T

89 lines
4.6 KiB
TypeScript

import { expect, loginAs, test } from "./fixtures";
test.describe("role-based authorization, driven end to end from the browser", () => {
test("a viewer never sees the client-create form; an admin does", async ({ page }) => {
await loginAs(page, "hub-viewer");
await page.goto("/clients");
await expect(page.getByRole("button", { name: "Create pending client" })).toHaveCount(0);
await expect(page.getByText("Client creation requires the admin role.")).toBeVisible();
await loginAs(page, "hub-admin");
await page.goto("/clients");
await expect(page.getByRole("button", { name: "Create pending client" })).toBeVisible();
});
test("client.create is still server-enforced even if the hidden form were bypassed", async ({ page, request }) => {
// Proves the security property independent of the UI: the hidden
// button is a usability affordance, not the authorization boundary.
await loginAs(page, "hub-viewer");
const cookies = await page.context().cookies();
const sessionCookie = cookies.find((cookie) => cookie.name.toLowerCase().includes("session"));
if (!sessionCookie) throw new Error("expected a session cookie to be set after login");
const sessionResponse = await request.get("/auth/session", { headers: { Cookie: `${sessionCookie.name}=${sessionCookie.value}` } });
const { csrf_token: csrfToken } = await sessionResponse.json();
const response = await request.post("/api/client-management", {
headers: { Cookie: `${sessionCookie.name}=${sessionCookie.value}`, "X-CSRF-Token": csrfToken },
data: { name: "e2e-bypass-attempt", project: "ludarium", environment: "prod", kind: "native-2026" },
});
expect(response.status()).toBe(403);
});
test("an auditor can read the audit log; a viewer never sees the Audit nav link at all", async ({ page }) => {
await loginAs(page, "hub-viewer");
await page.goto("/");
await expect(page.getByRole("link", { name: "Audit" })).toHaveCount(0);
await loginAs(page, "hub-auditor");
await page.goto("/");
await expect(page.getByRole("link", { name: "Audit" })).toBeVisible();
await page.goto("/audit");
await expect(page.getByRole("alert")).toHaveCount(0);
await expect(page.getByText(/total · server paginated/)).toBeVisible();
});
test("a viewer cannot select the audit section of a project workspace", async ({ page }) => {
await loginAs(page, "hub-viewer");
await page.goto("/projects/ludarium?tab=Audit");
await expect(page.getByRole("link", { name: "Audit" })).toHaveCount(0);
await expect(page.getByRole("link", { name: "Summary" })).toHaveAttribute("aria-current", "page");
});
test("an operator sees state-appropriate client actions; a viewer does not", async ({ page }) => {
await loginAs(page, "hub-admin");
await page.goto("/clients");
await page.getByLabel("Name").fill("e2e-operator-target");
await page.getByRole("button", { name: "Create pending client" }).click();
await expect(page.getByText(/Client created: e2e-operator-target/)).toBeVisible();
await loginAs(page, "hub-viewer");
await page.goto("/clients");
const viewerRow = page.getByRole("row", { name: /e2e-operator-target/ });
await expect(viewerRow).toBeVisible();
await expect(viewerRow.getByRole("button", { name: "Disable" })).toHaveCount(0);
await loginAs(page, "hub-operator");
await page.goto("/clients");
const operatorRow = page.getByRole("row", { name: /e2e-operator-target/ });
await expect(operatorRow.getByRole("button", { name: "Disable" })).toBeDisabled();
await operatorRow.getByRole("button", { name: "Revoke" }).click();
// The confirmation modal's title is dynamic ("Revoke e2e-operator-target?"),
// which makes an exact accessible-name match on the confirm button awkward
// to pin reliably against the row-scoped locator above. This is the one
// deliberate exception to this file's role/text-based query convention,
// justified by the dynamic title: DataTable.tsx's modal confirm button
// carries data-testid="confirm-action" specifically for this case.
await page.getByTestId("confirm-action").click();
await expect(page.getByText(/completed\./)).toBeVisible();
});
test("tool readiness is hidden below operator and visible to an operator", async ({ page }) => {
await loginAs(page, "hub-viewer");
await page.goto("/tools");
await expect(page.getByRole("button", { name: "Readiness" })).toHaveCount(0);
await loginAs(page, "hub-operator");
await page.goto("/tools");
await expect(page.getByRole("button", { name: "Readiness" }).first()).toBeVisible();
});
});