import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { chromium } from 'playwright'; import type { Browser } from 'playwright'; import { BASE, ADMIN_PASS, setupTestEnv, spawnServer, killServer, login, logout, } from './helpers.ts'; let browser: Browser; let server: Awaited>; beforeAll(async () => { await setupTestEnv(); server = await spawnServer(); browser = await chromium.launch(); }); afterAll(async () => { await browser.close(); await killServer(server); }); // ─── Auth ───────────────────────────────────────────────────────────────────── describe('auth', () => { test('homepage loads', async () => { const ctx = await browser.newContext(); const page = await ctx.newPage(); try { await page.goto(BASE); expect(await page.title()).toContain('Hearthforge'); } finally { await ctx.close(); } }); test('wrong password shows error', async () => { const ctx = await browser.newContext(); const page = await ctx.newPage(); try { await page.goto(`${BASE}/login`); await page.fill('[name=username]', 'admin'); await page.fill('[name=password]', 'wrongpassword'); await page.click('button[type=submit]'); expect(await page.locator('.form-error').textContent()).toContain('Invalid'); } finally { await ctx.close(); } }); test('correct credentials redirect to homepage', async () => { const ctx = await browser.newContext(); const page = await ctx.newPage(); try { await login(page); expect(page.url()).toBe(BASE + '/'); expect(await page.locator('.nav-user').isVisible()).toBe(true); } finally { await ctx.close(); } }); test('register new user', async () => { const ctx = await browser.newContext(); const page = await ctx.newPage(); try { await page.goto(`${BASE}/register`); await page.fill('[name=username]', 'alice'); await page.fill('[name=password]', 'password123'); await page.fill('[name=password2]', 'password123'); await page.click('button[type=submit]'); await page.waitForURL(BASE + '/'); expect(await page.locator('.nav-user').textContent()).toBe('alice'); } finally { await ctx.close(); } }); test('register with mismatched passwords shows error', async () => { const ctx = await browser.newContext(); const page = await ctx.newPage(); try { await page.goto(`${BASE}/register`); await page.fill('[name=username]', 'bob'); await page.fill('[name=password]', 'password123'); await page.fill('[name=password2]', 'different456'); await page.click('button[type=submit]'); expect(await page.locator('.form-error').textContent()).toContain('match'); } finally { await ctx.close(); } }); test('register with duplicate username shows error', async () => { const ctx = await browser.newContext(); const page = await ctx.newPage(); try { await page.goto(`${BASE}/register`); await page.fill('[name=username]', 'alice'); // already registered above await page.fill('[name=password]', 'password123'); await page.fill('[name=password2]', 'password123'); await page.click('button[type=submit]'); expect(await page.locator('.form-error').textContent()).toContain('taken'); } finally { await ctx.close(); } }); test('cross-origin POST is rejected (CSRF defense)', async () => { const ctx = await browser.newContext(); try { // Forge an Origin from a different host; server should refuse the POST. const resp = await ctx.request.post(`${BASE}/login`, { headers: { Origin: 'http://evil.example' }, form: { username: 'admin', password: ADMIN_PASS }, maxRedirects: 0, }); expect(resp.status()).toBe(403); // Other tests (login, register) cover the same-origin success path. } finally { await ctx.close(); } }); test('logout clears session', async () => { const ctx = await browser.newContext(); const page = await ctx.newPage(); try { await login(page); await logout(page); expect(await page.locator('.nav-user').count()).toBe(0); expect(await page.locator('a[href="/login"]').isVisible()).toBe(true); } finally { await ctx.close(); } }); });