import { test, expect } from "@playwright/test"; /** * RCA-14: Login page + first-run setup wizard * * Covers: * - Login / logout flow * - Form validation and error display * - Route guard: unauthenticated redirect to /login * - Route guard: authenticated redirect away from /login → dashboard * - First-run setup wizard (GET /admin/setup/status → redirect to /setup) */ test.describe("Login page", () => { test("shows username and password fields", async ({ page }) => { await page.goto("/login"); await expect(page.getByLabel(/username/i)).toBeVisible(); await expect(page.getByLabel(/password/i)).toBeVisible(); await expect( page.getByRole("button", { name: /log in|sign in/i }), ).toBeVisible(); }); test("disables submit while fields are empty", async ({ page }) => { await page.goto("/login"); const btn = page.getByRole("button", { name: /log in|sign in/i }); // Should either be disabled or clicking it shows a validation error const isEmpty = (await btn.getAttribute("disabled")) !== null; if (!isEmpty) { await btn.click(); // At least one validation error should appear const hasError = await page .getByRole("alert") .or(page.locator("[class*=error]")) .or(page.locator("[class*=invalid]")) .count(); expect(hasError).toBeGreaterThan(0); } }); test("shows error on invalid credentials", async ({ page }) => { await page.route("**/admin/auth/login", (route) => route.fulfill({ status: 401, contentType: "application/json", body: JSON.stringify({ error: "Invalid credentials" }), }), ); await page.goto("/login"); await page.getByLabel(/username/i).fill("wrong"); await page.getByLabel(/password/i).fill("wrong"); await page.getByRole("button", { name: /log in|sign in/i }).click(); await expect(page.getByText(/invalid credentials|wrong|incorrect/i)).toBeVisible(); // Should remain on /login await expect(page).toHaveURL(/\/login/); }); test("submits form on Enter key", async ({ page }) => { await page.route("**/admin/auth/login", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ token: "test-jwt", expiresAt: "2099-01-01" }), }), ); await page.goto("/login"); await page.getByLabel(/username/i).fill("admin"); await page.getByLabel(/password/i).fill("password"); await page.getByLabel(/password/i).press("Enter"); await expect(page).toHaveURL(/\/dashboard/); }); test("redirects to dashboard on successful login", async ({ page }) => { await page.route("**/admin/auth/login", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ token: "test-jwt", expiresAt: "2099-01-01" }), }), ); await page.goto("/login"); await page.getByLabel(/username/i).fill("admin"); await page.getByLabel(/password/i).fill("password"); await page.getByRole("button", { name: /log in|sign in/i }).click(); await expect(page).toHaveURL(/\/dashboard/); }); }); test.describe("Route guards", () => { test("unauthenticated user is redirected to /login from protected routes", async ({ page, }) => { for (const route of ["/dashboard", "/cookies", "/devices", "/settings"]) { await page.goto(route); await expect(page).toHaveURL(/\/login/); } }); test("authenticated user visiting /login is redirected to /dashboard", async ({ page, }) => { // Seed a token so the app thinks we're logged in await page.goto("/login"); await page.evaluate(() => localStorage.setItem("cb_admin_token", "fake-jwt")); // Mock /admin/auth/me to return a valid user await page.route("**/admin/auth/me", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ username: "admin" }), }), ); await page.goto("/login"); await expect(page).toHaveURL(/\/dashboard/); }); }); test.describe("First-run setup wizard", () => { test("redirects to /setup when not yet initialised", async ({ page }) => { await page.route("**/admin/setup/status", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ initialised: false }), }), ); await page.goto("/"); await expect(page).toHaveURL(/\/setup/); }); test("wizard has 4 steps and can be completed", async ({ page }) => { await page.route("**/admin/setup/status", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ initialised: false }), }), ); await page.route("**/admin/setup/init", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true }), }), ); await page.goto("/setup"); // Step 1: Welcome await expect(page.getByText(/welcome|cookiebridge/i)).toBeVisible(); await page.getByRole("button", { name: /next|continue/i }).click(); // Step 2: Create admin account await page.getByLabel(/username/i).fill("admin"); await page.getByLabel(/^password$/i).fill("Secure123!"); await page.getByLabel(/confirm password/i).fill("Secure123!"); await page.getByRole("button", { name: /next|continue/i }).click(); // Step 3: Basic config (port, HTTPS) await expect( page.getByLabel(/port/i).or(page.getByText(/port|https/i)), ).toBeVisible(); await page.getByRole("button", { name: /next|continue/i }).click(); // Step 4: Completion await expect(page.getByText(/done|complete|finish/i)).toBeVisible(); await page.getByRole("button", { name: /go to login|finish/i }).click(); await expect(page).toHaveURL(/\/login/); }); test("password mismatch in setup shows error", async ({ page }) => { await page.route("**/admin/setup/status", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ initialised: false }), }), ); await page.goto("/setup"); await page.getByRole("button", { name: /next|continue/i }).click(); await page.getByLabel(/username/i).fill("admin"); await page.getByLabel(/^password$/i).fill("Secure123!"); await page.getByLabel(/confirm password/i).fill("Mismatch999!"); await page.getByRole("button", { name: /next|continue/i }).click(); await expect(page.getByText(/do not match|mismatch|must match/i)).toBeVisible(); }); }); test.describe("Logout", () => { test("logout clears session and redirects to /login", async ({ page }) => { await page.route("**/admin/auth/login", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ token: "test-jwt", expiresAt: "2099-01-01" }), }), ); await page.route("**/admin/auth/logout", (route) => route.fulfill({ status: 204 }), ); await page.route("**/admin/dashboard", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ devices: {}, cookies: {}, syncCount: 0, uptimeSeconds: 0 }), }), ); // Log in first await page.goto("/login"); await page.getByLabel(/username/i).fill("admin"); await page.getByLabel(/password/i).fill("password"); await page.getByRole("button", { name: /log in|sign in/i }).click(); await expect(page).toHaveURL(/\/dashboard/); // Log out const logoutBtn = page .getByRole("button", { name: /log ?out|sign ?out/i }) .or(page.getByRole("link", { name: /log ?out|sign ?out/i })); await logoutBtn.click(); await expect(page).toHaveURL(/\/login/); // Token should be gone const token = await page.evaluate(() => localStorage.getItem("cb_admin_token")); expect(token).toBeNull(); }); });