import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import Fastify, { FastifyInstance } from 'fastify'; import helmet from '@fastify/helmet'; describe('Server Security Configuration', () => { 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 () => { // Register helmet with the same config as the server await app.register(helmet, { contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:'], fontSrc: ["'self'"], objectSrc: ["'none'"], frameAncestors: ["'none'"], formAction: ["'self'"], baseUri: ["'self'"], }, }, }); 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', () => { // Test the escapeHtml function that prevents XSS in templates const escapeHtml = (unsafe: string): string => { return unsafe .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; 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 = '
content
'; const escaped = escapeHtml(input); expect(escaped).toBe( '<div class="test" data-value='a & b'>content</div>', ); }); it('should handle empty string', () => { expect(escapeHtml('')).toBe(''); }); it('should handle string with no special characters', () => { const input = 'Hello World'; expect(escapeHtml(input)).toBe('Hello World'); }); it('should escape quotes to prevent attribute injection', () => { // An attacker might try to break out of an attribute and add an event handler const input = '" onmouseover="alert(1)"'; const escaped = escapeHtml(input); // The quotes are escaped, so even though 'onmouseover' appears, it's harmless text // because the quote before it is escaped and won't break out of the attribute expect(escaped).toBe('" onmouseover="alert(1)"'); // The key protection is that " is escaped to " expect(escaped).not.toContain('"'); }); }); }); describe('Password Reset Page', () => { let app: FastifyInstance; beforeEach(async () => { vi.resetModules(); }); afterEach(async () => { if (app) { await app.close(); } }); it('should render password reset form with token', async () => { const { pageRoutes } = await import('../src/pages'); app = Fastify(); await app.register(pageRoutes, { prefix: '/' }); await app.ready(); const response = await app.inject({ method: 'GET', url: '/reset-password?token=test-token-123', }); expect(response.statusCode).toBe(200); expect(response.headers['content-type']).toContain('text/html'); const html = response.body; expect(html).toContain('Reset Password'); expect(html).toContain('
'); expect(html).toContain('type="password"'); expect(html).toContain('Minimum 12 characters'); // Token should be escaped in the JavaScript expect(html).toContain('test-token-123'); }); it('should return 400 when token is missing', async () => { const { pageRoutes } = await import('../src/pages'); app = Fastify(); await app.register(pageRoutes, { prefix: '/' }); await app.ready(); const response = await app.inject({ method: 'GET', url: '/reset-password', }); expect(response.statusCode).toBe(400); expect(response.body).toBe('Token is required'); }); it('should escape malicious token in data attribute', async () => { const { pageRoutes } = await import('../src/pages'); app = Fastify(); await app.register(pageRoutes, { prefix: '/' }); await app.ready(); // Test attribute breakout attempt - double quotes should be escaped const maliciousToken = '">'; const response = await app.inject({ method: 'GET', url: `/reset-password?token=${encodeURIComponent(maliciousToken)}`, }); expect(response.statusCode).toBe(200); const html = response.body; // Token is in data-token attribute, escapeHtml prevents attribute breakout expect(html).toContain('data-token="'); // The raw attack string should not appear unescaped expect(html).not.toContain('">'; const response = await app.inject({ method: 'GET', url: `/reset-password?token=${encodeURIComponent(maliciousToken)}`, }); expect(response.statusCode).toBe(200); const html = response.body; // escapeHtml escapes < as < to prevent injection expect(html).not.toContain('