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 = [
'
',
'