import { test as base, expect } from "@playwright/test"; import { test as uiTest } from "./helpers"; const API = "http://localhost:13490"; async function getAuthToken(): 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; } /** Auth header only — for GET and DELETE (no body). */ function authOnly(token: string): Record { return { Authorization: `Bearer ${token}` }; } /** Auth + JSON content-type — for POST/PUT with body. */ function authJson(token: string): Record { return { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }; } /** Delete all non-admin users (cleanup helper). */ async function cleanupTestUsers(token: string) { const listRes = await fetch(`${API}/api/auth/users`, { headers: authOnly(token), }); if (!listRes.ok) return; const { users } = await listRes.json(); for (const u of users) { if (u.username === "admin") continue; await fetch(`${API}/api/auth/users/${u.id}`, { method: "DELETE", headers: authOnly(token), }); } } base.describe("People Management — API", () => { let token: string; base.beforeAll(async () => { token = await getAuthToken(); }); base.beforeEach(async () => { await cleanupTestUsers(token); }); base.afterAll(async () => { await cleanupTestUsers(token); }); // ── GET /api/auth/users ─────────────────────────────────────────── base.test("GET /api/auth/users returns users with team and maxUsers", async () => { const res = await fetch(`${API}/api/auth/users`, { headers: authOnly(token), }); expect(res.status).toBe(200); const data = await res.json(); expect(Array.isArray(data.users)).toBe(true); expect(data.maxUsers).toBe(5); expect(data.users[0]).toHaveProperty("team"); }); // ── POST /api/auth/register ─────────────────────────────────────── base.test("register a new user with team", async () => { const res = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "testuser1", password: "Test1234", role: "user", team: "Engineering", }), }); expect(res.status).toBe(201); const data = await res.json(); expect(data.username).toBe("testuser1"); expect(data.role).toBe("user"); expect(data.team).toBe("Engineering"); }); base.test("register defaults team to Default", async () => { const res = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "teamdefault", password: "Test1234", role: "user", }), }); expect(res.status).toBe(201); const data = await res.json(); expect(data.team).toBe("Default"); }); base.test("register enforces user limit of 5", async () => { // admin is user 1, create 4 more to hit the limit for (let i = 1; i <= 4; i++) { const res = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: `limituser${i}`, password: "Test1234", role: "user", }), }); expect(res.status).toBe(201); } // 6th user should be rejected const res = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "limituser5", password: "Test1234", role: "user", }), }); expect(res.status).toBe(403); const data = await res.json(); expect(data.code).toBe("USER_LIMIT_REACHED"); }); base.test("register rejects duplicate username", async () => { await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "dupuser", password: "Test1234" }), }); const res = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "dupuser", password: "Test1234" }), }); expect(res.status).toBe(409); }); base.test("register rejects weak password", async () => { const res = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "weakpw", password: "weak" }), }); expect(res.status).toBe(400); }); // ── PUT /api/auth/users/:id ─────────────────────────────────────── base.test("update user role and team", async () => { const createRes = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "editable", password: "Test1234", role: "user" }), }); const { id } = await createRes.json(); const updateRes = await fetch(`${API}/api/auth/users/${id}`, { method: "PUT", headers: authJson(token), body: JSON.stringify({ role: "admin", team: "Design" }), }); expect(updateRes.status).toBe(200); // Verify const listRes = await fetch(`${API}/api/auth/users`, { headers: authOnly(token), }); const { users } = await listRes.json(); const updated = users.find((u: { id: string }) => u.id === id); expect(updated.role).toBe("admin"); expect(updated.team).toBe("Design"); }); base.test("cannot demote yourself", async () => { const sessionRes = await fetch(`${API}/api/auth/session`, { headers: authOnly(token), }); const { user } = await sessionRes.json(); const res = await fetch(`${API}/api/auth/users/${user.id}`, { method: "PUT", headers: authJson(token), body: JSON.stringify({ role: "user" }), }); expect(res.status).toBe(400); const data = await res.json(); expect(data.code).toBe("SELF_DEMOTE"); }); base.test("update nonexistent user returns 404", async () => { const res = await fetch(`${API}/api/auth/users/nonexistent-id`, { method: "PUT", headers: authJson(token), body: JSON.stringify({ role: "admin" }), }); expect(res.status).toBe(404); }); // ── POST /api/auth/users/:id/reset-password ────────────────────── base.test("admin can reset another user password", async () => { const createRes = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "resetme", password: "Test1234" }), }); const { id } = await createRes.json(); const res = await fetch(`${API}/api/auth/users/${id}/reset-password`, { method: "POST", headers: authJson(token), body: JSON.stringify({ newPassword: "NewPass123" }), }); expect(res.status).toBe(200); // Verify new password works const loginRes = await fetch(`${API}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "resetme", password: "NewPass123" }), }); expect(loginRes.status).toBe(200); }); base.test("reset password rejects weak password", async () => { const createRes = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "weakreset", password: "Test1234" }), }); const { id } = await createRes.json(); const res = await fetch(`${API}/api/auth/users/${id}/reset-password`, { method: "POST", headers: authJson(token), body: JSON.stringify({ newPassword: "weak" }), }); expect(res.status).toBe(400); }); base.test("reset password for nonexistent user returns 404", async () => { const res = await fetch(`${API}/api/auth/users/nonexistent-id/reset-password`, { method: "POST", headers: authJson(token), body: JSON.stringify({ newPassword: "Test1234" }), }); expect(res.status).toBe(404); }); // ── DELETE /api/auth/users/:id ──────────────────────────────────── base.test("delete a user", async () => { const createRes = await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "deleteme", password: "Test1234" }), }); const { id } = await createRes.json(); const res = await fetch(`${API}/api/auth/users/${id}`, { method: "DELETE", headers: authOnly(token), }); expect(res.status).toBe(200); // Verify deleted const listRes = await fetch(`${API}/api/auth/users`, { headers: authOnly(token), }); const { users } = await listRes.json(); expect(users.find((u: { id: string }) => u.id === id)).toBeUndefined(); }); base.test("cannot delete yourself", async () => { const sessionRes = await fetch(`${API}/api/auth/session`, { headers: authOnly(token), }); const { user } = await sessionRes.json(); const res = await fetch(`${API}/api/auth/users/${user.id}`, { method: "DELETE", headers: authOnly(token), }); expect(res.status).toBe(400); }); // ── Non-admin access ────────────────────────────────────────────── base.test("non-admin cannot access user management", async () => { await fetch(`${API}/api/auth/register`, { method: "POST", headers: authJson(token), body: JSON.stringify({ username: "regularuser", password: "Test1234", role: "user" }), }); const loginRes = await fetch(`${API}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "regularuser", password: "Test1234" }), }); const { token: userToken } = await loginRes.json(); const res = await fetch(`${API}/api/auth/users`, { headers: authOnly(userToken), }); // 403 — MUST_CHANGE_PASSWORD blocks non-auth API calls expect(res.status).toBe(403); }); }); uiTest.describe("People Management — UI", () => { uiTest("People section displays user count and table", async ({ loggedInPage: page }) => { await page.locator("aside").getByText("Settings").click(); await page.getByRole("button", { name: /people/i }).click(); await page.waitForTimeout(500); // Should show user count await expect(page.getByText(/\d+ \/ 5 users/)).toBeVisible(); // Should show table headers await expect(page.getByText("User").first()).toBeVisible(); await expect(page.getByText("Role").first()).toBeVisible(); await expect(page.getByText("Team").first()).toBeVisible(); // Should show admin user in table await expect(page.getByText("admin").first()).toBeVisible(); await expect(page.getByText("ADMIN").first()).toBeVisible(); await expect(page.getByText("Default").first()).toBeVisible(); }); uiTest("Search filters users", async ({ loggedInPage: page }) => { await page.locator("aside").getByText("Settings").click(); await page.getByRole("button", { name: /people/i }).click(); await page.waitForTimeout(500); // Search for nonexistent user await page.getByPlaceholder("Search members...").fill("zzzznonexistent"); await expect(page.getByText("No members match your search.")).toBeVisible(); // Clear search shows admin again await page.getByPlaceholder("Search members...").fill(""); await expect(page.getByText("admin").first()).toBeVisible(); }); uiTest("Add Members button is visible and interactive", async ({ loggedInPage: page }) => { await page.locator("aside").getByText("Settings").click(); await page.getByRole("button", { name: /people/i }).click(); await page.waitForTimeout(500); // The Add Members button should be visible const addBtn = page.getByRole("button", { name: /add members/i }); await expect(addBtn).toBeVisible(); // If at limit, it should be disabled; if not, clicking should show form const isDisabled = await addBtn.isDisabled(); if (!isDisabled) { await addBtn.click(); await expect(page.getByPlaceholder("Username")).toBeVisible(); await expect(page.getByPlaceholder("Password")).toBeVisible(); await expect(page.getByPlaceholder("Team")).toBeVisible(); await expect(page.getByRole("button", { name: /create/i })).toBeVisible(); await expect(page.getByRole("button", { name: /cancel/i })).toBeVisible(); } }); uiTest( "Three-dot menu shows edit, reset password, and delete options", async ({ loggedInPage: page }) => { await page.locator("aside").getByText("Settings").click(); await page.getByRole("button", { name: /people/i }).click(); await page.waitForTimeout(500); await page.getByTitle("Actions").first().click(); await expect(page.getByText("Edit Role / Team")).toBeVisible(); await expect(page.getByText("Reset Password")).toBeVisible(); await expect(page.getByText("Delete User")).toBeVisible(); }, ); });