2025-12-19 10:12:55 +01:00
|
|
|
import { test, expect, Page } from "@playwright/test";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Profile E2E tests
|
2025-12-21 21:59:26 +01:00
|
|
|
*
|
2025-12-19 10:12:55 +01:00
|
|
|
* 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();
|
2025-12-21 21:59:26 +01:00
|
|
|
const authCookie = cookies.find((c) => c.name === "auth_token");
|
|
|
|
|
|
2025-12-19 10:12:55 +01:00
|
|
|
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", () => {
|
2025-12-25 22:35:27 +01:00
|
|
|
test.beforeEach(async ({ context, page }) => {
|
|
|
|
|
// Set English language before any navigation
|
|
|
|
|
await context.addInitScript(() => {
|
|
|
|
|
if (typeof window !== "undefined") {
|
|
|
|
|
window.localStorage.setItem("arbret-locale", "en");
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-19 10:12:55 +01:00
|
|
|
await clearAuth(page);
|
|
|
|
|
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
test("can navigate to profile page and page displays correct elements", async ({ page }) => {
|
|
|
|
|
// Test navigation from exchange
|
2025-12-22 20:18:33 +01:00
|
|
|
await page.goto("/exchange");
|
2025-12-19 10:12:55 +01:00
|
|
|
await expect(page.getByText("My Profile")).toBeVisible();
|
|
|
|
|
await page.click('a[href="/profile"]');
|
|
|
|
|
await expect(page).toHaveURL("/profile");
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test navigation from trades
|
2025-12-22 20:18:33 +01:00
|
|
|
await page.goto("/trades");
|
2025-12-19 10:12:55 +01:00
|
|
|
await expect(page.getByText("My Profile")).toBeVisible();
|
|
|
|
|
await page.click('a[href="/profile"]');
|
|
|
|
|
await expect(page).toHaveURL("/profile");
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test page structure
|
2025-12-19 10:12:55 +01:00
|
|
|
await expect(page.getByRole("heading", { name: "My Profile" })).toBeVisible();
|
|
|
|
|
await expect(page.getByText("Login EmailRead only")).toBeVisible();
|
|
|
|
|
await expect(page.getByText("Contact Details")).toBeVisible();
|
|
|
|
|
await expect(page.getByText(/communication purposes only/i)).toBeVisible();
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test form fields visibility
|
2025-12-19 10:12:55 +01:00
|
|
|
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();
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test login email is read-only
|
2025-12-19 10:12:55 +01:00
|
|
|
const loginEmailInput = page.locator('input[type="email"][disabled]');
|
|
|
|
|
await expect(loginEmailInput).toHaveValue(REGULAR_USER.email);
|
|
|
|
|
await expect(loginEmailInput).toBeDisabled();
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test navigation links
|
2025-12-22 20:18:33 +01:00
|
|
|
await expect(page.locator('a[href="/exchange"]')).toBeVisible();
|
|
|
|
|
await expect(page.locator('a[href="/trades"]')).toBeVisible();
|
2025-12-19 10:12:55 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
test("form state management, save, persistence, and clearing fields", async ({ page }) => {
|
2025-12-19 10:12:55 +01:00
|
|
|
await page.goto("/profile");
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-19 10:12:55 +01:00
|
|
|
// 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("");
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Save button should be disabled when no changes
|
2025-12-19 10:12:55 +01:00
|
|
|
const saveButton = page.getByRole("button", { name: /save changes/i });
|
|
|
|
|
await expect(saveButton).toBeDisabled();
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Make a change - button should be enabled
|
2025-12-19 10:12:55 +01:00
|
|
|
await page.fill("#telegram", "@testhandle");
|
|
|
|
|
await expect(saveButton).toBeEnabled();
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Now test saving and persistence - fill in all fields
|
2025-12-19 10:12:55 +01:00
|
|
|
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);
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-19 10:12:55 +01:00
|
|
|
// Save
|
|
|
|
|
await page.click('button:has-text("Save Changes")');
|
|
|
|
|
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-19 10:12:55 +01:00
|
|
|
// 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);
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test clearing a field
|
2025-12-19 10:12:55 +01:00
|
|
|
await page.fill("#telegram", "@initial");
|
|
|
|
|
await page.click('button:has-text("Save Changes")');
|
|
|
|
|
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
2025-12-19 10:30:23 +01:00
|
|
|
await expect(page.getByText(/saved successfully/i)).not.toBeVisible({ timeout: 5000 });
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-19 10:12:55 +01:00
|
|
|
// Clear the field
|
|
|
|
|
await page.fill("#telegram", "");
|
|
|
|
|
await page.click('button:has-text("Save Changes")');
|
|
|
|
|
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-19 10:12:55 +01:00
|
|
|
// 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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
test("validation - all field validations and error fixing", async ({ page }) => {
|
2025-12-19 10:12:55 +01:00
|
|
|
await page.goto("/profile");
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test telegram auto-prepend
|
2025-12-19 10:52:47 +01:00
|
|
|
await page.fill("#telegram", "testhandle");
|
|
|
|
|
await expect(page.locator("#telegram")).toHaveValue("@testhandle");
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test telegram error - no characters after @
|
2025-12-19 11:08:19 +01:00
|
|
|
await page.fill("#telegram", "@");
|
2025-12-25 00:06:32 +01:00
|
|
|
await expect(page.getByText(/at least one character after @/i)).toBeVisible({ timeout: 2000 });
|
2025-12-19 10:12:55 +01:00
|
|
|
const saveButton = page.getByRole("button", { name: /save changes/i });
|
|
|
|
|
await expect(saveButton).toBeDisabled();
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Test invalid npub
|
2025-12-19 10:12:55 +01:00
|
|
|
await page.fill("#nostr_npub", "invalidnpub");
|
|
|
|
|
await expect(page.getByText(/must start with 'npub1'/i)).toBeVisible();
|
|
|
|
|
await expect(saveButton).toBeDisabled();
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Test invalid email format
|
|
|
|
|
await page.fill("#contact_email", "not-an-email");
|
|
|
|
|
await expect(page.getByText(/valid email/i)).toBeVisible();
|
|
|
|
|
await expect(saveButton).toBeDisabled();
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Fix all validation errors and save
|
2025-12-19 10:12:55 +01:00
|
|
|
await page.fill("#telegram", "@validhandle");
|
2025-12-25 00:06:32 +01:00
|
|
|
await expect(page.getByText(/at least one character after @/i)).not.toBeVisible({
|
|
|
|
|
timeout: 2000,
|
|
|
|
|
});
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
await page.fill("#nostr_npub", VALID_NPUB);
|
|
|
|
|
await expect(page.getByText(/must start with 'npub1'/i)).not.toBeVisible({ timeout: 2000 });
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
await page.fill("#contact_email", "valid@email.com");
|
|
|
|
|
await expect(page.getByText(/valid email/i)).not.toBeVisible({ timeout: 2000 });
|
2025-12-21 21:59:26 +01:00
|
|
|
|
2025-12-25 00:06:32 +01:00
|
|
|
// Now all errors are fixed, save button should be enabled
|
2025-12-19 10:12:55 +01:00
|
|
|
await expect(saveButton).toBeEnabled();
|
|
|
|
|
await page.click('button:has-text("Save Changes")');
|
|
|
|
|
await expect(page.getByText(/saved successfully/i)).toBeVisible();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test.describe("Profile - Admin User Access", () => {
|
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
|
|
|
await clearAuth(page);
|
|
|
|
|
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
test("admin cannot access profile page or API", async ({ page, request }) => {
|
|
|
|
|
// Admin should not see profile link
|
2025-12-22 20:18:33 +01:00
|
|
|
await page.goto("/admin/trades");
|
|
|
|
|
await expect(page).toHaveURL("/admin/trades");
|
2025-12-19 10:12:55 +01:00
|
|
|
await expect(page.locator('a[href="/profile"]')).toHaveCount(0);
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Admin should be redirected when accessing profile page
|
2025-12-19 10:12:55 +01:00
|
|
|
await page.goto("/profile");
|
2025-12-22 20:18:33 +01:00
|
|
|
await expect(page).toHaveURL("/admin/trades");
|
2025-12-19 10:12:55 +01:00
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// Admin API call should return 403
|
2025-12-19 10:12:55 +01:00
|
|
|
const cookies = await page.context().cookies();
|
2025-12-21 21:59:26 +01:00
|
|
|
const authCookie = cookies.find((c) => c.name === "auth_token");
|
|
|
|
|
|
2025-12-19 10:12:55 +01:00
|
|
|
if (authCookie) {
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
test("profile page and API require authentication", async ({ page, request }) => {
|
|
|
|
|
// Page redirects to login
|
2025-12-19 10:12:55 +01:00
|
|
|
await page.goto("/profile");
|
|
|
|
|
await expect(page).toHaveURL("/login");
|
|
|
|
|
|
2025-12-24 23:52:52 +01:00
|
|
|
// API requires authentication
|
2025-12-19 10:12:55 +01:00
|
|
|
const response = await request.get(`${API_URL}/api/profile`);
|
|
|
|
|
expect(response.status()).toBe(401);
|
|
|
|
|
});
|
|
|
|
|
});
|