ghostguild-org/tests/server/middleware/csrf.test.js
Jennie Robinson Faber 29c96a207e Add Vitest security test suite and update security evaluation doc
Set up Vitest with server (node) and client (jsdom) test projects.
79 tests across 8 files verify all Phase 0-1 security controls:
escapeHtml sanitization, DOMPurify markdown XSS prevention, CSRF
enforcement, security headers, rate limiting, auth guards, profile
field allowlist, and login anti-enumeration. Updated SECURITY_EVALUATION.md
with remediation status, implementation summary, and automated test
coverage details.
2026-03-01 12:30:06 +00:00

127 lines
4 KiB
JavaScript

import { describe, it, expect, beforeEach } from 'vitest'
import { createMockEvent } from '../helpers/createMockEvent.js'
// Import the handler — defineEventHandler is a global passthrough,
// so the default export is the raw handler function.
import csrfMiddleware from '../../../server/middleware/01.csrf.js'
describe('CSRF middleware', () => {
describe('safe methods bypass', () => {
it.each(['GET', 'HEAD', 'OPTIONS'])('%s requests pass through', (method) => {
const event = createMockEvent({ method, path: '/api/test' })
expect(() => csrfMiddleware(event)).not.toThrow()
})
})
describe('non-API paths bypass', () => {
it('POST to non-/api/ path passes through', () => {
const event = createMockEvent({ method: 'POST', path: '/some/page' })
expect(() => csrfMiddleware(event)).not.toThrow()
})
})
describe('exempt routes bypass', () => {
it.each([
'/api/helcim/webhook',
'/api/slack/webhook',
'/api/auth/verify'
])('POST to %s passes through', (path) => {
const event = createMockEvent({ method: 'POST', path })
expect(() => csrfMiddleware(event)).not.toThrow()
})
})
describe('CSRF enforcement on state-changing API requests', () => {
it('throws 403 when POST to /api/* has no x-csrf-token header', () => {
const event = createMockEvent({
method: 'POST',
path: '/api/members/profile',
cookies: { 'csrf-token': 'abc123' }
})
expect(() => csrfMiddleware(event)).toThrowError(
expect.objectContaining({
statusCode: 403,
statusMessage: 'CSRF token missing or invalid'
})
)
})
it('throws 403 when header and cookie tokens do not match', () => {
const event = createMockEvent({
method: 'POST',
path: '/api/members/profile',
cookies: { 'csrf-token': 'cookie-token' },
headers: { 'x-csrf-token': 'different-token' }
})
expect(() => csrfMiddleware(event)).toThrowError(
expect.objectContaining({
statusCode: 403,
statusMessage: 'CSRF token missing or invalid'
})
)
})
it('passes when header and cookie tokens match', () => {
const token = 'matching-token-value'
const event = createMockEvent({
method: 'POST',
path: '/api/members/profile',
cookies: { 'csrf-token': token },
headers: { 'x-csrf-token': token }
})
expect(() => csrfMiddleware(event)).not.toThrow()
})
it('enforces CSRF on DELETE requests', () => {
const event = createMockEvent({
method: 'DELETE',
path: '/api/admin/events/123',
cookies: { 'csrf-token': 'abc' }
})
expect(() => csrfMiddleware(event)).toThrowError(
expect.objectContaining({ statusCode: 403 })
)
})
it('enforces CSRF on PATCH requests', () => {
const event = createMockEvent({
method: 'PATCH',
path: '/api/members/profile',
cookies: { 'csrf-token': 'abc' }
})
expect(() => csrfMiddleware(event)).toThrowError(
expect.objectContaining({ statusCode: 403 })
)
})
})
describe('cookie provisioning', () => {
it('sets csrf-token cookie when none exists', () => {
const event = createMockEvent({ method: 'GET', path: '/api/test' })
csrfMiddleware(event)
// The middleware calls setCookie which sets a Set-Cookie header
const setCookieHeader = event._testSetHeaders['set-cookie']
expect(setCookieHeader).toBeDefined()
expect(setCookieHeader).toContain('csrf-token=')
})
it('does not set a new cookie when one already exists', () => {
const event = createMockEvent({
method: 'GET',
path: '/api/test',
cookies: { 'csrf-token': 'existing-token' }
})
csrfMiddleware(event)
const setCookieHeader = event._testSetHeaders['set-cookie']
// Should not have set a new cookie
expect(setCookieHeader).toBeUndefined()
})
})
})