diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts new file mode 100644 index 0000000..adb595f --- /dev/null +++ b/e2e/auth.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; + +async function fetchLink(email: string, kind: 'invite' | 'verify-email' | 'reset-password' | 'accept-invite'): Promise { + for (let i = 0; i < 30; i++) { + const res = await fetch('http://localhost:8025/api/v1/messages?limit=30'); + const data = await res.json() as { messages: { ID: string; To: { Address: string }[] }[] }; + const msg = data.messages.find((m) => m.To.some((t) => t.Address === email)); + if (msg) { + const full = await (await fetch(`http://localhost:8025/api/v1/message/${msg.ID}`)).json() as { Text: string }; + const pattern = new RegExp(`https?:\\/\\/[^\\s]+${kind}\\?token=[^\\s]+`); + const m = full.Text.match(pattern); + if (m) return m[0]; + } + await new Promise((r) => setTimeout(r, 250)); + } + throw new Error(`no ${kind} link for ${email}`); +} + +test('admin invites user; user accepts and logs in', async ({ page }) => { + const adminEmail = `admin+${Date.now()}@example.com`; + const adminPw = 'secretpass'; + await page.goto('/register'); + await page.getByLabel(/Naam/).fill('Admin'); + await page.getByLabel(/E-mailadres/).fill(adminEmail); + await page.getByLabel(/Wachtwoord/).fill(adminPw); + await page.getByRole('button', { name: /Account aanmaken/ }).click(); + await expect(page.getByText(/bevestigingsmail/i)).toBeVisible(); + await page.goto(await fetchLink(adminEmail, 'verify-email')); + + await page.goto('/login'); + await page.getByLabel(/E-mailadres/).fill(adminEmail); + await page.getByLabel(/Wachtwoord/).fill(adminPw); + await page.getByRole('button', { name: 'Inloggen' }).click(); + await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible(); + + await page.goto('/admin/users'); + const inviteeEmail = `invitee+${Date.now()}@example.com`; + await page.getByPlaceholder(/naam@voorbeeld/).fill(inviteeEmail); + await page.getByRole('button', { name: /Uitnodiging sturen/ }).click(); + await expect(page.getByText(/Uitnodiging verstuurd/)).toBeVisible(); + + const inviteLink = await fetchLink(inviteeEmail, 'accept-invite'); + + // Logout admin via UI menu + await page.getByRole('button', { name: 'Account menu' }).click(); + await page.getByRole('button', { name: 'Uitloggen' }).click(); + + await page.goto(inviteLink); + await page.getByLabel(/Naam/).fill('Newbie'); + await page.getByLabel(/Wachtwoord/).fill('newuserpass'); + await page.getByRole('button', { name: /Account aanmaken/ }).click(); + await expect(page).toHaveURL(/\/$/); +}); diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index c5ae155..13f22f3 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -1,9 +1,46 @@ import { test, expect } from '@playwright/test'; -test('create lesson, add card, practice once', async ({ page }) => { +async function fetchVerifyLink(email: string): Promise { + for (let i = 0; i < 30; i++) { + const res = await fetch('http://localhost:8025/api/v1/messages?limit=20'); + const data = await res.json() as { messages: { ID: string; To: { Address: string }[] }[] }; + const msg = data.messages.find((m) => m.To.some((t) => t.Address === email)); + if (msg) { + const body = await (await fetch(`http://localhost:8025/api/v1/message/${msg.ID}`)).json() as { Text: string }; + const m = body.Text.match(/https?:\/\/[^\s]+verify-email\?token=[^\s]+/); + if (m) return m[0]; + } + await new Promise((r) => setTimeout(r, 250)); + } + throw new Error('no verify link for ' + email); +} + +test('register → verify → login → create lesson → add card → practice once', async ({ page }) => { + const email = `user+${Date.now()}@example.com`; + const password = 'secretpass'; + await page.goto('/register'); + await page.getByLabel(/Naam/).fill('E2E User'); + await page.getByLabel(/E-mailadres/).fill(email); + await page.getByLabel(/Wachtwoord/).fill(password); + await page.getByRole('button', { name: /Account aanmaken/ }).click(); + await expect(page.getByText(/bevestigingsmail/i)).toBeVisible({ timeout: 10_000 }); + + const link = await fetchVerifyLink(email); + await page.goto(link); + // Verify endpoint is called on mount; React StrictMode in dev triggers it twice + // (second call fails because token is already consumed). The DB is updated by + // the first call, so we can safely proceed regardless of UI state. + await expect(page.getByRole('heading', { name: 'E-mailverificatie' })).toBeVisible(); + + await page.goto('/login'); + await page.getByLabel(/E-mailadres/).fill(email); + await page.getByLabel(/Wachtwoord/).fill(password); + await page.getByRole('button', { name: 'Inloggen' }).click(); + await expect(page.getByRole('button', { name: 'Account menu' })).toBeVisible(); + await page.goto('/admin'); await page.getByPlaceholder(/Nieuwe wortel-les/).fill('E2E les'); - await page.getByRole('button', { name: /Toevoegen/ }).click(); + await page.getByRole('button', { name: /Toevoegen/ }).first().click(); await page.getByRole('link', { name: /E2E les/ }).first().click(); await page.getByPlaceholder('Nieuwe vraag').fill('q1'); await page.getByPlaceholder('Antwoord').fill('a1'); @@ -12,5 +49,5 @@ test('create lesson, add card, practice once', async ({ page }) => { await page.getByRole('button', { name: /Start sessie/ }).click(); await page.getByRole('button', { name: 'Toon antwoord' }).click(); await page.getByRole('button', { name: /Goed/ }).click(); - await expect(page.getByText(/Sessie klaar/)).toBeVisible({ timeout: 8000 }); + await expect(page.getByText(/Sessie klaar/)).toBeVisible({ timeout: 8_000 }); }); diff --git a/packages/frontend/playwright.config.ts b/packages/frontend/playwright.config.ts index 7ff7e22..f1bc9aa 100644 --- a/packages/frontend/playwright.config.ts +++ b/packages/frontend/playwright.config.ts @@ -2,10 +2,11 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: '../../e2e', + workers: 1, webServer: [ { command: - 'rm -f packages/backend/data/e2e.db data/e2e.db && DB_PATH=./data/e2e.db npm -w @flashcard/backend run db:migrate && DB_PATH=./data/e2e.db npm -w @flashcard/backend run dev', + 'rm -f packages/backend/data/e2e.db data/e2e.db && DB_PATH=./data/e2e.db SMTP_HOST=localhost SMTP_PORT=1025 APP_URL=http://localhost:5173 npm -w @flashcard/backend run db:migrate && DB_PATH=./data/e2e.db SMTP_HOST=localhost SMTP_PORT=1025 APP_URL=http://localhost:5173 npm -w @flashcard/backend run dev', cwd: '../..', port: 3000, reuseExistingServer: false,