import { test as base, expect } from "@playwright/test"; import { login } from "./helpers"; const API = process.env.API_URL || "http://localhost:13490"; const UID = Date.now().toString(36); const EDITOR_USER = `guieditor-${UID}`; const EDITOR_PASS = "EditorPass1"; const USER_USER = `guiuser-${UID}`; const USER_PASS = "UserPass1"; /** Auth header only (GET, DELETE). */ function authOnly(token: string): Record { return { Authorization: `Bearer ${token}` }; } /** Auth + JSON content-type (POST, PUT). */ function authJson(token: string): Record { return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; } async function getAdminToken(): Promise { const res = await fetch(`${API}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "admin", password: "admin" }), }); const data = await res.json(); return data.token; } /** * Create a user with a given role and clear mustChangePassword * so the browser login redirects to "/" instead of "/change-password". */ async function createReadyUser( adminToken: string, username: string, password: string, role: string, ): Promise { const createRes = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(adminToken), body: JSON.stringify({ username, password, role }), }); if (createRes.status !== 201 && createRes.status !== 409) { throw new Error(`Failed to create user ${username}: ${createRes.status}`); } // Login to get token, then change password to clear mustChangePassword const loginRes = await fetch(`${API}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); if (!loginRes.ok) throw new Error(`Login failed for ${username}: ${loginRes.status}`); const loginData = await loginRes.json(); await fetch(`${API}/api/auth/change-password`, { method: "POST", headers: authJson(loginData.token), body: JSON.stringify({ currentPassword: password, newPassword: password }), }); // Re-login and dismiss analytics consent const reLogin = await fetch(`${API}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); const reLoginData = await reLogin.json(); await fetch(`${API}/api/v1/user/analytics`, { method: "PUT", headers: authJson(reLoginData.token), body: JSON.stringify({ enabled: false }), }); } /** Delete a user by username if it exists. */ async function deleteUser(adminToken: string, username: string): Promise { const listRes = await fetch(`${API}/api/auth/users`, { headers: authOnly(adminToken), }); if (!listRes.ok) return; const { users } = await listRes.json(); const found = users.find((u: { username: string }) => u.username === username); if (found) { await fetch(`${API}/api/auth/users/${found.id}`, { method: "DELETE", headers: authOnly(adminToken), }); } } // --------------------------------------------------------------------------- // RBAC role visibility verification for settings dialog tabs // --------------------------------------------------------------------------- // The NAV_ITEMS and their required permissions from the source: // general - none // system - settings:write // security - none // people - users:manage // teams - teams:manage // roles - users:manage // audit-log - audit:read // api-keys - none // ai-features - settings:write // tools - none // analytics - none (Product Analytics) // about - none base.describe("RBAC Settings Visibility - Admin", () => { base.use({ storageState: "test-results/.auth/user.json" }); base.test("admin sees all settings tabs including admin-only ones", async ({ page }) => { await page.goto("/"); await page.locator("aside").getByText("Settings").click(); // Tabs visible to all roles await expect(page.getByRole("button", { name: /general/i })).toBeVisible(); await expect(page.getByRole("button", { name: /security/i })).toBeVisible(); await expect(page.getByRole("button", { name: /api keys/i })).toBeVisible(); await expect(page.getByRole("button", { name: /tools/i })).toBeVisible(); await expect(page.getByRole("button", { name: /product analytics/i })).toBeVisible(); await expect(page.getByRole("button", { name: /about/i })).toBeVisible(); // Admin-only tabs (require settings:write, users:manage, teams:manage, audit:read) await expect(page.getByRole("button", { name: /system settings/i })).toBeVisible(); await expect(page.getByRole("button", { name: /people/i })).toBeVisible(); await expect(page.getByRole("button", { name: /teams/i })).toBeVisible(); await expect(page.getByRole("button", { name: /^roles$/i })).toBeVisible(); await expect(page.getByRole("button", { name: /audit log/i })).toBeVisible(); await expect(page.getByRole("button", { name: /ai features/i })).toBeVisible(); }); }); base.describe("RBAC Settings Visibility - Editor", () => { let adminToken: string; base.beforeAll(async () => { adminToken = await getAdminToken(); await createReadyUser(adminToken, EDITOR_USER, EDITOR_PASS, "editor"); }); base.afterAll(async () => { await deleteUser(adminToken, EDITOR_USER); }); base.test( "editor sees general, security, api-keys, tools, analytics, about", async ({ page }) => { await login(page, EDITOR_USER, EDITOR_PASS); await page.locator("aside").getByText("Settings").click(); // Should see these tabs await expect(page.getByRole("button", { name: /general/i })).toBeVisible(); await expect(page.getByRole("button", { name: /security/i })).toBeVisible(); await expect(page.getByRole("button", { name: /api keys/i })).toBeVisible(); await expect(page.getByRole("button", { name: /tools/i })).toBeVisible(); await expect(page.getByRole("button", { name: /product analytics/i })).toBeVisible(); await expect(page.getByRole("button", { name: /about/i })).toBeVisible(); }, ); base.test( "editor does NOT see system settings, people, teams, roles, audit log, ai features", async ({ page }) => { await login(page, EDITOR_USER, EDITOR_PASS); await page.locator("aside").getByText("Settings").click(); // Wait for dialog to fully render await expect(page.getByRole("button", { name: /general/i })).toBeVisible(); // Should NOT see admin-only tabs await expect(page.getByRole("button", { name: /system settings/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /people/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /teams/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /^roles$/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /audit log/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /ai features/i })).not.toBeVisible(); }, ); }); base.describe("RBAC Settings Visibility - User", () => { let adminToken: string; base.beforeAll(async () => { adminToken = await getAdminToken(); await createReadyUser(adminToken, USER_USER, USER_PASS, "user"); }); base.afterAll(async () => { await deleteUser(adminToken, USER_USER); }); base.test("user sees general, security, api-keys, tools, analytics, about", async ({ page }) => { await login(page, USER_USER, USER_PASS); await page.locator("aside").getByText("Settings").click(); await expect(page.getByRole("button", { name: /general/i })).toBeVisible(); await expect(page.getByRole("button", { name: /security/i })).toBeVisible(); await expect(page.getByRole("button", { name: /api keys/i })).toBeVisible(); await expect(page.getByRole("button", { name: /tools/i })).toBeVisible(); await expect(page.getByRole("button", { name: /product analytics/i })).toBeVisible(); await expect(page.getByRole("button", { name: /about/i })).toBeVisible(); }); base.test( "user does NOT see system settings, people, teams, roles, audit log, ai features", async ({ page }) => { await login(page, USER_USER, USER_PASS); await page.locator("aside").getByText("Settings").click(); // Wait for dialog to fully render await expect(page.getByRole("button", { name: /general/i })).toBeVisible(); await expect(page.getByRole("button", { name: /system settings/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /people/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /teams/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /^roles$/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /audit log/i })).not.toBeVisible(); await expect(page.getByRole("button", { name: /ai features/i })).not.toBeVisible(); }, ); });