arbret/frontend/e2e/auth.spec.ts
2025-12-26 19:21:34 +01:00

303 lines
11 KiB
TypeScript

import { test, expect, Page, APIRequestContext } from "@playwright/test";
import { getBackendUrl } from "./helpers/backend-url";
// Helper to generate unique email for each test
function uniqueEmail(): string {
return `test-${Date.now()}-${Math.random().toString(36).substring(7)}@example.com`;
}
// Helper to clear auth cookies
async function clearAuth(page: Page) {
await page.context().clearCookies();
}
// Admin credentials from seed data
const ADMIN_EMAIL = "admin@example.com";
const ADMIN_PASSWORD = "admin123";
async function createInvite(request: APIRequestContext): Promise<string> {
const apiBase = getBackendUrl();
// Login as admin
const loginResp = await request.post(`${apiBase}/api/auth/login`, {
data: { email: ADMIN_EMAIL, password: ADMIN_PASSWORD },
});
const cookies = loginResp.headers()["set-cookie"];
// Get admin user ID (we'll use admin as godfather for simplicity)
const meResp = await request.get(`${apiBase}/api/auth/me`, {
headers: { Cookie: cookies },
});
const admin = await meResp.json();
// Create invite
const inviteResp = await request.post(`${apiBase}/api/admin/invites`, {
data: { godfather_id: admin.id },
headers: { Cookie: cookies },
});
const invite = await inviteResp.json();
return invite.identifier;
}
test.describe("Authentication Flow", () => {
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
await clearAuth(page);
});
test("redirects to login when not authenticated and auth pages have correct UI", async ({
page,
}) => {
// Test redirect
await page.goto("/");
await expect(page).toHaveURL("/login");
// Test login page UI
await page.goto("/login");
await expect(page.locator("h1")).toHaveText("Welcome back");
await expect(page.locator('input[type="email"]')).toBeVisible();
await expect(page.locator('input[type="password"]')).toBeVisible();
await expect(page.locator('button[type="submit"]')).toHaveText("Sign in");
await expect(page.locator('a[href="/signup"]')).toBeVisible();
// Test navigation to signup
await page.click('a[href="/signup"]');
await expect(page).toHaveURL("/signup");
// Test signup page UI
await expect(page.locator("h1")).toHaveText("Join with Invite");
await expect(page.locator("input#inviteCode")).toBeVisible();
await expect(page.locator('button[type="submit"]')).toHaveText("Continue");
await expect(page.locator('a[href="/login"]')).toBeVisible();
// Test navigation back to login
await page.click('a[href="/login"]');
await expect(page).toHaveURL("/login");
});
});
test.describe("Signup with Invite", () => {
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
await clearAuth(page);
});
test("can create account with valid invite via form and direct URL, and logged-in users are redirected", async ({
page,
request,
}) => {
// Test signup via form
const email1 = uniqueEmail();
const inviteCode1 = await createInvite(request);
await page.goto("/signup");
await page.fill("input#inviteCode", inviteCode1);
await page.click('button[type="submit"]');
await expect(page.locator("h1")).toHaveText("Create account");
await page.fill("input#email", email1);
await page.fill("input#password", "password123");
await page.fill("input#confirmPassword", "password123");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/exchange");
await expect(page.getByRole("heading", { name: "Exchange Bitcoin" })).toBeVisible();
// Test logged-in user visiting invite URL - should redirect to exchange
const anotherInvite = await createInvite(request);
await page.goto(`/signup/${anotherInvite}`);
await expect(page).toHaveURL("/exchange");
// Test logged-in user visiting signup page - should redirect to exchange
await page.goto("/signup", { waitUntil: "networkidle" });
await expect(page).toHaveURL("/exchange", { timeout: 10000 });
// Test signup via direct URL (new session)
await clearAuth(page);
const email2 = uniqueEmail();
const inviteCode2 = await createInvite(request);
await page.goto(`/signup/${inviteCode2}`);
await page.waitForURL(/\/signup\?code=/);
await expect(page.locator("h1")).toHaveText("Create account");
await page.fill("input#email", email2);
await page.fill("input#password", "password123");
await page.fill("input#confirmPassword", "password123");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/exchange");
});
test("shows errors for invalid invite code and password validation", async ({
page,
request,
}) => {
// Test invalid invite code
await page.goto("/signup");
await page.fill("input#inviteCode", "fake-code-99");
await page.click('button[type="submit"]');
await expect(page.getByText(/not found/i)).toBeVisible();
// Test password validation with valid invite
const inviteCode = await createInvite(request);
await page.goto("/signup");
await page.fill("input#inviteCode", inviteCode);
await page.click('button[type="submit"]');
await expect(page.locator("h1")).toHaveText("Create account");
// Test password mismatch
await page.fill("input#email", uniqueEmail());
await page.fill("input#password", "password123");
await page.fill("input#confirmPassword", "differentpassword");
await page.click('button[type="submit"]');
await expect(page.getByText("Passwords do not match")).toBeVisible();
// Test short password
await page.fill("input#email", uniqueEmail());
await page.fill("input#password", "short");
await page.fill("input#confirmPassword", "short");
await page.click('button[type="submit"]');
await expect(page.getByText("Password must be at least 6 characters")).toBeVisible();
});
});
test.describe("Login", () => {
let testEmail: string;
const testPassword = "testpassword123";
test.beforeAll(async ({ request }) => {
// Create a test user with invite
testEmail = uniqueEmail();
const inviteCode = await createInvite(request);
// Register the test user via backend API
await request.post(`${getBackendUrl()}/api/auth/register`, {
data: {
email: testEmail,
password: testPassword,
invite_identifier: inviteCode,
},
});
});
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
await clearAuth(page);
});
test("can login with valid credentials and shows loading state", async ({ page }) => {
// Test loading state
await page.goto("/login");
await page.fill('input[type="email"]', testEmail);
await page.fill('input[type="password"]', testPassword);
const submitPromise = page.click('button[type="submit"]');
await expect(page.locator('button[type="submit"]')).toHaveText("Signing in...");
await submitPromise;
// Regular user redirects to exchange
await expect(page).toHaveURL("/exchange");
await expect(page.getByRole("heading", { name: "Exchange Bitcoin" })).toBeVisible();
});
test("shows error for wrong password and non-existent user", async ({ page }) => {
// Test wrong password
await page.goto("/login");
await page.fill('input[type="email"]', testEmail);
await page.fill('input[type="password"]', "wrongpassword");
await page.click('button[type="submit"]');
await expect(page.getByText("Incorrect email or password")).toBeVisible();
// Test non-existent user
await page.goto("/login");
await page.fill('input[type="email"]', "nonexistent@example.com");
await page.fill('input[type="password"]', "password123");
await page.click('button[type="submit"]');
await expect(page.getByText("Incorrect email or password")).toBeVisible();
});
});
test.describe("Logout", () => {
test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
});
test("can logout and cannot access protected pages after logout", async ({ page, request }) => {
const email = uniqueEmail();
const inviteCode = await createInvite(request);
// Sign up
await page.goto("/signup");
await page.fill("input#inviteCode", inviteCode);
await page.click('button[type="submit"]');
await expect(page.locator("h1")).toHaveText("Create account");
await page.fill("input#email", email);
await page.fill("input#password", "password123");
await page.fill("input#confirmPassword", "password123");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/exchange");
// Click logout
await page.click("text=Sign out");
await expect(page).toHaveURL("/login");
// Try to access exchange (protected page)
await page.goto("/exchange");
await expect(page).toHaveURL("/login");
});
});
test.describe("Session Persistence", () => {
test.beforeEach(async ({ context }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
});
test("session persists after page reload and cookies are managed correctly", async ({
page,
request,
}) => {
const email = uniqueEmail();
const inviteCode = await createInvite(request);
// Sign up
await page.goto("/signup");
await page.fill("input#inviteCode", inviteCode);
await page.click('button[type="submit"]');
await expect(page.locator("h1")).toHaveText("Create account");
await page.fill("input#email", email);
await page.fill("input#password", "password123");
await page.fill("input#confirmPassword", "password123");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/exchange");
await expect(page.getByRole("heading", { name: "Exchange Bitcoin" })).toBeVisible();
// Check cookies are set after signup
let cookies = await page.context().cookies();
let authCookie = cookies.find((c) => c.name === "auth_token");
expect(authCookie).toBeTruthy();
expect(authCookie!.httpOnly).toBe(true);
// Reload page - session should persist
await page.reload();
await expect(page).toHaveURL("/exchange");
await expect(page.getByRole("heading", { name: "Exchange Bitcoin" })).toBeVisible();
// Logout and verify cookie is cleared
await page.click("text=Sign out");
await expect(page).toHaveURL("/login");
cookies = await page.context().cookies();
authCookie = cookies.find((c) => c.name === "auth_token");
expect(!authCookie || authCookie.value === "").toBe(true);
});
});