304 lines
9.1 KiB
TypeScript
304 lines
9.1 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
|