/** * USER_JOURNEYS.md §3 (recovery on a new device). Uses the standalone * /recover route as well as the inline-recovery flow on /join. */ import { test, expect } from '../../fixtures/test'; import { RecoverPage } from '../../page-objects'; import { clearAllStorage } from '../../helpers/storage-helpers'; test.describe('Auth — /recover route', () => { test('happy path: correct name + PIN → /feed', async ({ page, guest }) => { const handle = await guest('Greta'); await clearAllStorage(page); const recover = new RecoverPage(page); await recover.goto(); await recover.recover('Greta', handle.pin); await page.waitForURL('**/feed'); }); test('wrong PIN shows the localized error', async ({ page, guest }) => { const handle = await guest('Hans'); await clearAllStorage(page); const recover = new RecoverPage(page); await recover.goto(); await recover.recover('Hans', handle.pin === '9999' ? '0000' : '9999'); await expect(recover.errorMessage).toContainText(/PIN ist falsch|falsch/i); }); test('non-existent name returns the "not found" error', async ({ page }) => { await clearAllStorage(page); const recover = new RecoverPage(page); await recover.goto(); await recover.recover('Doesnt-Exist-' + Date.now(), '1234'); await expect(recover.errorMessage).toContainText(/nicht gefunden|kein/i); }); test('lockout expires and counter resets (per recover handler)', async ({ api, guest, db }) => { const handle = await guest('Ida'); await db.lockUserPin(handle.userId, 15); // Direct API call: even the correct PIN must fail while locked. await api.recover('Ida', handle.pin, { expectedStatus: [429] }); // Now move the lockout into the past. await db.lockUserPin(handle.userId, -1); // Correct PIN must succeed, AND the counter must be reset so the next wrong attempt isn't immediately re-locked. const { body } = await api.recover('Ida', handle.pin, { expectedStatus: [200] }); expect(body.jwt).toBeTruthy(); }); });