arbret/frontend/e2e/permissions.spec.ts
counterweight c89e0312fa
Phase 0.3: Update E2E tests for cleanup
- Delete counter.spec.ts and random-jobs.spec.ts
- Rewrite permissions.spec.ts for new permission structure
- Update scripts/e2e.sh: remove worker.py execution
- Update generated api.ts types
2025-12-22 18:13:24 +01:00

257 lines
8.2 KiB
TypeScript

import { test, expect, Page } from "@playwright/test";
/**
* Permission-based E2E tests
*
* These tests verify that:
* 1. Regular users can access booking and appointments pages
* 2. Admin users can access admin pages (invites, availability, appointments)
* 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("redirected from home to booking page", async ({ page }) => {
await page.goto("/");
// Should be redirected to booking page
await expect(page).toHaveURL("/booking");
});
test("can access booking page", async ({ page }) => {
await page.goto("/booking");
// Should stay on booking page
await expect(page).toHaveURL("/booking");
// Should see booking UI
await expect(page.getByText("Book an Appointment")).toBeVisible();
});
test("can access appointments page", async ({ page }) => {
await page.goto("/appointments");
// Should stay on appointments page
await expect(page).toHaveURL("/appointments");
// Should see appointments UI
await expect(page.getByText("My Appointments")).toBeVisible();
});
test("navigation shows booking and appointments", async ({ page }) => {
await page.goto("/appointments");
// From appointments page, we can see the nav links
// "Appointments" is the current page (shown as span, not link)
// "Book" should be a link - use first() since there may be other booking links on page
await expect(page.locator('a[href="/booking"]').first()).toBeVisible();
// Should NOT see admin links
const availabilityLinks = page.locator('a[href="/admin/availability"]');
await expect(availabilityLinks).toHaveCount(0);
});
});
test.describe("Admin User Access", () => {
test.beforeEach(async ({ page }) => {
await clearAuth(page);
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
});
test("redirected from home to admin appointments", async ({ page }) => {
await page.goto("/");
// Should be redirected to admin appointments page
await expect(page).toHaveURL("/admin/appointments");
});
test("can access admin appointments page", async ({ page }) => {
await page.goto("/admin/appointments");
// Should stay on admin appointments page
await expect(page).toHaveURL("/admin/appointments");
// Should see appointments UI (use heading for specificity)
await expect(page.getByRole("heading", { name: "All Appointments" })).toBeVisible();
});
test("can access admin availability page", async ({ page }) => {
await page.goto("/admin/availability");
// Should stay on availability page
await expect(page).toHaveURL("/admin/availability");
// Should see availability UI (use heading for specificity)
await expect(page.getByRole("heading", { name: "Availability" })).toBeVisible();
});
test("navigation shows admin links", async ({ page }) => {
await page.goto("/admin/appointments");
// Should see admin nav items (use locator for nav links)
await expect(page.locator('a[href="/admin/invites"]')).toBeVisible();
await expect(page.locator('a[href="/admin/availability"]')).toBeVisible();
await expect(page.locator('a[href="/admin/appointments"]')).toHaveCount(0); // Current page, shown as text not link
// Should NOT see regular user links
const bookLinks = page.locator('a[href="/booking"]');
await expect(bookLinks).toHaveCount(0);
});
});
test.describe("Unauthenticated Access", () => {
test.beforeEach(async ({ page }) => {
await clearAuth(page);
});
test("home page redirects to login", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveURL("/login");
});
test("booking page redirects to login", async ({ page }) => {
await page.goto("/booking");
await expect(page).toHaveURL("/login");
});
test("admin page redirects to login", async ({ page }) => {
await page.goto("/admin/appointments");
await expect(page).toHaveURL("/login");
});
});
test.describe("Permission Boundary via API", () => {
test("regular user API call to admin appointments 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 admin appointments API directly
const response = await request.get(`${API_URL}/api/admin/appointments`, {
headers: {
Cookie: `auth_token=${authCookie.value}`,
},
});
expect(response.status()).toBe(403);
}
});
test("admin user API call to booking slots 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 booking slots API directly (requires regular user permission)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const dateStr = tomorrow.toISOString().split("T")[0];
const response = await request.get(`${API_URL}/api/booking/slots?date=${dateStr}`, {
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("/booking");
// Logout
await page.click("text=Sign out");
await expect(page).toHaveURL("/login");
// Try to access booking
await page.goto("/booking");
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("/booking");
// Should be redirected to login
await expect(page).toHaveURL("/login");
});
});