/** * RBAC / permissions tests — dev system * * Validates that role-based access control is enforced correctly * against the running dev server with real seed data. * * All tests reuse pre-authenticated storage states from global setup * so they don't trigger the auth rate limiter. * * Role hierarchy: * ADMIN — full access including all /admin/* routes * MANAGER — can view allocations, cannot access /admin/users etc. * VIEWER — limited read-only; allocations are forbidden (TRPC FORBIDDEN) */ import { expect, test } from "@playwright/test"; import { STORAGE_STATE } from "../../playwright.dev.config.js"; // --------------------------------------------------------------------------- // Admin routes — admin session // --------------------------------------------------------------------------- test.describe("RBAC — admin routes (admin session)", () => { test.use({ storageState: STORAGE_STATE.admin }); test("admin can access /admin/users and sees user rows", async ({ page }) => { await page.goto("/admin/users"); await page.waitForLoadState("networkidle"); await expect(page.locator("table")).toBeVisible({ timeout: 10000 }); // Seed users have planarchy.dev or capakraken.dev email domains await expect( page.locator("text=/planarchy\\.dev|capakraken\\.dev/").first(), ).toBeVisible({ timeout: 10000 }); }); test("admin can access /admin/system-roles without errors", async ({ page }) => { await page.goto("/admin/system-roles"); await page.waitForLoadState("networkidle"); await expect(page.locator("h1,h2").first()).toBeVisible({ timeout: 10000 }); }); test("admin can access /admin/blueprints without errors", async ({ page }) => { await page.goto("/admin/blueprints"); await page.waitForLoadState("networkidle"); await expect(page.locator("h1,h2,h3").first()).toBeVisible({ timeout: 10000 }); }); }); // --------------------------------------------------------------------------- // Admin routes — non-admin sessions should be blocked // --------------------------------------------------------------------------- test.describe("RBAC — /admin/users blocked for manager", () => { test.use({ storageState: STORAGE_STATE.manager }); test("manager is redirected or sees an error on /admin/users", async ({ page }) => { await page.goto("/admin/users"); await page.waitForLoadState("networkidle"); const isRedirected = !page.url().includes("/admin/users"); const hasForbiddenText = await page .locator("text=/forbidden|permission|access denied|not authorized|unauthorized/i") .first() .isVisible() .catch(() => false); expect(isRedirected || hasForbiddenText).toBe(true); }); }); test.describe("RBAC — /admin/users blocked for viewer", () => { test.use({ storageState: STORAGE_STATE.viewer }); test("viewer is redirected or sees an error on /admin/users", async ({ page }) => { await page.goto("/admin/users"); await page.waitForLoadState("networkidle"); const isRedirected = !page.url().includes("/admin/users"); const hasForbiddenText = await page .locator("text=/forbidden|permission|access denied|not authorized|unauthorized/i") .first() .isVisible() .catch(() => false); expect(isRedirected || hasForbiddenText).toBe(true); }); }); // --------------------------------------------------------------------------- // Allocations access by role // --------------------------------------------------------------------------- test.describe("RBAC — allocations permitted for admin", () => { test.use({ storageState: STORAGE_STATE.admin }); test("admin can view /allocations without permission errors", async ({ page }) => { await page.goto("/allocations"); await page.waitForLoadState("networkidle"); await expect( page.locator("text=/do not have permission to view allocations/i"), ).toHaveCount(0, { timeout: 8000 }); }); }); test.describe("RBAC — allocations permitted for manager", () => { test.use({ storageState: STORAGE_STATE.manager }); test("manager can view /allocations without permission errors", async ({ page }) => { await page.goto("/allocations"); await page.waitForLoadState("networkidle"); await expect( page.locator("text=/do not have permission to view allocations/i"), ).toHaveCount(0, { timeout: 8000 }); }); }); test.describe("RBAC — allocations forbidden for viewer", () => { test.use({ storageState: STORAGE_STATE.viewer }); test("viewer sees a permission error on /allocations", async ({ page }) => { await page.goto("/allocations"); await page.waitForLoadState("networkidle"); // AllocationsClient renders the message in both the page subtitle and the card body; // use .first() to avoid strict-mode violation. await expect( page.locator("text=/permission|forbidden|not have permission/i").first(), ).toBeVisible({ timeout: 10000 }); }); }); // --------------------------------------------------------------------------- // Dashboard loads for every role // --------------------------------------------------------------------------- test.describe("RBAC — dashboard (admin)", () => { test.use({ storageState: STORAGE_STATE.admin }); test("admin dashboard loads without errors", async ({ page }) => { await page.goto("/dashboard"); await page.waitForLoadState("networkidle"); await expect(page).not.toHaveURL(/\/auth\/signin/, { timeout: 5000 }); await expect(page.locator("text=/500|Internal Server Error/i")).toHaveCount(0); }); }); test.describe("RBAC — dashboard (manager)", () => { test.use({ storageState: STORAGE_STATE.manager }); test("manager dashboard loads without errors", async ({ page }) => { await page.goto("/dashboard"); await page.waitForLoadState("networkidle"); await expect(page).not.toHaveURL(/\/auth\/signin/, { timeout: 5000 }); await expect(page.locator("text=/500|Internal Server Error/i")).toHaveCount(0); }); }); test.describe("RBAC — dashboard (viewer)", () => { test.use({ storageState: STORAGE_STATE.viewer }); test("viewer dashboard loads without errors", async ({ page }) => { await page.goto("/dashboard"); await page.waitForLoadState("networkidle"); await expect(page).not.toHaveURL(/\/auth\/signin/, { timeout: 5000 }); await expect(page.locator("text=/500|Internal Server Error/i")).toHaveCount(0); }); });