import { checkRateLimit, createSafeErrorMessage, escapeHtml, sanitizeText, sanitizeUrl, validateOrigin, validateUsername } from '../security-utils' // Mock the helpers module jest.mock('../security-utils-helpers', () => { const actual = jest.requireActual('../security-utils-helpers') const mockRateLimitMap = new Map() return { rateLimitMap: mockRateLimitMap, sanitizeErrorMessage: jest.fn(msg => msg), escapeHtml: actual.escapeHtml, stripHtml: actual.stripHtml, clearRateLimiting: jest.fn(() => mockRateLimitMap.clear()), getRateLimitKey: jest.fn((ip, action) => `${ip}:${action}`), checkRateLimit: jest.fn(({ identifier, windowMs = 60000, maxRequests = 10 }) => { const now = Date.now() const record = mockRateLimitMap.get(identifier) if (!record || now > record.resetTime) { const resetTime = now + windowMs mockRateLimitMap.set(identifier, { count: 1, resetTime }) return { allowed: true, resetTime } } if (record.count < maxRequests) { record.count++ return { allowed: true, resetTime: record.resetTime } } return { allowed: false, resetTime: record.resetTime } }) } }) const { rateLimitMap, sanitizeErrorMessage } = require('../security-utils-helpers') describe('security-utils', () => { beforeEach(() => { rateLimitMap.clear() jest.clearAllMocks() }) describe('checkRateLimit', () => { it('should allow first request', () => { const result = checkRateLimit({ identifier: 'test-key' }) expect(result.allowed).toBe(true) expect(result.resetTime).toBeDefined() expect(typeof result.resetTime).toBe('number') }) it('should allow requests within limit', () => { const identifier = 'test-key' const options = { windowMs: 60000, maxRequests: 3 } const result1 = checkRateLimit({ identifier, ...options }) const result2 = checkRateLimit({ identifier, ...options }) const result3 = checkRateLimit({ identifier, ...options }) expect(result1.allowed).toBe(true) expect(result2.allowed).toBe(true) expect(result3.allowed).toBe(true) }) it('should deny requests over limit', () => { const identifier = 'test-key' const options = { windowMs: 60000, maxRequests: 2 } checkRateLimit({ identifier, ...options }) checkRateLimit({ identifier, ...options }) const result = checkRateLimit({ identifier, ...options }) expect(result.allowed).toBe(false) expect(result.resetTime).toBeDefined() expect(typeof result.resetTime).toBe('number') }) it('should reset after time window', () => { const identifier = 'test-key' const options = { windowMs: 1, maxRequests: 1 } // First request should be allowed const result1 = checkRateLimit({ identifier, ...options }) expect(result1.allowed).toBe(true) // Wait for window to expire setTimeout(() => { const result2 = checkRateLimit({ identifier, ...options }) expect(result2.allowed).toBe(true) }, 10) }) it('should handle different keys independently', () => { const options = { windowMs: 60000, maxRequests: 1 } const result1 = checkRateLimit({ identifier: 'key1', ...options }) const result2 = checkRateLimit({ identifier: 'key2', ...options }) expect(result1.allowed).toBe(true) expect(result2.allowed).toBe(true) }) it('should use default options when none provided', () => { const result = checkRateLimit({ identifier: 'test-key' }) expect(result.allowed).toBe(true) // Should use default maxRequests of 10 for (let i = 0; i < 9; i++) { checkRateLimit({ identifier: 'test-key' }) } const finalResult = checkRateLimit({ identifier: 'test-key' }) expect(finalResult.allowed).toBe(false) }) }) describe('sanitizeText', () => { it('should return null for null input', () => { expect(sanitizeText(null)).toBe(null) }) it('should return null for undefined input', () => { expect(sanitizeText(undefined)).toBe(null) }) it('should return empty string for empty input', () => { expect(sanitizeText('')).toBe('') }) it('should preserve safe text', () => { expect(sanitizeText('Hello World')).toBe('Hello World') }) it('should strip all HTML tags but preserve text content', () => { expect(sanitizeText('Hello world')).toBe('Hello world') expect(sanitizeText('Text with emphasis')).toBe('Text with emphasis') expect(sanitizeText('Paragraph
break')).toBe('Paragraphbreak') }) it('should remove dangerous HTML tags', () => { const result = sanitizeText('Safe text') expect(result).toBe('Safe text') expect(result).not.toContain('')).toThrow( 'Invalid URL format' ) }) it('should reject vbscript: URLs', () => { expect(() => sanitizeUrl('vbscript:msgbox("xss")')).toThrow('Invalid URL format') }) it('should reject file: URLs', () => { expect(() => sanitizeUrl('file:///etc/passwd')).toThrow('Invalid URL format') }) it('should reject invalid URL formats', () => { expect(() => sanitizeUrl('not-a-url')).toThrow('Invalid URL format') expect(() => sanitizeUrl('ftp://example.com')).toThrow('Invalid URL format') expect(() => sanitizeUrl('example.com')).toThrow('Invalid URL format') }) it('should handle case-insensitive protocol checks', () => { expect(() => sanitizeUrl('JAVASCRIPT:alert("xss")')).toThrow('Invalid URL format') expect(() => sanitizeUrl('JavaScript:alert("xss")')).toThrow('Invalid URL format') }) }) describe('validateUsername', () => { it('should reject empty username', () => { const result = validateUsername('') expect(result.valid).toBe(false) expect(result.error).toBe('Username is required') }) it('should reject username that becomes empty after sanitization', () => { const result = validateUsername('') expect(result.valid).toBe(false) expect(result.error).toBe('Username cannot be empty') }) it('should reject username shorter than 3 characters', () => { const result = validateUsername('ab') expect(result.valid).toBe(false) expect(result.error).toBe('Username must be at least 3 characters') }) it('should reject username longer than 30 characters', () => { const longUsername = 'a'.repeat(31) const result = validateUsername(longUsername) expect(result.valid).toBe(false) expect(result.error).toBe('Username must be 30 characters or less') }) it('should accept valid usernames', () => { const validUsernames = ['user123', 'test_user', 'my-username', 'User1'] validUsernames.forEach(username => { const result = validateUsername(username) expect(result.valid).toBe(true) expect(result.error).toBeUndefined() }) }) it('should reject usernames with invalid characters', () => { const invalidUsernames = ['user@name', 'user name', 'user.name', 'user$name', 'user#name'] invalidUsernames.forEach(username => { const result = validateUsername(username) expect(result.valid).toBe(false) expect(result.error).toBe( 'Username can only contain letters, numbers, underscores, and hyphens' ) }) }) it('should reject reserved usernames', () => { const reservedUsernames = ['admin', 'api', 'root', 'system', 'support', 'help', 'www'] reservedUsernames.forEach(username => { const result = validateUsername(username) expect(result.valid).toBe(false) expect(result.error).toBe('Username is reserved') }) }) it('should reject reserved usernames case-insensitively', () => { const result1 = validateUsername('ADMIN') const result2 = validateUsername('Admin') expect(result1.valid).toBe(false) expect(result2.valid).toBe(false) expect(result1.error).toBe('Username is reserved') expect(result2.error).toBe('Username is reserved') }) }) describe('createSafeErrorMessage', () => { beforeEach(() => { sanitizeErrorMessage.mockImplementation((msg: string) => msg) }) it('should handle null/undefined errors', () => { expect(createSafeErrorMessage(null)).toBe('An error occurred') expect(createSafeErrorMessage(undefined)).toBe('An error occurred') }) it('should handle string errors', () => { expect(createSafeErrorMessage('Simple error')).toBe('Simple error') }) it('should handle Error objects', () => { const error = new Error('Test error message') expect(createSafeErrorMessage(error)).toBe('Test error message') }) it('should map database connection errors', () => { const error = new Error('Database connection failed') expect(createSafeErrorMessage(error)).toBe('Database error occurred') }) it('should map user not found errors', () => { const error = new Error('User not found in database') expect(createSafeErrorMessage(error)).toBe('User not found') }) it('should map authentication errors', () => { const error = new Error('Invalid token provided') expect(createSafeErrorMessage(error)).toBe('Authentication error') }) it('should map rate limit errors', () => { const error = new Error('Rate limit exceeded for user') expect(createSafeErrorMessage(error)).toBe('Too many requests') }) it('should handle file not found errors', () => { const error = new Error('ENOENT: no such file or directory') expect(createSafeErrorMessage(error)).toBe('File not found') }) it('should handle connection refused errors', () => { const error = new Error('ECONNREFUSED to server') expect(createSafeErrorMessage(error)).toBe('Connection refused') }) it('should handle timeout errors', () => { const error = new Error('Timeout exceeded for request') expect(createSafeErrorMessage(error)).toBe('Request timeout') }) it('should use fallback message when sanitization returns null', () => { sanitizeErrorMessage.mockReturnValue(null) const error = new Error('Sensitive information') expect(createSafeErrorMessage(error)).toBe('An error occurred') }) it('should handle custom fallback message (legacy API)', () => { expect(createSafeErrorMessage(null, 'Custom fallback')).toBe('Custom fallback') }) it('should handle non-Error objects', () => { expect(createSafeErrorMessage({ message: 'object error' })).toBe('An error occurred') expect(createSafeErrorMessage(123)).toBe('An error occurred') expect(createSafeErrorMessage(true)).toBe('An error occurred') }) }) describe('escapeHtml', () => { it('should handle null/undefined input', () => { expect(escapeHtml(null)).toBe('') expect(escapeHtml(undefined)).toBe('') }) it('should handle number input', () => { expect(escapeHtml(123)).toBe('123') expect(escapeHtml(0)).toBe('0') }) it('should handle non-string input', () => { expect(escapeHtml(true as any)).toBe('') expect(escapeHtml({} as any)).toBe('') expect(escapeHtml([] as any)).toBe('') }) it('should escape basic HTML entities', () => { expect(escapeHtml('')).toBe( '<script>alert("xss")</script>' ) expect(escapeHtml('Tom & Jerry')).toBe('Tom & Jerry') expect(escapeHtml("Single 'quotes'")).toBe('Single 'quotes'') }) it('should escape closing script tags specifically', () => { expect(escapeHtml('')).toBe('</script>') expect(escapeHtml('')).toBe('</script>') expect(escapeHtml('')).toBe('</script>') }) it('should handle complex XSS attempts', () => { const xssAttempts = [ '', '', '', '">' ] xssAttempts.forEach(xss => { const result = escapeHtml(xss) expect(result).not.toContain('