/** * Data Masking Unit Tests * * Tests for the data masking module. * Covers field detection, pattern matching, recursive masking, and special formats. */ import { describe, it, expect, beforeEach, vi } from 'vitest' import { maskString, maskObject, maskError, isSensitiveField, safeStringify, createSafeLogger, maskSmtpError, } from '@/lib/security/data-masking' describe('Data Masking', () => { describe('isSensitiveField', () => { it('detects password field', () => { expect(isSensitiveField('password')).toBe(true) expect(isSensitiveField('PASSWORD')).toBe(true) expect(isSensitiveField('userPassword')).toBe(true) expect(isSensitiveField('password_hash')).toBe(true) }) it('detects token fields', () => { expect(isSensitiveField('token')).toBe(true) expect(isSensitiveField('accessToken')).toBe(true) expect(isSensitiveField('refreshToken')).toBe(true) expect(isSensitiveField('apiToken')).toBe(true) }) it('detects API key fields', () => { expect(isSensitiveField('apiKey')).toBe(true) expect(isSensitiveField('api_key')).toBe(true) expect(isSensitiveField('apikey')).toBe(true) }) it('detects secret fields', () => { expect(isSensitiveField('secret')).toBe(true) expect(isSensitiveField('clientSecret')).toBe(true) expect(isSensitiveField('payloadSecret')).toBe(true) }) it('detects credential fields', () => { expect(isSensitiveField('credentials')).toBe(true) expect(isSensitiveField('authCredentials')).toBe(true) }) it('detects private key fields', () => { expect(isSensitiveField('privateKey')).toBe(true) expect(isSensitiveField('private_key')).toBe(true) }) it('detects SMTP-related fields', () => { expect(isSensitiveField('smtpPassword')).toBe(true) expect(isSensitiveField('smtp_pass')).toBe(true) }) it('returns false for non-sensitive fields', () => { expect(isSensitiveField('email')).toBe(false) expect(isSensitiveField('name')).toBe(false) expect(isSensitiveField('id')).toBe(false) expect(isSensitiveField('status')).toBe(false) }) }) describe('maskString', () => { it('masks JWT tokens', () => { const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c' const masked = maskString(jwt) // JWT format: keeps header, redacts payload and signature expect(masked).toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9') expect(masked).toContain('[PAYLOAD REDACTED]') expect(masked).toContain('[SIGNATURE REDACTED]') }) it('masks connection strings', () => { const connectionString = 'postgresql://user:secret123@localhost:5432/mydb' const masked = maskString(connectionString) expect(masked).toContain('[REDACTED]') expect(masked).not.toContain('secret123') }) it('masks private keys (inline format)', () => { // Test with inline private key format const privateKey = '-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA...-----END RSA PRIVATE KEY-----' const masked = maskString(privateKey) expect(masked).toBe('[PRIVATE KEY REDACTED]') }) it('masks password patterns in text', () => { const text = 'User logged in with password=secret123 successfully' const masked = maskString(text) // The pattern replaces password=... with password: [REDACTED] or pass: [REDACTED] expect(masked).toContain('[REDACTED]') expect(masked).not.toContain('secret123') }) it('preserves non-sensitive text', () => { const text = 'Normal log message without sensitive data' const masked = maskString(text) expect(masked).toBe(text) }) it('handles empty string', () => { expect(maskString('')).toBe('') }) it('handles null/undefined gracefully', () => { // The function returns the input for non-strings expect(maskString(null as unknown as string)).toBeNull() expect(maskString(undefined as unknown as string)).toBeUndefined() }) }) describe('maskObject', () => { it('masks sensitive fields in flat object', () => { const obj = { email: 'user@example.com', password: 'supersecret', name: 'John Doe', } const masked = maskObject(obj) expect(masked.email).toBe('user@example.com') expect(masked.password).toBe('[REDACTED]') expect(masked.name).toBe('John Doe') }) it('masks sensitive fields in nested object', () => { const obj = { user: { email: 'user@example.com', credentials: { password: 'secret', apiKey: 'key-12345', }, }, status: 'active', } const masked = maskObject(obj) as typeof obj expect(masked.user.email).toBe('user@example.com') // credentials itself is a sensitive field name expect(masked.user.credentials).toBe('[REDACTED OBJECT]') expect(masked.status).toBe('active') }) it('masks arrays with sensitive values', () => { const obj = { tokens: ['token1', 'token2'], users: [{ name: 'Alice', password: 'secret1' }, { name: 'Bob', password: 'secret2' }], } const masked = maskObject(obj) as typeof obj // tokens is a sensitive field name (contains 'token') expect(masked.tokens).toBe('[REDACTED OBJECT]') expect(masked.users[0].name).toBe('Alice') expect(masked.users[0].password).toBe('[REDACTED]') }) it('handles circular references gracefully', () => { const obj: Record = { name: 'test' } obj.self = obj // Should not throw expect(() => maskObject(obj)).not.toThrow() }) it('respects depth limit', () => { const deepObj = { level1: { level2: { level3: { level4: { level5: { password: 'secret', }, }, }, }, }, } const masked = maskObject(deepObj, { maxDepth: 3 }) // Deep nested should still be handled expect(masked).toBeDefined() }) it('preserves specified fields', () => { const obj = { password: 'secret', debugPassword: 'debug-secret', } const masked = maskObject(obj, { preserveFields: ['debugPassword'] }) expect(masked.password).toBe('[REDACTED]') expect(masked.debugPassword).toBe('debug-secret') }) it('masks additional custom fields', () => { const obj = { customSecret: 'my-custom-value', normalField: 'normal', } const masked = maskObject(obj, { additionalFields: ['customSecret'] }) expect(masked.customSecret).toBe('[REDACTED]') expect(masked.normalField).toBe('normal') }) it('masks string values containing secrets', () => { const obj = { config: 'database_url=postgresql://admin:password123@localhost/db apiKey=sk-12345', } const masked = maskObject(obj) as typeof obj expect(masked.config).not.toContain('password123') }) it('handles null and undefined values', () => { const obj = { password: null, token: undefined, name: 'test', } const masked = maskObject(obj) expect(masked.password).toBeNull() expect(masked.token).toBeUndefined() expect(masked.name).toBe('test') }) }) describe('maskError', () => { it('masks error message', () => { const error = new Error('Database connection failed: password=secret123') const masked = maskError(error) expect(masked.message).not.toContain('secret123') expect(masked.message).toContain('[REDACTED]') }) it('masks error stack', () => { const error = new Error('Error with password=secret') const masked = maskError(error) if (masked.stack) { expect(masked.stack).not.toContain('secret') } }) it('preserves error structure', () => { const error = new Error('Test error') error.name = 'CustomError' const masked = maskError(error) expect(masked.name).toBe('CustomError') expect(masked.message).toBe('Test error') }) it('handles non-Error objects', () => { const notAnError = { message: 'password=secret', code: 500 } const masked = maskError(notAnError as unknown as Error) expect(masked).toBeDefined() }) it('handles string errors', () => { const stringError = 'Connection failed: apiKey=12345' const masked = maskError(stringError as unknown as Error) expect(masked).toBeDefined() }) }) describe('safeStringify', () => { it('stringifies and masks object', () => { const obj = { user: 'admin', password: 'secret', } const result = safeStringify(obj) expect(result).toContain('admin') expect(result).toContain('[REDACTED]') expect(result).not.toContain('secret') }) it('handles circular references', () => { const obj: Record = { name: 'test' } obj.self = obj // Should not throw expect(() => safeStringify(obj)).not.toThrow() }) it('formats with spaces when specified', () => { const obj = { name: 'test' } const result = safeStringify(obj, 2) expect(result).toContain('\n') }) it('handles arrays', () => { const arr = [{ password: 'secret' }, { name: 'test' }] const result = safeStringify(arr) expect(result).toContain('[REDACTED]') expect(result).toContain('test') }) }) describe('createSafeLogger', () => { beforeEach(() => { vi.spyOn(console, 'log').mockImplementation(() => {}) vi.spyOn(console, 'error').mockImplementation(() => {}) vi.spyOn(console, 'warn').mockImplementation(() => {}) }) it('creates logger with info method', () => { const logger = createSafeLogger('TestModule') logger.info('Test message') expect(console.log).toHaveBeenCalledWith( expect.stringContaining('[TestModule]'), expect.any(String), expect.any(String), ) }) it('masks sensitive data in log messages', () => { const logger = createSafeLogger('Auth') logger.info('User login', { password: 'secret123' }) const lastCall = (console.log as ReturnType).mock.calls[0] const logOutput = JSON.stringify(lastCall) expect(logOutput).toContain('[REDACTED]') expect(logOutput).not.toContain('secret123') }) it('provides error method', () => { const logger = createSafeLogger('Error') logger.error('Error occurred', new Error('password=secret')) expect(console.error).toHaveBeenCalled() }) it('provides warn method', () => { const logger = createSafeLogger('Warn') logger.warn('Warning', { apiKey: 'key-123' }) expect(console.warn).toHaveBeenCalled() }) }) describe('maskSmtpError', () => { it('masks SMTP authentication failure with credentials', () => { const error = 'AUTH LOGIN failed: Username and Password not accepted.' const masked = maskSmtpError(error) expect(masked).toContain('[') expect(masked).not.toContain('Username and Password') }) it('masks 535 authentication error codes with details', () => { const error = '535 5.7.8 Error: Username and Password not accepted for user@example.com' const masked = maskSmtpError(error) expect(masked).not.toContain('user@example.com') expect(masked).toContain('535') }) it('masks invalid login errors with user details', () => { const error = 'Invalid login: 535-5.7.8 Username and Password not accepted' const masked = maskSmtpError(error) expect(masked).toContain('[') }) it('masks connection strings in SMTP errors', () => { const error = 'Connection failed to smtp://admin:secret123@mail.example.com:587' const masked = maskSmtpError(error) expect(masked).not.toContain('secret123') expect(masked).toContain('[REDACTED]') }) it('preserves safe SMTP error codes', () => { const error = '530 5.7.0 Must issue a STARTTLS command first' const masked = maskSmtpError(error) // Should keep the diagnostic error code expect(masked).toContain('530') expect(masked).toContain('STARTTLS') }) it('truncates excessively long error messages', () => { const longError = 'Error: ' + 'A'.repeat(500) const masked = maskSmtpError(longError) expect(masked!.length).toBeLessThanOrEqual(203) // 200 + "..." expect(masked).toContain('...') }) it('handles null and undefined gracefully', () => { expect(maskSmtpError(null)).toBeNull() expect(maskSmtpError(undefined)).toBeNull() }) it('handles empty string', () => { expect(maskSmtpError('')).toBeNull() }) it('passes through simple error messages unchanged', () => { const error = 'Connection timeout' const masked = maskSmtpError(error) expect(masked).toBe('Connection timeout') }) it('masks Base64 encoded credentials in AUTH', () => { const error = 'AUTH PLAIN dXNlcm5hbWU6cGFzc3dvcmQxMjM= failed' const masked = maskSmtpError(error) expect(masked).not.toContain('dXNlcm5hbWU6cGFzc3dvcmQxMjM=') }) it('masks password= patterns in SMTP error details', () => { const error = 'SMTP config: host=mail.example.com password=supersecret123' const masked = maskSmtpError(error) expect(masked).not.toContain('supersecret123') expect(masked).toContain('[REDACTED]') }) }) describe('Real-world Scenarios', () => { it('masks SMTP configuration', () => { const smtpConfig = { host: 'smtp.example.com', port: 587, smtpUser: 'sender@example.com', smtpPassword: 'smtp-password-123', } const masked = maskObject(smtpConfig) as typeof smtpConfig expect(masked.host).toBe('smtp.example.com') expect(masked.smtpUser).toBe('sender@example.com') expect(masked.smtpPassword).toBe('[REDACTED]') }) it('masks tenant data', () => { const tenant = { id: 1, name: 'Test Tenant', smtpSettings: { host: 'smtp.tenant.com', password: 'tenant-smtp-pass', apiKey: 'sendgrid-key-123', }, } const masked = maskObject(tenant) as typeof tenant expect(masked.id).toBe(1) expect(masked.name).toBe('Test Tenant') expect(masked.smtpSettings.host).toBe('smtp.tenant.com') expect(masked.smtpSettings.password).toBe('[REDACTED]') expect(masked.smtpSettings.apiKey).toBe('[REDACTED]') }) it('masks audit log data', () => { const auditLog = { action: 'login_failed', userEmail: 'user@example.com', metadata: { attemptedPassword: 'wrong-password', ipAddress: '192.168.1.1', }, } const masked = maskObject(auditLog) as typeof auditLog expect(masked.action).toBe('login_failed') expect(masked.userEmail).toBe('user@example.com') expect(masked.metadata.attemptedPassword).toBe('[REDACTED]') expect(masked.metadata.ipAddress).toBe('192.168.1.1') }) it('masks database connection errors', () => { const error = new Error( 'FATAL: password authentication failed for user "admin" at host "db.example.com"', ) const masked = maskError(error) expect(masked.message).not.toContain('authentication failed') }) }) })