/** * Security enhancements tests * Tests for rate limiting, input sanitization, and security headers * * Requirements: 9.3, 9.4, 9.5 */ import { describe, it, expect, beforeEach, vi } from 'vitest' import { checkRateLimit, resetRateLimit, LOGIN_RATE_LIMIT, REGISTER_RATE_LIMIT, INVITATION_RATE_LIMIT, getClientIdentifier } from '../rate-limit' import { sanitizeString, sanitizeEmail, sanitizeHtml, sanitizeSqlInput, validateEmail, validatePassword } from '../validation' describe('Rate Limiting', () => { beforeEach(() => { // Reset rate limits before each test resetRateLimit('test:key') resetRateLimit('login:test@example.com') resetRateLimit('register:192.168.1.1') }) describe('checkRateLimit', () => { it('should allow requests within the limit', () => { const result = checkRateLimit('test:key', { maxRequests: 5, windowMs: 60000, }) expect(result.allowed).toBe(true) expect(result.remaining).toBe(4) }) it('should block requests exceeding the limit', () => { const config = { maxRequests: 3, windowMs: 60000, } // Make 3 requests (should all succeed) for (let i = 0; i < 3; i++) { const result = checkRateLimit('test:key', config) expect(result.allowed).toBe(true) } // 4th request should be blocked const result = checkRateLimit('test:key', config) expect(result.allowed).toBe(false) expect(result.remaining).toBe(0) expect(result.retryAfter).toBeGreaterThan(0) }) it('should reset after window expires', () => { const config = { maxRequests: 2, windowMs: 100, // 100ms window } // Make 2 requests checkRateLimit('test:key', config) checkRateLimit('test:key', config) // 3rd request should be blocked let result = checkRateLimit('test:key', config) expect(result.allowed).toBe(false) // Wait for window to expire return new Promise((resolve) => { setTimeout(() => { // Should be allowed again result = checkRateLimit('test:key', config) expect(result.allowed).toBe(true) resolve() }, 150) }) }) it('should track different keys separately', () => { const config = { maxRequests: 2, windowMs: 60000, } // Make 2 requests for key1 checkRateLimit('test:key1', config) checkRateLimit('test:key1', config) // key1 should be blocked let result = checkRateLimit('test:key1', config) expect(result.allowed).toBe(false) // key2 should still be allowed result = checkRateLimit('test:key2', config) expect(result.allowed).toBe(true) }) }) describe('resetRateLimit', () => { it('should reset rate limit for a key', () => { const config = { maxRequests: 2, windowMs: 60000, } // Exhaust the limit checkRateLimit('test:key', config) checkRateLimit('test:key', config) // Should be blocked let result = checkRateLimit('test:key', config) expect(result.allowed).toBe(false) // Reset the limit resetRateLimit('test:key') // Should be allowed again result = checkRateLimit('test:key', config) expect(result.allowed).toBe(true) }) }) describe('Rate limit configurations', () => { it('should have appropriate login rate limit', () => { expect(LOGIN_RATE_LIMIT.maxRequests).toBe(5) expect(LOGIN_RATE_LIMIT.windowMs).toBe(15 * 60 * 1000) // 15 minutes }) it('should have appropriate registration rate limit', () => { expect(REGISTER_RATE_LIMIT.maxRequests).toBe(3) expect(REGISTER_RATE_LIMIT.windowMs).toBe(60 * 60 * 1000) // 1 hour }) it('should have appropriate invitation rate limit', () => { expect(INVITATION_RATE_LIMIT.maxRequests).toBe(10) expect(INVITATION_RATE_LIMIT.windowMs).toBe(60 * 60 * 1000) // 1 hour }) }) describe('getClientIdentifier', () => { it('should extract IP from X-Forwarded-For header', () => { const headers = new Headers() headers.set('x-forwarded-for', '192.168.1.1, 10.0.0.1') const identifier = getClientIdentifier(headers) expect(identifier).toBe('192.168.1.1') }) it('should extract IP from X-Real-IP header', () => { const headers = new Headers() headers.set('x-real-ip', '192.168.1.1') const identifier = getClientIdentifier(headers) expect(identifier).toBe('192.168.1.1') }) it('should return unknown when no IP headers present', () => { const headers = new Headers() const identifier = getClientIdentifier(headers) expect(identifier).toBe('unknown') }) }) }) describe('Input Sanitization', () => { describe('sanitizeString', () => { it('should trim whitespace', () => { expect(sanitizeString(' hello ')).toBe('hello') expect(sanitizeString('\n\thello\t\n')).toBe('hello') }) it('should remove null bytes', () => { expect(sanitizeString('hello\0world')).toBe('helloworld') }) it('should remove control characters', () => { expect(sanitizeString('hello\x00\x01\x02world')).toBe('helloworld') }) it('should handle null and undefined', () => { expect(sanitizeString(null)).toBe('') expect(sanitizeString(undefined)).toBe('') }) it('should preserve newlines and tabs', () => { expect(sanitizeString('hello\nworld')).toBe('hello\nworld') expect(sanitizeString('hello\tworld')).toBe('hello\tworld') }) }) describe('sanitizeEmail', () => { it('should convert to lowercase', () => { expect(sanitizeEmail('TEST@EXAMPLE.COM')).toBe('test@example.com') }) it('should trim whitespace', () => { expect(sanitizeEmail(' test@example.com ')).toBe('test@example.com') }) it('should remove internal whitespace', () => { expect(sanitizeEmail('test @example.com')).toBe('test@example.com') }) it('should remove control characters', () => { expect(sanitizeEmail('test\x00@example.com')).toBe('test@example.com') }) it('should handle null and undefined', () => { expect(sanitizeEmail(null)).toBe('') expect(sanitizeEmail(undefined)).toBe('') }) }) describe('sanitizeHtml', () => { it('should remove HTML tags', () => { expect(sanitizeHtml('

hello

')).toBe('hello') expect(sanitizeHtml('
test
')).toBe('test') }) it('should remove script tags and content', () => { expect(sanitizeHtml('hello')).toBe('hello') }) it('should remove event handlers', () => { expect(sanitizeHtml('
test
')).toBe('test') expect(sanitizeHtml('')).toBe('') }) it('should remove javascript: protocol', () => { expect(sanitizeHtml('javascript:alert()')).toBe('alert()') }) it('should encode special HTML characters', () => { // Test encoding of special characters that aren't part of tags const result = sanitizeHtml('&"\'/') expect(result).toContain('&') expect(result).toContain('"') expect(result).toContain(''') expect(result).toContain('/') // Note: The regex /<[^>]*>/g will match anything between < and > // This is intentional for security - it's better to be overly cautious // and remove potential tags than to risk XSS const result2 = sanitizeHtml('test & value') expect(result2).toBe('test & value') }) it('should handle empty input', () => { expect(sanitizeHtml('')).toBe('') }) }) describe('sanitizeSqlInput', () => { it('should remove SQL comment markers', () => { expect(sanitizeSqlInput('test -- comment')).toBe('test comment') expect(sanitizeSqlInput('test /* comment */')).toBe('test comment ') }) it('should remove semicolons', () => { expect(sanitizeSqlInput('test; DROP TABLE')).toBe('test DROP TABLE') }) it('should handle empty input', () => { expect(sanitizeSqlInput('')).toBe('') }) }) }) describe('Input Validation', () => { describe('validateEmail', () => { it('should accept valid emails', () => { const result = validateEmail('test@example.com') expect(result.isValid).toBe(true) }) it('should reject invalid emails', () => { expect(validateEmail('invalid').isValid).toBe(false) expect(validateEmail('test@').isValid).toBe(false) expect(validateEmail('@example.com').isValid).toBe(false) }) it('should reject empty emails', () => { expect(validateEmail('').isValid).toBe(false) }) it('should reject emails that are too long', () => { const longEmail = 'a'.repeat(256) + '@example.com' expect(validateEmail(longEmail).isValid).toBe(false) }) it('should sanitize email before validation', () => { const result = validateEmail(' TEST@EXAMPLE.COM ') expect(result.isValid).toBe(true) }) }) describe('validatePassword', () => { it('should accept valid passwords', () => { const result = validatePassword('password123') expect(result.isValid).toBe(true) }) it('should reject passwords shorter than 8 characters', () => { expect(validatePassword('short').isValid).toBe(false) expect(validatePassword('1234567').isValid).toBe(false) }) it('should reject empty passwords', () => { expect(validatePassword('').isValid).toBe(false) }) it('should reject passwords longer than 128 characters', () => { const longPassword = 'a'.repeat(129) expect(validatePassword(longPassword).isValid).toBe(false) }) it('should accept passwords exactly 8 characters', () => { expect(validatePassword('12345678').isValid).toBe(true) }) it('should accept passwords exactly 128 characters', () => { const maxPassword = 'a'.repeat(128) expect(validatePassword(maxPassword).isValid).toBe(true) }) }) })