arbret/frontend/e2e/permissions.spec.ts

215 lines
7.1 KiB
TypeScript
Raw Normal View History

2025-12-18 23:54:51 +01:00
import { test, expect, Page } from "@playwright/test";
2025-12-26 19:21:34 +01:00
import { getBackendUrl } from "./helpers/backend-url";
2025-12-18 23:33:32 +01:00
/**
* Permission-based E2E tests
*
2025-12-18 23:33:32 +01:00
* These tests verify that:
* 1. Regular users can access exchange and trades pages
* 2. Admin users can access admin pages (trades, invites, availability)
2025-12-18 23:33:32 +01:00
* 3. Users are properly redirected based on their permissions
* 4. API calls respect permission boundaries
*/
// 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
2025-12-18 23:54:51 +01:00
// 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.`
);
2025-12-18 23:54:51 +01:00
}
return value;
}
2025-12-18 23:33:32 +01:00
const REGULAR_USER = {
2025-12-18 23:54:51 +01:00
email: getRequiredEnv("DEV_USER_EMAIL"),
password: getRequiredEnv("DEV_USER_PASSWORD"),
2025-12-18 23:33:32 +01:00
};
const ADMIN_USER = {
2025-12-18 23:54:51 +01:00
email: getRequiredEnv("DEV_ADMIN_EMAIL"),
password: getRequiredEnv("DEV_ADMIN_PASSWORD"),
2025-12-18 23:33:32 +01:00
};
// 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 ({ context, page }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
2025-12-18 23:33:32 +01:00
await clearAuth(page);
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
});
2025-12-24 23:52:52 +01:00
test("can access exchange and trades pages with correct navigation", async ({ page }) => {
// Test redirect from home
2025-12-18 23:33:32 +01:00
await page.goto("/");
await expect(page).toHaveURL("/exchange");
2025-12-18 23:33:32 +01:00
2025-12-24 23:52:52 +01:00
// Test exchange page access
await page.goto("/exchange");
await expect(page).toHaveURL("/exchange");
await expect(page.getByText("Exchange Bitcoin")).toBeVisible();
2025-12-18 23:33:32 +01:00
2025-12-24 23:52:52 +01:00
// Test trades page access
await page.goto("/trades");
await expect(page).toHaveURL("/trades");
await expect(page.getByRole("heading", { name: "My Trades" })).toBeVisible();
2025-12-18 23:33:32 +01:00
2025-12-24 23:52:52 +01:00
// Test navigation shows exchange and trades, but not admin links
await expect(page.locator('a[href="/exchange"]').first()).toBeVisible();
const adminTradesLinks = page.locator('a[href="/admin/trades"]');
await expect(adminTradesLinks).toHaveCount(0);
2025-12-18 23:33:32 +01:00
});
});
test.describe("Admin User Access", () => {
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
2025-12-18 23:33:32 +01:00
await clearAuth(page);
2025-12-18 23:54:51 +01:00
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
2025-12-18 23:33:32 +01:00
});
2025-12-24 23:52:52 +01:00
test("can access admin pages with correct navigation", async ({ page }) => {
// Test redirect from home
2025-12-18 23:33:32 +01:00
await page.goto("/");
await expect(page).toHaveURL("/admin/trades");
2025-12-18 23:33:32 +01:00
2025-12-24 23:52:52 +01:00
// Test admin trades page
await page.goto("/admin/trades");
await expect(page).toHaveURL("/admin/trades");
await expect(page.getByRole("heading", { name: "Trades" })).toBeVisible();
2025-12-18 23:33:32 +01:00
2025-12-24 23:52:52 +01:00
// Test admin availability page
await page.goto("/admin/availability");
await expect(page).toHaveURL("/admin/availability");
await expect(page.getByRole("heading", { name: "Availability" })).toBeVisible();
2025-12-18 23:33:32 +01:00
2025-12-24 23:52:52 +01:00
// Test navigation shows admin links but not regular user links
await page.goto("/admin/trades");
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/trades"]')).toHaveCount(0); // Current page, shown as text not link
const exchangeLinks = page.locator('a[href="/exchange"]');
await expect(exchangeLinks).toHaveCount(0);
2025-12-18 23:33:32 +01:00
});
});
test.describe("Unauthenticated Access", () => {
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
2025-12-18 23:33:32 +01:00
await clearAuth(page);
});
2025-12-24 23:52:52 +01:00
test("all protected pages redirect to login", async ({ page }) => {
// Test home page redirect
2025-12-18 23:33:32 +01:00
await page.goto("/");
await expect(page).toHaveURL("/login");
2025-12-24 23:52:52 +01:00
// Test exchange page redirect
await page.goto("/exchange");
2025-12-18 23:33:32 +01:00
await expect(page).toHaveURL("/login");
2025-12-24 23:52:52 +01:00
// Test admin page redirect
await page.goto("/admin/trades");
2025-12-18 23:33:32 +01:00
await expect(page).toHaveURL("/login");
});
});
test.describe("Permission Boundary via API", () => {
2025-12-24 23:52:52 +01:00
test("API calls respect permission boundaries", async ({ page, request }) => {
// Test regular user cannot access admin API
2025-12-18 23:33:32 +01:00
await clearAuth(page);
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
2025-12-24 23:52:52 +01:00
let cookies = await page.context().cookies();
let authCookie = cookies.find((c) => c.name === "auth_token");
2025-12-18 23:33:32 +01:00
if (authCookie) {
2025-12-26 19:21:34 +01:00
const response = await request.get(`${getBackendUrl()}/api/admin/trades/upcoming`, {
2025-12-18 23:33:32 +01:00
headers: {
Cookie: `auth_token=${authCookie.value}`,
},
});
expect(response.status()).toBe(403);
}
2025-12-24 23:52:52 +01:00
// Test admin cannot access regular user API
2025-12-18 23:33:32 +01:00
await clearAuth(page);
2025-12-18 23:54:51 +01:00
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
2025-12-24 23:52:52 +01:00
cookies = await page.context().cookies();
authCookie = cookies.find((c) => c.name === "auth_token");
2025-12-18 23:33:32 +01:00
if (authCookie) {
2025-12-26 19:21:34 +01:00
const response = await request.get(`${getBackendUrl()}/api/exchange/price`, {
2025-12-18 23:33:32 +01:00
headers: {
Cookie: `auth_token=${authCookie.value}`,
},
});
expect(response.status()).toBe(403);
}
});
});
test.describe("Session and Logout", () => {
test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
});
2025-12-24 23:52:52 +01:00
test("logout clears permissions and tampered cookies are rejected", async ({ page, context }) => {
// Test logout clears permissions
2025-12-18 23:33:32 +01:00
await clearAuth(page);
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
await expect(page).toHaveURL("/exchange");
2025-12-18 23:33:32 +01:00
await page.click("text=Sign out");
await expect(page).toHaveURL("/login");
await page.goto("/exchange");
2025-12-18 23:33:32 +01:00
await expect(page).toHaveURL("/login");
2025-12-24 23:52:52 +01:00
// Test tampered cookie is rejected
2025-12-18 23:33:32 +01:00
await context.addCookies([
{
name: "auth_token",
value: "fake-token-that-should-not-work",
domain: "localhost",
path: "/",
},
]);
await page.goto("/exchange");
2025-12-18 23:33:32 +01:00
await expect(page).toHaveURL("/login");
});
});