Files
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

250 lines
7.8 KiB
TypeScript

/**
* 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("admin shell is not storable by shared caches", async ({ request }) => {
// Regression: the admin shell had no Cache-Control header, so shared
// caches applying RFC 9111 heuristic freshness (e.g. Cloudflare's
// Workers Cache) could store an authenticated 200 and replay it to
// anonymous visitors instead of the login redirect.
const res = await request.get("/_emdash/admin/login");
expect(res.status()).toBe(200);
expect(res.headers()["cache-control"]).toBe("private, no-store");
});
});
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,
});
});
});