/** * Tests for server/middleware/verifyCsrf.js * * CSRF protection via the double-submit cookie pattern. These tests verify * that the timing-safe comparison works correctly and that safe HTTP methods * are correctly exempted. */ import verifyCsrf from '../../server/middleware/verifyCsrf.js'; function makeReq(method, cookieToken, headerToken) { return { method, cookies: { csrf_token: cookieToken }, headers: { 'x-csrf-token': headerToken } }; } function makeRes() { const res = { statusCode: null, body: null, status(code) { this.statusCode = code; return this; }, json(body) { this.body = body; return this; } }; return res; } describe('verifyCsrf middleware', () => { describe('safe methods are exempted', () => { for (const method of ['GET', 'HEAD', 'OPTIONS']) { it(`allows ${method} with no CSRF token`, () => { const next = vi.fn(); verifyCsrf(makeReq(method, undefined, undefined), makeRes(), next); expect(next).toHaveBeenCalledOnce(); }); } }); describe('mutating methods require valid token', () => { const TOKEN = 'a'.repeat(64); // 64 hex chars = 32 bytes for (const method of ['POST', 'PUT', 'PATCH', 'DELETE']) { it(`allows ${method} with matching tokens`, () => { const next = vi.fn(); const res = makeRes(); verifyCsrf(makeReq(method, TOKEN, TOKEN), res, next); expect(next).toHaveBeenCalledOnce(); expect(res.statusCode).toBeNull(); }); it(`blocks ${method} with mismatched tokens`, () => { const next = vi.fn(); const res = makeRes(); verifyCsrf(makeReq(method, TOKEN, TOKEN.replace('a', 'b')), res, next); expect(res.statusCode).toBe(403); expect(next).not.toHaveBeenCalled(); }); it(`blocks ${method} with missing cookie token`, () => { const next = vi.fn(); const res = makeRes(); verifyCsrf(makeReq(method, undefined, TOKEN), res, next); expect(res.statusCode).toBe(403); expect(res.body.error).toBe('CSRF token missing'); }); it(`blocks ${method} with missing header token`, () => { const next = vi.fn(); const res = makeRes(); verifyCsrf(makeReq(method, TOKEN, undefined), res, next); expect(res.statusCode).toBe(403); }); } it('blocks when tokens have different lengths (timing-safe path)', () => { const next = vi.fn(); const res = makeRes(); verifyCsrf(makeReq('POST', 'short', 'much-longer-token-here'), res, next); expect(res.statusCode).toBe(403); expect(res.body.error).toBe('CSRF token invalid'); }); }); });