import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { BASE, ADMIN_PASS, setupTestEnv, spawnServer, killServer, } from './helpers.ts'; let server: Awaited>; // The Go server derives PUBLIC_HTTPS / PUBLIC_ORIGIN from BASE_URL at // startup, so the HTTPS-mode block restarts the server with a different // BASE_URL rather than mutating config in process. beforeAll(async () => { await setupTestEnv(); server = await spawnServer(); }); afterAll(async () => { await killServer(server); }); // `bun:test` runs describe blocks in source order, so the dev-mode block runs // first against the default BASE_URL, then we restart in HTTPS mode. describe('CSRF / Secure cookie — dev mode (http BASE_URL)', () => { test('starts in dev mode (no HSTS header)', async () => { // PUBLIC_HTTPS is not readable out of process. The HSTS header is the // observable signal that the server is in plain-http mode. const r = await fetch(`${BASE}/health`); expect(r.headers.get('strict-transport-security')).toBeNull(); }); 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(async () => { // Restart with `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. await killServer(server); server = await spawnServer({ BASE_URL: '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', ); }); });