import { describe, it, expect } from 'vitest'; import { diffFields, maskSensitiveFields } from '@/lib/audit'; describe('diffFields', () => { it('returns empty array when records are identical', () => { const result = diffFields({ name: 'Alice', status: 'active' }, { name: 'Alice', status: 'active' }); expect(result).toEqual([]); }); it('detects a single field change with correct field/old/new', () => { const result = diffFields({ name: 'Alice', status: 'active' }, { name: 'Alice', status: 'inactive' }); expect(result).toHaveLength(1); expect(result[0]).toEqual({ field: 'status', oldValue: 'active', newValue: 'inactive' }); }); it('detects multiple field changes', () => { const result = diffFields( { name: 'Alice', status: 'active', count: 1 }, { name: 'Bob', status: 'inactive', count: 2 }, ); expect(result).toHaveLength(3); const fields = result.map((r) => r.field); expect(fields).toContain('name'); expect(fields).toContain('status'); expect(fields).toContain('count'); }); it('detects null-to-value change', () => { const result = diffFields({ note: null }, { note: 'hello' }); expect(result).toHaveLength(1); expect(result[0]).toEqual({ field: 'note', oldValue: null, newValue: 'hello' }); }); it('detects value-to-null change', () => { const result = diffFields({ note: 'hello' }, { note: null }); expect(result).toHaveLength(1); expect(result[0]).toEqual({ field: 'note', oldValue: 'hello', newValue: null }); }); it('uses JSON comparison for nested objects', () => { const old = { meta: { x: 1, y: 2 } }; const updated = { meta: { x: 1, y: 3 } }; const result = diffFields(old, updated); expect(result).toHaveLength(1); expect(result[0].field).toBe('meta'); }); it('no diff when nested objects are deeply equal', () => { const result = diffFields({ meta: { x: 1 } }, { meta: { x: 1 } }); expect(result).toHaveLength(0); }); it('only checks keys present in newRecord', () => { // 'extra' key in old is irrelevant const result = diffFields({ name: 'Alice', extra: 'ignored' }, { name: 'Alice' }); expect(result).toHaveLength(0); }); }); describe('maskSensitiveFields', () => { it('masks email field', () => { const result = maskSensitiveFields({ email: 'alice@example.com' }); expect(result?.email).not.toBe('alice@example.com'); expect(typeof result?.email).toBe('string'); expect(result?.email).toContain('***'); }); it('masks phone field', () => { const result = maskSensitiveFields({ phone: '+61400000000' }); expect(result?.phone).toContain('***'); }); it('masks password field', () => { const result = maskSensitiveFields({ password: 'mySecret123' }); expect(result?.password).toContain('***'); }); it('masks credentials_enc field', () => { const result = maskSensitiveFields({ credentials_enc: 'eyJpdiI6IjEyMzQ1' }); expect(result?.credentials_enc).toContain('***'); }); it('masks token field', () => { const result = maskSensitiveFields({ token: 'abc-def-ghi-jkl' }); expect(result?.token).toContain('***'); }); it('preserves non-sensitive fields unchanged', () => { const result = maskSensitiveFields({ name: 'Alice', status: 'active', count: 5 }); expect(result?.name).toBe('Alice'); expect(result?.status).toBe('active'); expect(result?.count).toBe(5); }); it('applies partial masking: first 2 + *** + last 2 chars for strings longer than 4', () => { const result = maskSensitiveFields({ email: 'alice@example.com' }); // 'alice@example.com' length > 4, so al***om expect(result?.email).toBe('al***om'); }); it('replaces short strings (<=4 chars) with just ***', () => { const result = maskSensitiveFields({ email: 'ab@c' }); // length 4 expect(result?.email).toBe('***'); }); it('replaces 1-char sensitive string with ***', () => { const result = maskSensitiveFields({ token: 'x' }); expect(result?.token).toBe('***'); }); it('handles undefined input by returning undefined', () => { expect(maskSensitiveFields(undefined)).toBeUndefined(); }); it('does not mutate the original object', () => { const original = { email: 'alice@example.com', name: 'Alice' }; maskSensitiveFields(original); expect(original.email).toBe('alice@example.com'); }); });