import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import config from '../src/config.ts'; import { BASE, ADMIN_PASS, setupTestEnv, spawnServer, killServer, } from './helpers.ts'; let server: Awaited>; // The CSRF middleware reads `config.PUBLIC_HTTPS` / `config.PUBLIC_ORIGIN` per // request, so we toggle those between the dev-mode and HTTPS-mode describe // blocks rather than spinning up two servers. const ORIGINAL_PUBLIC_HTTPS = config.PUBLIC_HTTPS; const ORIGINAL_PUBLIC_ORIGIN = config.PUBLIC_ORIGIN; beforeAll(async () => { await setupTestEnv(); server = await spawnServer(); }); afterAll(async () => { await killServer(server); config.PUBLIC_HTTPS = ORIGINAL_PUBLIC_HTTPS; config.PUBLIC_ORIGIN = ORIGINAL_PUBLIC_ORIGIN; }); // `bun:test` runs describe blocks in source order, so the dev-mode block runs // first against the unmodified config, then we flip into HTTPS mode. describe('CSRF / Secure cookie — dev mode (http BASE_URL)', () => { test('starts with PUBLIC_HTTPS off', () => { expect(config.PUBLIC_HTTPS).toBe(false); }); test('POST with no Origin is allowed (non-browser path)', async () => { const r = await fetch(`${BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'username=admin&password=wrong', redirect: 'manual', }); expect(r.status).not.toBe(403); }); test('POST with same-origin Origin is allowed', async () => { const r = await fetch(`${BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: BASE, }, body: 'username=admin&password=wrong', redirect: 'manual', }); expect(r.status).not.toBe(403); }); test('POST with mismatched Origin is rejected', async () => { const r = await fetch(`${BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: 'http://attacker.example', }, body: 'username=admin&password=wrong', redirect: 'manual', }); expect(r.status).toBe(403); }); test('successful login Set-Cookie omits Secure', async () => { const r = await fetch(`${BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `username=admin&password=${encodeURIComponent(ADMIN_PASS)}`, redirect: 'manual', }); expect(r.status).toBe(302); const cookie = r.headers.get('set-cookie') ?? ''; expect(cookie).toContain('session='); expect(cookie).not.toContain('Secure'); }); test('responses do not include Strict-Transport-Security', async () => { const r = await fetch(`${BASE}/health`); expect(r.headers.get('strict-transport-security')).toBeNull(); }); }); describe('CSRF / Secure cookie — HTTPS mode (https BASE_URL)', () => { beforeAll(() => { // Simulate `BASE_URL=https://forge.test`. Note that the test client still // talks to the server over plain HTTP on localhost — that's the whole // point of the reverse-proxy story: the app trusts BASE_URL, not the // transport it sees on the proxy↔app hop. config.PUBLIC_HTTPS = true; config.PUBLIC_ORIGIN = 'https://forge.test'; }); test('POST with no Origin is allowed (non-browser path)', async () => { const r = await fetch(`${BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: 'username=admin&password=wrong', redirect: 'manual', }); expect(r.status).not.toBe(403); }); test('POST with matching public Origin is allowed', async () => { const r = await fetch(`${BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: 'https://forge.test', }, body: 'username=admin&password=wrong', redirect: 'manual', }); expect(r.status).not.toBe(403); }); test('POST whose Origin only matches Host (not BASE_URL) is rejected', async () => { // Stricter than dev mode: `Origin: ${BASE}` (http://localhost:PORT) would // pass the Host-match check but must fail the BASE_URL check. const r = await fetch(`${BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: BASE, }, body: 'username=admin&password=wrong', redirect: 'manual', }); expect(r.status).toBe(403); }); test('POST with attacker Origin is rejected', async () => { const r = await fetch(`${BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: 'https://attacker.example', }, body: 'username=admin&password=wrong', redirect: 'manual', }); expect(r.status).toBe(403); }); test('successful login Set-Cookie includes Secure', async () => { const r = await fetch(`${BASE}/login`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: 'https://forge.test', }, body: `username=admin&password=${encodeURIComponent(ADMIN_PASS)}`, redirect: 'manual', }); expect(r.status).toBe(302); const cookie = r.headers.get('set-cookie') ?? ''; expect(cookie).toContain('session='); expect(cookie).toContain('Secure'); }); test('responses include Strict-Transport-Security', async () => { const r = await fetch(`${BASE}/health`); expect(r.headers.get('strict-transport-security')).toBe( 'max-age=31536000; includeSubDomains', ); }); });