/** * Authentication helpers for E2E tests. * * Provides functions to inject session cookies and to perform a full * registration-and-verify flow through the UI + direct DB access. */ import type { Page } from '@playwright/test'; import pg from 'pg'; import { createHash } from 'node:crypto'; const { Client } = pg; // --------------------------------------------------------------------------- // Known test tokens — must match seed.ts // --------------------------------------------------------------------------- export const ADMIN_SESSION_TOKEN = 'e2e-admin-session-token-for-testing-only'; export const USER_SESSION_TOKEN = 'e2e-user-session-token-for-testing-only'; export const SESSION_COOKIE_NAME = 'ai_synth_session'; // --------------------------------------------------------------------------- // Cookie injection helpers // --------------------------------------------------------------------------- /** * Set the session cookie for the admin test user. * The page must already be navigated to (or about to navigate to) * the app domain so the cookie is scoped correctly. */ export async function loginAsAdmin(page: Page): Promise { await page.context().addCookies([ { name: SESSION_COOKIE_NAME, value: ADMIN_SESSION_TOKEN, domain: 'localhost', path: '/', httpOnly: true, sameSite: 'Lax', }, ]); } /** * Set the session cookie for the regular test user. */ export async function loginAsUser(page: Page): Promise { await page.context().addCookies([ { name: SESSION_COOKIE_NAME, value: USER_SESSION_TOKEN, domain: 'localhost', path: '/', httpOnly: true, sameSite: 'Lax', }, ]); } /** * Perform a full registration flow for a new email address: * 1. Call the registration API directly (avoids Turnstile DOM issues) * 2. Insert a known magic link token into the DB * 3. Navigate to the verify URL with that token * * Uses the API directly because the Cloudflare Turnstile script causes * continuous DOM mutations that prevent Playwright from interacting with * elements. The backend bypasses Turnstile verification in test mode. */ export async function registerAndVerify( page: Page, email: string, ): Promise { const databaseUrl = process.env.DATABASE_URL ?? 'postgres://ai_synth_test:testpassword@localhost:5433/ai_synth_test'; // Register via the API directly (backend is in test mode) await page.goto('/', { waitUntil: 'domcontentloaded' }); await page.evaluate(async (em: string) => { const resp = await fetch('/api/v1/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', }, body: JSON.stringify({ email: em, turnstile_token: 'mock-token', }), }); if (!resp.ok) throw new Error(`Registration failed: ${resp.status}`); }, email); // Insert a known magic token for this email directly const knownRawToken = `e2e-magic-token-${Date.now()}`; const knownTokenHash = createHash('sha256') .update(knownRawToken) .digest('hex'); const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString(); const client = new Client({ connectionString: databaseUrl }); await client.connect(); try { await client.query( `INSERT INTO magic_tokens (email, token_hash, expires_at, used) VALUES ($1, $2, $3, false)`, [email.toLowerCase(), knownTokenHash, expiresAt], ); } finally { await client.end(); } // Navigate to the verify URL (GET endpoint that sets cookie and redirects) await page.goto(`/api/v1/auth/verify?token=${knownRawToken}`, { waitUntil: 'domcontentloaded' }); // The GET verify endpoint redirects to APP_URL (http://localhost:8080) // Wait for the redirect to complete and the home page to load await page.waitForURL('**/', { timeout: 10_000 }); } /** * Connect directly to the test database and return a pg Client. */ export function createDbClient(): InstanceType { const databaseUrl = process.env.DATABASE_URL ?? 'postgres://ai_synth_test:testpassword@localhost:5433/ai_synth_test'; return new Client({ connectionString: databaseUrl }); }