/** * Security: Sensitive Data Masking * * Verifies the maskSensitiveFields() function from @/lib/audit correctly * redacts PII and secrets from audit log payloads. * * Sensitive fields per SECURITY-GUIDELINES.md §5.2: * email, phone, password, credentials_enc, token * * Masking format: * - len > 4 → first 2 chars + "***" + last 2 chars (e.g. "al***om") * - len ≤ 4 → "***" */ import { describe, expect, it } from 'vitest'; import { maskSensitiveFields } from '@/lib/audit'; // ───────────────────────────────────────────────────────────────────────────── describe('Sensitive data masking — field detection', () => { it('masks "email" field', () => { const result = maskSensitiveFields({ email: 'user@example.com' }); expect(result?.email).not.toBe('user@example.com'); expect(result?.email).toContain('***'); }); it('masks "phone" field', () => { const result = maskSensitiveFields({ phone: '+61400000000' }); expect(result?.phone).not.toBe('+61400000000'); expect(result?.phone).toContain('***'); }); it('masks "password" field', () => { const result = maskSensitiveFields({ password: 'MySecretPassword123' }); expect(result?.password).not.toBe('MySecretPassword123'); expect(result?.password).toContain('***'); }); it('masks "credentials_enc" field', () => { const result = maskSensitiveFields({ credentials_enc: 'encrypted-secret-data' }); expect(result?.credentials_enc).not.toBe('encrypted-secret-data'); expect(result?.credentials_enc).toContain('***'); }); it('masks "token" field', () => { const result = maskSensitiveFields({ token: 'eyJhbGciOiJIUzI1NiJ9.test' }); expect(result?.token).not.toBe('eyJhbGciOiJIUzI1NiJ9.test'); expect(result?.token).toContain('***'); }); }); describe('Sensitive data masking — masking format', () => { it('long email (len > 4) uses partial mask: first 2 + *** + last 2', () => { // 'user@example.com' → 'us***om' const result = maskSensitiveFields({ email: 'user@example.com' }); expect(result?.email).toBe('us***om'); }); it('short sensitive value (len ≤ 4) is fully replaced with ***', () => { const result = maskSensitiveFields({ email: 'ab' }); expect(result?.email).toBe('***'); }); it('exactly 4-char sensitive value is fully masked', () => { const result = maskSensitiveFields({ email: 'abcd' }); expect(result?.email).toBe('***'); }); it('5-char sensitive value uses partial mask', () => { // 'abcde' → 'ab***de' const result = maskSensitiveFields({ password: 'abcde' }); expect(result?.password).toBe('ab***de'); }); it('single char sensitive value becomes ***', () => { const result = maskSensitiveFields({ token: 'x' }); expect(result?.token).toBe('***'); }); it('partial mask exposes only 2 leading and 2 trailing characters', () => { const result = maskSensitiveFields({ password: 'SuperSecret2025!' }); const masked = result?.password as string; // 'SuperSecret2025!' → first 2 = 'Su', last 2 = '5!', mask = 'Su***5!' expect(masked).toMatch(/^Su\*{3}5!$/); }); }); describe('Sensitive data masking — non-sensitive fields', () => { it('preserves string non-sensitive fields unchanged', () => { const result = maskSensitiveFields({ name: 'John Smith', status: 'active' }); expect(result?.name).toBe('John Smith'); expect(result?.status).toBe('active'); }); it('preserves numeric non-sensitive fields unchanged', () => { const result = maskSensitiveFields({ count: 42, score: 9.5 }); expect(result?.count).toBe(42); expect(result?.score).toBe(9.5); }); it('preserves boolean non-sensitive fields unchanged', () => { const result = maskSensitiveFields({ isProxy: true, isActive: false }); expect(result?.isProxy).toBe(true); expect(result?.isActive).toBe(false); }); it('preserves null non-sensitive fields unchanged', () => { const result = maskSensitiveFields({ companyName: null }); expect(result?.companyName).toBeNull(); }); it('mixed record: masks sensitive, preserves non-sensitive', () => { const result = maskSensitiveFields({ name: 'John', email: 'john@example.com', status: 'active', password: 'hunter2', }); expect(result?.name).toBe('John'); expect(result?.status).toBe('active'); expect(result?.email).toContain('***'); expect(result?.password).toContain('***'); }); }); describe('Sensitive data masking — edge cases', () => { it('returns undefined for undefined input', () => { expect(maskSensitiveFields(undefined)).toBeUndefined(); }); it('returns empty object for empty object input', () => { const result = maskSensitiveFields({}); expect(result).toEqual({}); }); it('does not mutate the original object', () => { const original = { email: 'alice@example.com', name: 'Alice' }; const originalEmail = original.email; maskSensitiveFields(original); expect(original.email).toBe(originalEmail); }); it('only masks string values — non-string sensitive fields are left as-is', () => { // e.g. if someone stores a number in an "email" field (type error upstream), // the masking logic gracefully skips it (typeof check) const result = maskSensitiveFields({ email: 12345 as unknown as string }); // The implementation only masks if typeof === 'string', so a number stays expect(result?.email).toBe(12345); }); });