import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import Fastify, { FastifyInstance } from 'fastify'; import helmet from '@fastify/helmet'; import { escapeHtml, sanitizeRequestUrlForLog, SERVER_HELMET_CONFIG, } from '../src/server'; describe('Server Security Configuration', () => { describe('request URL log sanitization', () => { it('should redact sensitive query params without removing non-sensitive context', () => { expect( sanitizeRequestUrlForLog( '/api/sync/ws?token=secret-jwt&clientId=B_AEh6&limit=10', ), ).toBe('/api/sync/ws?token=redacted&clientId=B_AEh6&limit=10'); }); it('should redact sensitive query params case-insensitively', () => { expect(sanitizeRequestUrlForLog('/reset-password?resetPasswordToken=secret')).toBe( '/reset-password?resetPasswordToken=redacted', ); }); }); describe('Content Security Policy', () => { let app: FastifyInstance; beforeEach(async () => { app = Fastify(); }); afterEach(async () => { if (app) { await app.close(); } }); it('should include CSP headers in response', async () => { await app.register(helmet, SERVER_HELMET_CONFIG); app.get('/test', async () => ({ status: 'ok' })); await app.ready(); const response = await app.inject({ method: 'GET', url: '/test', }); // Check that CSP header is present const cspHeader = response.headers['content-security-policy']; expect(cspHeader).toBeDefined(); // Verify key CSP directives expect(cspHeader).toContain("default-src 'self'"); expect(cspHeader).toContain("script-src 'self'"); expect(cspHeader).toContain("object-src 'none'"); expect(cspHeader).toContain("frame-ancestors 'none'"); }); it('should include X-Frame-Options header', async () => { await app.register(helmet); app.get('/test', async () => ({ status: 'ok' })); await app.ready(); const response = await app.inject({ method: 'GET', url: '/test', }); // Helmet sets X-Frame-Options by default expect(response.headers['x-frame-options']).toBeDefined(); }); it('should include X-Content-Type-Options header', async () => { await app.register(helmet); app.get('/test', async () => ({ status: 'ok' })); await app.ready(); const response = await app.inject({ method: 'GET', url: '/test', }); expect(response.headers['x-content-type-options']).toBe('nosniff'); }); }); describe('HTML Escape Function', () => { it('should escape < and > characters', () => { const input = ''; const escaped = escapeHtml(input); expect(escaped).toBe('<script>alert("xss")</script>'); expect(escaped).not.toContain('<'); expect(escaped).not.toContain('>'); }); it('should escape ampersand', () => { const input = 'Tom & Jerry'; const escaped = escapeHtml(input); expect(escaped).toBe('Tom & Jerry'); }); it('should escape double quotes', () => { const input = 'He said "hello"'; const escaped = escapeHtml(input); expect(escaped).toBe('He said "hello"'); }); it('should escape single quotes', () => { const input = "It's a test"; const escaped = escapeHtml(input); expect(escaped).toBe('It's a test'); }); it('should handle multiple special characters', () => { const input = '