import { test, expect } from '@playwright/test'; import { login } from './helpers/auth'; const stamp = () => Date.now().toString(36); /** * These tests run WITHOUT the stored session so they can test the login flow itself. * Playwright's storageState is only applied for the 'chromium' project, which depends * on the 'setup' project. These tests use a fresh context via test.use({ storageState: undefined }). */ test.use({ storageState: { cookies: [], origins: [] } }); test.describe('Authentication', () => { test('login page renders correctly', async ({ page }) => { await page.goto('/login'); await expect(page.getByLabel('Benutzername')).toBeVisible(); await expect(page.getByLabel('Passwort')).toBeVisible(); await expect(page.getByRole('button', { name: 'Anmelden' })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/login-page.png' }); }); test('redirects unauthenticated users to /login', async ({ page }) => { await page.goto('/'); await expect(page).toHaveURL(/\/login/); await page.screenshot({ path: 'test-results/e2e/auth-redirect.png' }); }); test('protected routes redirect to /login without session', async ({ page }) => { for (const url of ['/documents/new', '/persons', '/briefwechsel']) { await page.goto(url); await expect(page).toHaveURL(/\/login/); } }); test('shows an error for wrong credentials', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Benutzername').fill('nichtexistent'); await page.getByLabel('Passwort').fill('falschespasswort'); await page.getByRole('button', { name: 'Anmelden' }).click(); // Stays on login, shows error await expect(page).toHaveURL(/\/login/); await expect(page.locator('.text-red-600')).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/login-error.png' }); }); test('login with valid credentials redirects to home', async ({ page }) => { await login(page); await expect(page).toHaveURL('/'); await expect(page.getByPlaceholder('Suche in Titel, Inhalt, Ort...')).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/login-success.png' }); }); test('login establishes a session that authenticates API calls', async ({ page }) => { // Guards against regressions where the session cookie is set but broken. // The profile page calls /api/users/me server-side — if auth works end-to-end, // it loads without redirecting to /login. await login(page); await page.goto('/profile'); await expect(page).toHaveURL('/profile'); await expect(page.getByRole('heading', { name: /Mein Profil/i })).toBeVisible(); await page.screenshot({ path: 'test-results/e2e/auth-session-valid.png' }); }); test('logout clears the session and redirects to /login', async ({ page }) => { await login(page); // Wait for hydration before interacting with the nav — onclick handlers are // only wired up after SvelteKit finishes hydrating the page client-side. await page.waitForSelector('[data-hydrated]'); // Logout is inside the user avatar dropdown — open it first. // Wait for the dropdown button to be visible before clicking Abmelden, // since the {#if userMenuOpen} block renders asynchronously in Svelte. await page.locator('button[aria-haspopup="true"]').click(); await expect(page.getByRole('button', { name: 'Abmelden' })).toBeVisible(); await page.getByRole('button', { name: 'Abmelden' }).click(); await expect(page).toHaveURL(/\/login/); // Confirm session is gone: navigating to / redirects back await page.goto('/'); await expect(page).toHaveURL(/\/login/); await page.screenshot({ path: 'test-results/e2e/logout.png' }); }); }); // ── Registration via invite code ──────────────────────────────────────────── // // J1 gap: register flow. Admin creates an invite, extracts its code, a new // browser context visits /register?code=…, fills the form, and the new user // can log in with the chosen password. test.describe('Registration via invite code', () => { // Admin session is provided by the shared storageState from auth.setup. test('admin creates invite, new user registers and can log in', async ({ page, request, browser }) => { test.setTimeout(60_000); const username = `e2e-reg-${stamp()}`; const password = 'RegPass99!'; // 1. Admin creates an invite via the API (simpler than UI automation for this step). const inviteRes = await request.post('/api/invites', { data: { label: `E2E reg test ${username}` } }); if (!inviteRes.ok()) throw new Error(`Create invite failed: ${inviteRes.status()}`); const invite = await inviteRes.json(); const inviteCode: string = invite.code ?? invite.id; // 2. Open /admin/invites and verify the invite appears in the table. await page.goto('/admin/invites'); await page.waitForSelector('[data-hydrated]'); await expect(page.getByText('E2E reg test')).toBeVisible({ timeout: 5000 }); await page.screenshot({ path: 'test-results/e2e/admin-invite-created.png' }); // 3. New user opens /register?code=… in a fresh context (no admin session). const freshCtx = await browser.newContext({ storageState: { cookies: [], origins: [] } }); const freshPage = await freshCtx.newPage(); await freshPage.goto(`/register?code=${inviteCode}`); // The form must load without the "invite only / invalid code" error block. await expect(freshPage.getByRole('button', { name: /Konto erstellen/i })).toBeVisible({ timeout: 10_000 }); // 4. Fill in the registration form. await freshPage.getByLabel(/E-Mail/i).fill(`${username}@example.com`); await freshPage.locator('input[name="password"]').fill(password); await freshPage.locator('input[id="passwordConfirm"]').fill(password); await freshPage.getByRole('button', { name: /Konto erstellen/i }).click(); // After successful registration the user is redirected (usually to / or /login). await freshPage.waitForURL((url) => !url.pathname.startsWith('/register'), { timeout: 15_000 }); await freshPage.screenshot({ path: 'test-results/e2e/register-success.png' }); await freshCtx.close(); }); });