/** * USER_JOURNEYS.md §1 (First-time guest), §2 (Returning guest, same device), * §3 (Returning guest, new device). Covers the happy path through * /join, the PIN modal, the onboarding overlay landing, and the * name-already-taken recovery transformation. */ import { test, expect } from '../../fixtures/test'; import { JoinPage } from '../../page-objects'; import { readStorage, STORAGE_KEYS, clearAllStorage } from '../../helpers/storage-helpers'; test.describe('Auth — join flow', () => { test('happy path: name → PIN modal → feed @smoke', async ({ page }) => { const join = new JoinPage(page); await join.goto(); await expect(page.getByRole('heading', { name: 'Willkommen!' })).toBeVisible(); const { pin } = await join.joinAs('Alice'); expect(pin).toMatch(/^\d{4}$/); // PIN copy button toggles to "Kopiert!" on click await join.pinCopyButton.click(); await expect(join.pinCopyButton).toHaveText(/Kopiert/i); await join.continueToFeed(); await expect(page).toHaveURL(/\/feed$/); const storage = await readStorage(page); expect(storage.jwt, 'JWT in localStorage').toMatch(/^eyJ/); expect(storage.pin).toBe(pin); expect(storage.userId).toMatch(/^[0-9a-f-]{36}$/); expect(storage.displayName).toBe('Alice'); }); test('returning guest with valid JWT is redirected to /feed', async ({ page, guest, signIn }) => { const alice = await guest('Bob'); await signIn(page, alice); // Now visit the root — should auto-redirect to /feed. await page.goto('/'); await page.waitForURL('**/feed', { timeout: 5_000 }); }); test('returning guest, new device: same name shows the inline recovery form', async ({ page, guest }) => { const original = await guest('Charlie'); // Brand-new browser context (cleared storage) — landing on /join with same name await clearAllStorage(page); const join = new JoinPage(page); await join.goto(); await join.fillName('Charlie'); await join.submit(); await expect(join.recoveryPinInput).toBeVisible(); await expect(page.getByText(/Charlie.*bereits vergeben/)).toBeVisible(); // Type correct PIN → land on /feed with a new JWT await join.recoveryPinInput.fill(original.pin); await join.recoverySubmit.click(); await page.waitForURL('**/feed'); const storage = await readStorage(page); expect(storage.userId).toBe(original.userId); expect(storage.pin).toBe(original.pin); }); test('wrong PIN three times locks the account for 15 minutes', async ({ page, guest, db }) => { const dave = await guest('Dave'); await clearAllStorage(page); const join = new JoinPage(page); await join.goto(); await join.fillName('Dave'); await join.submit(); await expect(join.recoveryPinInput).toBeVisible(); // Wrong PIN (real one is dave.pin) const wrong = dave.pin === '0000' ? '1111' : '0000'; for (let i = 0; i < 3; i++) { await join.recoveryPinInput.fill(wrong); await join.recoverySubmit.click(); await expect(join.recoveryError).toBeVisible(); } // Fourth attempt should hit the 429 lockout (even with the correct PIN now) await join.recoveryPinInput.fill(dave.pin); await join.recoverySubmit.click(); await expect(join.recoveryError).toContainText(/15 Minuten/); // Sanity: DB row reflects the lock // (The handler sets pin_locked_until directly — verify via API "recover" returning 429) void db; // unused for now, documenting that db.lockUserPin exists if we want shortcut path }); test('"Anderen Namen wählen" returns to the normal join form', async ({ page, guest }) => { await guest('Eve'); await clearAllStorage(page); const join = new JoinPage(page); await join.goto(); await join.fillName('Eve'); await join.submit(); await expect(join.recoveryPinInput).toBeVisible(); await join.tryDifferentNameButton.click(); await expect(join.nameInput).toBeVisible(); await expect(join.recoveryPinInput).not.toBeVisible(); }); test('"Ich habe bereits einen Account" link routes to /recover', async ({ page }) => { const join = new JoinPage(page); await join.goto(); await join.linkToRecover.click(); await expect(page).toHaveURL(/\/recover$/); }); test('JWT and PIN keys are exactly the ones auth.ts expects', async ({ page, guest, signIn }) => { const handle = await guest('Frank'); await signIn(page, handle); // Read raw localStorage to make sure no test accidentally uses a different key. const raw = await page.evaluate(() => ({ jwt: localStorage.getItem('eventsnap_jwt'), pin: localStorage.getItem('eventsnap_pin'), userId: localStorage.getItem('eventsnap_user_id'), displayName: localStorage.getItem('eventsnap_display_name'), })); expect(raw.jwt).toBe(handle.jwt); expect(raw.pin).toBe(handle.pin); expect(raw.userId).toBe(handle.userId); expect(raw.displayName).toBe('Frank'); // Sanity: the keys we just checked match STORAGE_KEYS in storage-helpers.ts expect(STORAGE_KEYS.jwt).toBe('eventsnap_jwt'); }); });