import { test, expect, Page } from "@playwright/test"; /** * Profile E2E tests * * These tests verify that: * 1. Regular users can access and use the profile page * 2. Admin users cannot access the profile page * 3. Profile data persists correctly * 4. Validation works as expected */ 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 function getRequiredEnv(name: string): string { const value = process.env[name]; if (!value) { throw new Error(`Required environment variable ${name} is not set.`); } 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"), }; // Valid test npub (32 zero bytes encoded as bech32) const VALID_NPUB = "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqujme"; // 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"]'); await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 10000 }); } // Helper to clear profile data via API async function clearProfileData(page: Page) { const cookies = await page.context().cookies(); const authCookie = cookies.find(c => c.name === "auth_token"); if (authCookie) { await page.request.put(`${API_URL}/api/profile`, { headers: { Cookie: `auth_token=${authCookie.value}`, "Content-Type": "application/json", }, data: { contact_email: null, telegram: null, signal: null, nostr_npub: null, }, }); } } test.describe("Profile - Regular User Access", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); }); test("can navigate to profile page from counter", async ({ page }) => { await page.goto("/"); // Should see My Profile link await expect(page.getByText("My Profile")).toBeVisible(); // Click to navigate await page.click('a[href="/profile"]'); await expect(page).toHaveURL("/profile"); }); test("can navigate to profile page from sum", async ({ page }) => { await page.goto("/sum"); // Should see My Profile link await expect(page.getByText("My Profile")).toBeVisible(); // Click to navigate await page.click('a[href="/profile"]'); await expect(page).toHaveURL("/profile"); }); test("profile page displays correct elements", async ({ page }) => { await page.goto("/profile"); // Should see page title await expect(page.getByRole("heading", { name: "My Profile" })).toBeVisible(); // Should see login email label with read-only badge await expect(page.getByText("Login EmailRead only")).toBeVisible(); // Should see contact details section await expect(page.getByText("Contact Details")).toBeVisible(); await expect(page.getByText(/communication purposes only/i)).toBeVisible(); // Should see all form fields await expect(page.getByLabel("Contact Email")).toBeVisible(); await expect(page.getByLabel("Telegram")).toBeVisible(); await expect(page.getByLabel("Signal")).toBeVisible(); await expect(page.getByLabel("Nostr (npub)")).toBeVisible(); }); test("login email is displayed and read-only", async ({ page }) => { await page.goto("/profile"); // Login email should show the user's email const loginEmailInput = page.locator('input[type="email"][disabled]'); await expect(loginEmailInput).toHaveValue(REGULAR_USER.email); await expect(loginEmailInput).toBeDisabled(); }); test("navigation shows Counter, Sum, and My Profile", async ({ page }) => { await page.goto("/profile"); // Should see all nav items (Counter and Sum as links) await expect(page.locator('a[href="/"]')).toBeVisible(); await expect(page.locator('a[href="/sum"]')).toBeVisible(); // My Profile is the page title (h1) since we're on this page await expect(page.getByRole("heading", { name: "My Profile" })).toBeVisible(); }); }); test.describe("Profile - Form Behavior", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); // Clear any existing profile data await clearProfileData(page); }); test("new user has empty profile fields", async ({ page }) => { await page.goto("/profile"); // All editable fields should be empty await expect(page.getByLabel("Contact Email")).toHaveValue(""); await expect(page.getByLabel("Telegram")).toHaveValue(""); await expect(page.getByLabel("Signal")).toHaveValue(""); await expect(page.getByLabel("Nostr (npub)")).toHaveValue(""); }); test("save button is disabled when no changes", async ({ page }) => { await page.goto("/profile"); // Save button should be disabled const saveButton = page.getByRole("button", { name: /save changes/i }); await expect(saveButton).toBeDisabled(); }); test("save button is enabled after making changes", async ({ page }) => { await page.goto("/profile"); // Make a change await page.fill("#telegram", "@testhandle"); // Save button should be enabled const saveButton = page.getByRole("button", { name: /save changes/i }); await expect(saveButton).toBeEnabled(); }); test("can save profile and values persist", async ({ page }) => { await page.goto("/profile"); // Fill in all fields await page.fill("#contact_email", "contact@test.com"); await page.fill("#telegram", "@testuser"); await page.fill("#signal", "signal.42"); await page.fill("#nostr_npub", VALID_NPUB); // Save await page.click('button:has-text("Save Changes")'); // Should see success message await expect(page.getByText(/saved successfully/i)).toBeVisible(); // Reload and verify values persist await page.reload(); await expect(page.getByLabel("Contact Email")).toHaveValue("contact@test.com"); await expect(page.getByLabel("Telegram")).toHaveValue("@testuser"); await expect(page.getByLabel("Signal")).toHaveValue("signal.42"); await expect(page.getByLabel("Nostr (npub)")).toHaveValue(VALID_NPUB); }); test("can clear a field and save", async ({ page }) => { await page.goto("/profile"); // First set a value await page.fill("#telegram", "@initial"); await page.click('button:has-text("Save Changes")'); await expect(page.getByText(/saved successfully/i)).toBeVisible(); // Wait for toast to disappear await expect(page.getByText(/saved successfully/i)).not.toBeVisible({ timeout: 5000 }); // Clear the field await page.fill("#telegram", ""); await page.click('button:has-text("Save Changes")'); await expect(page.getByText(/saved successfully/i)).toBeVisible(); // Reload and verify it's cleared await page.reload(); await expect(page.getByLabel("Telegram")).toHaveValue(""); }); }); test.describe("Profile - Validation", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); await clearProfileData(page); }); test("auto-prepends @ for telegram when starting with letter", async ({ page }) => { await page.goto("/profile"); // Type a letter without @ - should auto-prepend @ await page.fill("#telegram", "testhandle"); // Should show @testhandle in the input await expect(page.locator("#telegram")).toHaveValue("@testhandle"); }); test("shows error for telegram handle that is too short", async ({ page }) => { await page.goto("/profile"); // Enter telegram with @ but too short (needs 5+ chars) await page.fill("#telegram", "@ab"); // Wait for debounced validation await page.waitForTimeout(600); // Should show error about length await expect(page.getByText(/at least 5 characters/i)).toBeVisible(); // Save button should be disabled const saveButton = page.getByRole("button", { name: /save changes/i }); await expect(saveButton).toBeDisabled(); }); test("shows error for invalid npub", async ({ page }) => { await page.goto("/profile"); // Enter invalid npub await page.fill("#nostr_npub", "invalidnpub"); // Should show error await expect(page.getByText(/must start with 'npub1'/i)).toBeVisible(); // Save button should be disabled const saveButton = page.getByRole("button", { name: /save changes/i }); await expect(saveButton).toBeDisabled(); }); test("can fix validation error and save", async ({ page }) => { await page.goto("/profile"); // Enter invalid telegram await page.fill("#telegram", "noat"); await expect(page.getByText(/must start with @/i)).toBeVisible(); // Fix it await page.fill("#telegram", "@validhandle"); // Error should disappear await expect(page.getByText(/must start with @/i)).not.toBeVisible(); // Should be able to save const saveButton = page.getByRole("button", { name: /save changes/i }); await expect(saveButton).toBeEnabled(); await page.click('button:has-text("Save Changes")'); await expect(page.getByText(/saved successfully/i)).toBeVisible(); }); test("shows error for invalid email format", async ({ page }) => { await page.goto("/profile"); // Enter invalid email await page.fill("#contact_email", "not-an-email"); // Should show error await expect(page.getByText(/valid email/i)).toBeVisible(); }); }); test.describe("Profile - Admin User Access", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); }); test("admin does not see My Profile link", async ({ page }) => { await page.goto("/audit"); // Should be on audit page await expect(page).toHaveURL("/audit"); // Should NOT see My Profile link await expect(page.locator('a[href="/profile"]')).toHaveCount(0); }); test("admin cannot access profile page - redirected to audit", async ({ page }) => { await page.goto("/profile"); // Should be redirected to audit await expect(page).toHaveURL("/audit"); }); test("admin API call to profile returns 403", async ({ page, request }) => { const cookies = await page.context().cookies(); const authCookie = cookies.find(c => c.name === "auth_token"); if (authCookie) { // Try to call profile API directly const response = await request.get(`${API_URL}/api/profile`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, }); expect(response.status()).toBe(403); } }); }); test.describe("Profile - Unauthenticated Access", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); }); test("profile page redirects to login", async ({ page }) => { await page.goto("/profile"); await expect(page).toHaveURL("/login"); }); test("profile API requires authentication", async ({ page, request }) => { const response = await request.get(`${API_URL}/api/profile`); expect(response.status()).toBe(401); }); });