Files
familienarchiv/frontend/e2e/auth.spec.ts
Marcel 20cceefbe1
Some checks failed
CI / Backend Unit Tests (pull_request) Failing after 3m23s
CI / Unit & Component Tests (pull_request) Failing after 3m23s
CI / OCR Service Tests (pull_request) Successful in 37s
CI / Unit & Component Tests (push) Failing after 3m36s
CI / OCR Service Tests (push) Successful in 35s
CI / Backend Unit Tests (push) Failing after 3m27s
test(e2e): add coverage for all 12 critical journeys (TEST-3 #405)
Adds docs/audits/e2e-coverage-report.md mapping all 12 critical journeys
to their test files. Fills the 6 coverage gaps with new e2e tests:

- J1: Register via invite code (auth.spec.ts)
- J3: Edit document tags via TagInput (documents.spec.ts)
- J4: Create brand-new tag via TagInput (documents.spec.ts)
- J5: Add SPOUSE_OF relationship on person edit page (persons.spec.ts)
- J6: Multi-filter search (text + date, text + tagId) (documents.spec.ts)
- J10: Notification bell opens dropdown (notification-deep-link.spec.ts)
- J11: Non-admin blocked from /admin/* (permissions.spec.ts)
- J12: Mass import trigger shows status (admin.spec.ts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 18:10:17 +02:00

143 lines
6.0 KiB
TypeScript

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();
});
});