sanitize.test.ts162 lines · main
1import { describe, expect, it } from 'vitest'
2
3import { sanitizeArrayOfObjects } from './sanitize'
4
5describe('sanitizeArrayOfObjects', () => {
6 it('redacts sensitive keys case-insensitively', () => {
7 const input = [{ Password: 'hunter2', username: 'alice' }]
8
9 const result = sanitizeArrayOfObjects(input) as Array<Record<string, unknown>>
10
11 expect(result).toEqual([{ Password: '[REDACTED]', username: 'alice' }])
12 })
13
14 it('honors custom redaction and extra sensitive keys', () => {
15 const input = [
16 {
17 customSensitive: 'value',
18 token: 'should hide',
19 nested: { customSensitive: 'also hide' },
20 },
21 ]
22
23 const result = sanitizeArrayOfObjects(input, {
24 redaction: '<removed>',
25 sensitiveKeys: ['customSensitive'],
26 }) as Array<any>
27
28 expect(result[0].customSensitive).toBe('<removed>')
29 expect(result[0].token).toBe('<removed>')
30 expect(result[0].nested).toEqual({ customSensitive: '<removed>' })
31 expect(input[0].nested.customSensitive).toBe('also hide')
32 })
33
34 it('redacts known secret patterns in strings', () => {
35 const samples = [
36 { value: '192.168.0.1' },
37 { value: '2001:0db8:85a3:0000:0000:8a2e:0370:7334' },
38 { value: 'AKIAIOSFODNN7EXAMPLE' },
39 { value: 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' },
40 { value: 'Bearer abcdEFGHijklMNOPqrstUVWXyz0123456789' },
41 {
42 value:
43 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
44 },
45 { value: 'A'.repeat(32) },
46 ]
47
48 const result = sanitizeArrayOfObjects(samples) as Array<{ value: string }>
49
50 for (const item of result) {
51 expect(item.value).toBe('[REDACTED]')
52 }
53 })
54
55 it('limits recursion depth and uses truncation notice', () => {
56 const input = [
57 {
58 level1: {
59 level2: {
60 level3: {
61 password: 'secret',
62 },
63 },
64 },
65 },
66 ]
67
68 const [result] = sanitizeArrayOfObjects(input, {
69 maxDepth: 2,
70 truncationNotice: '<truncated>',
71 }) as Array<any>
72
73 expect(result.level1.level2).toBe('<truncated>')
74 expect(result.level1).not.toBe(input[0].level1)
75 expect(input[0].level1.level2.level3.password).toBe('secret')
76 })
77
78 it('handles circular references without crashing', () => {
79 const obj: any = { name: 'loop' }
80 obj.self = obj
81
82 const [result] = sanitizeArrayOfObjects([obj]) as Array<any>
83
84 expect(result.self).toBe('[Circular]')
85 expect(result.name).toBe('loop')
86 })
87
88 it('sanitizes complex types consistently', () => {
89 const date = new Date('2024-01-01T00:00:00.000Z')
90 const regex = /abc/gi
91 const fn = () => {}
92 const arrayBuffer = new ArrayBuffer(8)
93 const typedArray = new Uint8Array([1, 2, 3])
94 const map = new Map<any, any>()
95 map.set('password', 'hunter2')
96 map.set('public', date)
97 const set = new Set<any>([1, date])
98 const url = new URL('https://example.com/path')
99 const error = new Error('Token is Bearer abcdEFGHijklMNOPqrstUVWXyz0123456789')
100 const custom = new (class Custom {
101 toString() {
102 return 'custom-instance'
103 }
104 })()
105
106 const [result] = sanitizeArrayOfObjects([
107 {
108 date,
109 regex,
110 fn,
111 arrayBuffer,
112 typedArray,
113 map,
114 set,
115 url,
116 error,
117 custom,
118 },
119 ]) as Array<any>
120
121 expect(result.date).toBe('2024-01-01T00:00:00.000Z')
122 expect(result.regex).toBe('/abc/gi')
123 expect(result.fn).toBe('[Function]')
124 expect(result.arrayBuffer).toBe('[ArrayBuffer byteLength=8]')
125 expect(result.typedArray).toBe('[TypedArray byteLength=3]')
126 expect(result.map).toEqual([
127 ['[REDACTED]', '[REDACTED]'],
128 ['public', '2024-01-01T00:00:00.000Z'],
129 ])
130 expect(result.set).toEqual([1, '2024-01-01T00:00:00.000Z'])
131 expect(result.url).toBe('https://example.com/path')
132 expect(result.error).toEqual({
133 name: 'Error',
134 message: 'Token is [REDACTED]',
135 stack: '[REDACTED: max depth reached]',
136 })
137 expect(result.custom).toBe('custom-instance')
138 })
139
140 it('sanitizes primitive array entries', () => {
141 const [redacted, number] = sanitizeArrayOfObjects([
142 'Bearer abcdEFGHijklMNOPqrstUVWXyz0123456789',
143 42,
144 ]) as Array<any>
145
146 expect(redacted).toBe('[REDACTED]')
147 expect(number).toBe(42)
148 })
149
150 it('applies maxDepth=0 to top-level entries', () => {
151 const result = sanitizeArrayOfObjects(
152 [{ password: 'secret', nested: { value: 'test' } }, 'visible'],
153 {
154 maxDepth: 0,
155 truncationNotice: '<blocked>',
156 }
157 ) as Array<any>
158
159 expect(result[0]).toBe('<blocked>')
160 expect(result[1]).toBe('visible')
161 })
162})