/** * Authentication E2E Tests * * Tests for authentication features: * - Login page UI * - Dev bypass authentication * - Session persistence * - Logout * - Protected routes redirect to login * - User management (admin only) * - Security settings (passkey management) * * Runs against an isolated fixture with seeded data. */ import { test, expect } from "../fixtures"; // Regex patterns const LOGIN_URL_PATTERN = /\/login/; const ADMIN_URL_PATTERN = /\/_emdash\/admin\/?$/; const USERS_URL_PATTERN = /\/users/; const SECURITY_SETTINGS_URL_PATTERN = /\/settings\/security/; const LOGIN_OR_ADMIN_URL_PATTERN = /\/(login|admin)/; const SECURITY_MENUITEM_REGEX = /Security/i; const ADD_PASSKEY_REGEX = /Add Passkey/i; const SIGN_HEADING_REGEX = /sign/i; test.describe("Authentication", () => { test.describe("Login Page", () => { test("displays login page with passkey button", async ({ admin }) => { await admin.goto("/login"); // Should show login page await expect(admin.page.locator("h1")).toContainText("Sign in"); // Should have passkey login button await expect(admin.page.locator('button:has-text("Sign in with Passkey")')).toBeVisible(); }); test("signup link is hidden when no allowed domains", async ({ admin }) => { await admin.goto("/login"); // No allowed domains are seeded, so signup should not be visible await expect(admin.page.locator('a:has-text("Sign up")')).not.toBeVisible(); }); }); test.describe("Protected Routes", () => { test("unauthenticated access redirects to login", async ({ admin }) => { // Clear cookies to ensure no session await admin.page.context().clearCookies(); // Try to access dashboard without auth await admin.goto("/"); // Should redirect to login (setup is already done via global-setup) await expect(admin.page).toHaveURL(LOGIN_URL_PATTERN); }); }); test.describe("Dev Bypass Authentication", () => { test("dev bypass creates session and allows access", async ({ admin }) => { // Use dev bypass to authenticate await admin.devBypassAuth(); // Now navigate to admin await admin.goto("/"); // Should see dashboard shell (sidebar with navigation) await admin.waitForShell(); // Should be on admin URL (not redirected to login) await expect(admin.page).toHaveURL(ADMIN_URL_PATTERN); }); test("session persists across page loads", async ({ admin }) => { await admin.devBypassAuth(); await admin.goto("/"); await admin.waitForShell(); // Navigate to another page via sidebar link await admin.page.click('a:has-text("Users")'); await admin.waitForShell(); // Should still be authenticated and see users page await expect(admin.page).toHaveURL(USERS_URL_PATTERN); await admin.expectPageTitle("Users"); }); }); test.describe("Logout", () => { test("logout clears session and redirects to login", async ({ admin }) => { // Authenticate first await admin.devBypassAuth(); await admin.goto("/"); await admin.waitForShell(); // Call logout via API (POST with required headers) await admin.page.evaluate(async () => { await fetch("/_emdash/api/auth/logout", { method: "POST", headers: { "X-EmDash-Request": "1" }, }); }); // Try to access admin again await admin.page.goto("/_emdash/admin/"); await admin.page.waitForURL(LOGIN_OR_ADMIN_URL_PATTERN, { timeout: 10000 }); // Should redirect to login await expect(admin.page).toHaveURL(LOGIN_URL_PATTERN); }); }); test.describe("User Menu", () => { test("shows user menu in header", async ({ admin }) => { await admin.devBypassAuth(); await admin.goto("/"); await admin.waitForShell(); // Click the user menu trigger (shows "Dev Admin" text) await admin.page.getByText("Dev Admin").click(); // Should show menu options await expect(admin.page.locator("text=Log out")).toBeVisible(); await expect(admin.page.locator("text=Security Settings")).toBeVisible(); await expect(admin.page.locator("text=Settings").last()).toBeVisible(); }); test("security settings link navigates correctly", async ({ admin }) => { await admin.devBypassAuth(); await admin.goto("/"); await admin.waitForShell(); // Open user menu await admin.page.getByText("Dev Admin").click(); // Click security settings (if present in menu) const securityLink = admin.page.getByRole("menuitem", { name: SECURITY_MENUITEM_REGEX }); if (await securityLink.isVisible({ timeout: 2000 }).catch(() => false)) { await securityLink.click(); await expect(admin.page).toHaveURL(SECURITY_SETTINGS_URL_PATTERN); } }); }); }); test.describe("User Management", () => { test.beforeEach(async ({ admin }) => { await admin.devBypassAuth(); }); test("users page shows user list", async ({ admin }) => { await admin.goto("/users"); await admin.waitForShell(); await admin.waitForLoading(); // Should show users page await admin.expectPageTitle("Users"); // Should show at least the dev user await expect(admin.page.locator("text=dev@emdash.local")).toBeVisible(); }); test("shows invite user button", async ({ admin }) => { await admin.goto("/users"); await admin.waitForShell(); // Should have invite button await expect(admin.page.locator('button:has-text("Invite User")')).toBeVisible(); }); test("invite user modal opens and closes", async ({ admin }) => { await admin.goto("/users"); await admin.waitForShell(); // Click invite button await admin.page.locator('button:has-text("Invite User")').click(); // Should show modal await expect(admin.page.locator('input[type="email"]')).toBeVisible(); await expect(admin.page.locator('button:has-text("Send Invite")')).toBeVisible(); // Close modal await admin.page.keyboard.press("Escape"); // Modal should close await expect(admin.page.locator('[role="dialog"]')).not.toBeVisible(); }); test("can click user to see details", async ({ admin }) => { await admin.goto("/users"); await admin.waitForShell(); await admin.waitForLoading(); // Click on user email link await admin.page.locator("text=dev@emdash.local").first().click(); // Should show detail panel with user info await expect(admin.page.locator("text=Dev Admin").first()).toBeVisible(); }); }); test.describe("Security Settings", () => { test.beforeEach(async ({ admin }) => { await admin.devBypassAuth(); }); test("shows security settings page", async ({ admin }) => { await admin.goto("/settings/security"); await admin.waitForShell(); await admin.waitForLoading(); await expect(admin.page.locator("text=Passkeys").first()).toBeVisible(); }); test("shows add passkey button", async ({ admin }) => { await admin.goto("/settings/security"); await admin.waitForShell(); await admin.waitForLoading(); await expect(admin.page.getByRole("button", { name: ADD_PASSKEY_REGEX })).toBeVisible(); }); }); test.describe("Signup Page", () => { test("displays signup page", async ({ admin }) => { // Navigate directly (not through admin which has auth) await admin.page.goto("/_emdash/admin/signup"); // Wait for the React app to hydrate and render a heading with sign-related content. // The SPA may render the login page if signup is disabled, so accept either. await expect( admin.page.getByRole("heading", { level: 1, name: SIGN_HEADING_REGEX }), ).toBeVisible({ timeout: 15000, }); }); });