import { test, expect, Page } from "@playwright/test"; /** * Permission-based E2E tests * * These tests verify that: * 1. Regular users can only access Counter and Sum pages * 2. Admin users can only access the Audit page * 3. Users are properly redirected based on their permissions * 4. API calls respect permission boundaries */ const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; // Test credentials - must match what's seeded in the database via seed.py // These come from environment variables DEV_USER_EMAIL/PASSWORD and DEV_ADMIN_EMAIL/PASSWORD // Tests will fail fast if these are not set function getRequiredEnv(name: string): string { const value = process.env[name]; if (!value) { throw new Error( `Required environment variable ${name} is not set. Run 'source .env' or set it in your environment.` ); } return value; } const REGULAR_USER = { email: getRequiredEnv("DEV_USER_EMAIL"), password: getRequiredEnv("DEV_USER_PASSWORD"), }; const ADMIN_USER = { email: getRequiredEnv("DEV_ADMIN_EMAIL"), password: getRequiredEnv("DEV_ADMIN_PASSWORD"), }; // Helper to clear auth cookies async function clearAuth(page: Page) { await page.context().clearCookies(); } // Helper to login a user async function loginUser(page: Page, email: string, password: string) { await page.goto("/login"); await page.fill('input[type="email"]', email); await page.fill('input[type="password"]', password); await page.click('button[type="submit"]'); // Wait for navigation away from login page await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 10000 }); } // Setup: Users are pre-seeded via seed.py before e2e tests run // The seed script creates: // - A regular user (DEV_USER_EMAIL/PASSWORD) with "regular" role // - An admin user (DEV_ADMIN_EMAIL/PASSWORD) with "admin" role test.beforeAll(async () => { // No need to create users - they are seeded by scripts/e2e.sh }); test.describe("Regular User Access", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); }); test("can access counter page", async ({ page }) => { await page.goto("/"); // Should stay on counter page await expect(page).toHaveURL("/"); // Should see counter UI await expect(page.getByText("Current Count")).toBeVisible(); await expect(page.getByRole("button", { name: /increment/i })).toBeVisible(); }); test("can access sum page", async ({ page }) => { await page.goto("/sum"); // Should stay on sum page await expect(page).toHaveURL("/sum"); // Should see sum UI await expect(page.getByText("Sum Calculator")).toBeVisible(); }); test("cannot access audit page - redirected to counter", async ({ page }) => { await page.goto("/audit"); // Should be redirected to counter page (home) await expect(page).toHaveURL("/"); }); test("navigation only shows Counter and Sum", async ({ page }) => { await page.goto("/"); // Should see Counter and Sum in nav await expect(page.getByText("Counter")).toBeVisible(); await expect(page.getByText("Sum")).toBeVisible(); // Should NOT see Audit in nav (for regular users) const auditLinks = page.locator('a[href="/audit"]'); await expect(auditLinks).toHaveCount(0); }); test("can navigate between Counter and Sum", async ({ page }) => { await page.goto("/"); // Go to Sum await page.click('a[href="/sum"]'); await expect(page).toHaveURL("/sum"); // Go back to Counter await page.click('a[href="/"]'); await expect(page).toHaveURL("/"); }); test("can use counter functionality", async ({ page }) => { await page.goto("/"); // Get initial count (might be any number) const countElement = page.locator("h1").first(); await expect(countElement).toBeVisible(); // Click increment await page.click('button:has-text("Increment")'); // Wait for update await page.waitForTimeout(500); // Counter should have updated (we just verify no error occurred) await expect(countElement).toBeVisible(); }); test("can use sum functionality", async ({ page }) => { await page.goto("/sum"); // Fill in numbers await page.fill('input[aria-label="First number"]', "5"); await page.fill('input[aria-label="Second number"]', "3"); // Calculate await page.click('button:has-text("Calculate")'); // Should show result await expect(page.getByText("8")).toBeVisible(); }); }); test.describe("Admin User Access", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); }); test("redirected from counter page to audit", async ({ page }) => { await page.goto("/"); // Should be redirected to audit page await expect(page).toHaveURL("/audit"); }); test("redirected from sum page to audit", async ({ page }) => { await page.goto("/sum"); // Should be redirected to audit page await expect(page).toHaveURL("/audit"); }); test("can access audit page", async ({ page }) => { await page.goto("/audit"); // Should stay on audit page await expect(page).toHaveURL("/audit"); // Should see audit tables await expect(page.getByText("Counter Activity")).toBeVisible(); await expect(page.getByText("Sum Activity")).toBeVisible(); }); test("navigation only shows Audit", async ({ page }) => { await page.goto("/audit"); // Should see Audit as current await expect(page.getByText("Audit")).toBeVisible(); // Should NOT see Counter or Sum links (for admin users) const counterLinks = page.locator('a[href="/"]'); const sumLinks = page.locator('a[href="/sum"]'); await expect(counterLinks).toHaveCount(0); await expect(sumLinks).toHaveCount(0); }); test("audit page shows records", async ({ page }) => { await page.goto("/audit"); // Should see the tables await expect(page.getByRole("table")).toHaveCount(2); // Should see column headers (use first() since there are two tables with same headers) await expect(page.getByRole("columnheader", { name: "User" }).first()).toBeVisible(); await expect(page.getByRole("columnheader", { name: "Date" }).first()).toBeVisible(); }); }); test.describe("Unauthenticated Access", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); }); test("counter page redirects to login", async ({ page }) => { await page.goto("/"); await expect(page).toHaveURL("/login"); }); test("sum page redirects to login", async ({ page }) => { await page.goto("/sum"); await expect(page).toHaveURL("/login"); }); test("audit page redirects to login", async ({ page }) => { await page.goto("/audit"); await expect(page).toHaveURL("/login"); }); }); test.describe("Permission Boundary via API", () => { test("regular user API call to audit returns 403", async ({ page, request }) => { // Login as regular user await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); // Get cookies const cookies = await page.context().cookies(); const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { // Try to call audit API directly const response = await request.get(`${API_URL}/api/audit/counter`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(403); } }); test("admin user API call to counter returns 403", async ({ page, request }) => { // Login as admin await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); // Get cookies const cookies = await page.context().cookies(); const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { // Try to call counter API directly const response = await request.get(`${API_URL}/api/counter`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(403); } }); }); test.describe("Session and Logout", () => { test("logout clears permissions - cannot access protected pages", async ({ page }) => { // Login await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); await expect(page).toHaveURL("/"); // Logout await page.click("text=Sign out"); await expect(page).toHaveURL("/login"); // Try to access counter await page.goto("/"); await expect(page).toHaveURL("/login"); }); test("cannot access pages with tampered cookie", async ({ page, context }) => { // Set a fake auth cookie await context.addCookies([ { name: "auth_token", value: "fake-token-that-should-not-work", domain: "localhost", path: "/", }, ]); // Try to access protected page await page.goto("/"); // Should be redirected to login await expect(page).toHaveURL("/login"); }); });