import { describe, it, expect } from 'vitest'; import { redact, isSensitiveKey, REDACTED_VALUE } from './itglue-redact'; describe('isSensitiveKey', () => { it.each([ ['password', true], ['Password', true], ['PASSWORD', true], ['user_password', true], ['secret', true], ['client_secret', true], ['apiKey', true], ['api_key', true], ['api-key', true], ['API_KEY', true], ['authToken', true], ['accessToken', true], ['credentials', true], ['masterKey', true], ['privateKey', true], ['name', false], ['email', false], ['id', false], ['title', false], ['hostname', false], ['username', false], // intentional: username alone isn't a credential ])('%s -> %s', (key, expected) => { expect(isSensitiveKey(key)).toBe(expected); }); }); describe('redact', () => { it('redacts top-level sensitive keys', () => { const input = { id: 'abc', password: 'hunter2', name: 'wifi' }; expect(redact(input)).toEqual({ id: 'abc', password: REDACTED_VALUE, name: 'wifi', }); }); it('redacts nested object credentials', () => { const input = { id: 'abc', traits: { username: 'svc-account', password: 'p@ss', api_key: 'ak_123', }, }; expect(redact(input)).toEqual({ id: 'abc', traits: { username: 'svc-account', password: REDACTED_VALUE, api_key: REDACTED_VALUE, }, }); }); it('redacts inside arrays of objects', () => { const input = { passwords: [ { id: 1, name: 'admin', password: 'topsecret' }, { id: 2, name: 'svc', password: 'alsotopsecret' }, ], }; // The outer key "passwords" matches → entire array is redacted. expect(redact(input)).toEqual({ passwords: REDACTED_VALUE }); }); it('redacts per-item secrets when the array key is benign', () => { const input = { accounts: [ { id: 1, name: 'admin', password: 'topsecret' }, { id: 2, name: 'svc', api_key: 'ak_456' }, ], }; expect(redact(input)).toEqual({ accounts: [ { id: 1, name: 'admin', password: REDACTED_VALUE }, { id: 2, name: 'svc', api_key: REDACTED_VALUE }, ], }); }); it('redacts whole subtree when key matches even if value is an object', () => { const input = { org_id: 7, auth: { type: 'oauth', client_id: 'cid', client_secret: 'shouldbehidden', nested: { tokens: { access: 'a', refresh: 'r' } }, }, }; // Note: "auth" itself does NOT match the pattern, so it's recursed into. // Inside, client_id is benign, client_secret matches, nested.tokens matches. expect(redact(input)).toEqual({ org_id: 7, auth: { type: 'oauth', client_id: 'cid', client_secret: REDACTED_VALUE, nested: { tokens: REDACTED_VALUE }, }, }); }); it('preserves non-sensitive values of all primitive types', () => { const input = { id: 'abc', count: 42, enabled: true, ratio: 0.5, tag: null, missing: undefined, }; expect(redact(input)).toEqual(input); }); it('does not mutate the input', () => { const input = { id: 'abc', password: 'hunter2', nested: { api_key: 'ak_123', name: 'svc' }, }; const snapshot = JSON.parse(JSON.stringify(input)); redact(input); expect(input).toEqual(snapshot); }); it('returns null/undefined unchanged on sensitive keys with null value', () => { expect(redact({ password: null })).toEqual({ password: null }); expect(redact({ password: undefined })).toEqual({ password: undefined }); }); it('handles primitives at the root', () => { expect(redact('plain string')).toBe('plain string'); expect(redact(42)).toBe(42); expect(redact(null)).toBe(null); expect(redact(undefined)).toBe(undefined); }); it('handles top-level arrays', () => { const input = [ { id: 1, password: 'a' }, { id: 2, name: 'b' }, ]; expect(redact(input)).toEqual([ { id: 1, password: REDACTED_VALUE }, { id: 2, name: 'b' }, ]); }); it('does not crash on cyclic references', () => { const a: Record = { id: 1 }; a.self = a; // cycle a.password = 'secret'; const out = redact(a) as Record; expect(out.id).toBe(1); expect(out.password).toBe(REDACTED_VALUE); expect(out.self).toBe('[CIRCULAR]'); }); });