autotel-edge 3.16.0 → 3.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,432 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createRedactor, REDACT_PRESETS } from './redact';
3
+
4
+ describe('createRedactor', () => {
5
+ describe('basic paths', () => {
6
+ it('should support built-in preset names', () => {
7
+ const redact = createRedactor('default');
8
+ expect(
9
+ redact({
10
+ password: 'secret',
11
+ headers: { authorization: 'Bearer token' },
12
+ safe: 'visible',
13
+ }),
14
+ ).toEqual({
15
+ password: '[Redacted]',
16
+ headers: { authorization: '[Redacted]' },
17
+ safe: 'visible',
18
+ });
19
+ });
20
+
21
+ it('should redact a top-level string key', () => {
22
+ const redact = createRedactor({ paths: ['password'] });
23
+ expect(redact({ password: 'secret', name: 'Alice' })).toEqual({
24
+ password: '[Redacted]',
25
+ name: 'Alice',
26
+ });
27
+ });
28
+
29
+ it('should redact multiple top-level keys', () => {
30
+ const redact = createRedactor({ paths: ['password', 'token'] });
31
+ expect(redact({ password: 's', token: 't', id: 1 })).toEqual({
32
+ password: '[Redacted]',
33
+ token: '[Redacted]',
34
+ id: 1,
35
+ });
36
+ });
37
+
38
+ it('should not add redacted keys that do not exist', () => {
39
+ const redact = createRedactor({ paths: ['missing'] });
40
+ expect(redact({ name: 'Alice' })).toEqual({ name: 'Alice' });
41
+ });
42
+
43
+ it('should handle null/undefined input', () => {
44
+ const redact = createRedactor({ paths: ['password'] });
45
+ expect(redact(null)).toBeNull();
46
+ // eslint-disable-next-line unicorn/no-useless-undefined
47
+ expect(redact(undefined)).toBeUndefined();
48
+ expect(redact(42 as any)).toBe(42);
49
+ });
50
+
51
+ it('should handle empty paths array', () => {
52
+ const redact = createRedactor({ paths: [] });
53
+ expect(redact({ password: 'secret' })).toEqual({ password: 'secret' });
54
+ });
55
+ });
56
+
57
+ describe('deep paths', () => {
58
+ it('should redact nested fields', () => {
59
+ const redact = createRedactor({ paths: ['user.email'] });
60
+ expect(redact({ user: { email: 'a@b.com', name: 'Alice' } })).toEqual({
61
+ user: { email: '[Redacted]', name: 'Alice' },
62
+ });
63
+ });
64
+
65
+ it('should redact deeply nested fields', () => {
66
+ const redact = createRedactor({ paths: ['a.b.c'] });
67
+ expect(redact({ a: { b: { c: 'secret', d: 'ok' } } })).toEqual({
68
+ a: { b: { c: '[Redacted]', d: 'ok' } },
69
+ });
70
+ });
71
+
72
+ it('should redact multiple nested paths', () => {
73
+ const redact = createRedactor({
74
+ paths: ['user.email', 'user.password'],
75
+ });
76
+ expect(
77
+ redact({ user: { email: 'a@b.com', password: 's', name: 'A' } }),
78
+ ).toEqual({
79
+ user: { email: '[Redacted]', password: '[Redacted]', name: 'A' },
80
+ });
81
+ });
82
+
83
+ it('should handle missing intermediate objects', () => {
84
+ const redact = createRedactor({ paths: ['user.email'] });
85
+ expect(redact({ other: true })).toEqual({ other: true });
86
+ });
87
+ });
88
+
89
+ describe('wildcards', () => {
90
+ it('should redact all properties with top-level wildcard', () => {
91
+ const redact = createRedactor({ paths: ['secrets.*'] });
92
+ expect(redact({ secrets: { a: '1', b: '2' } })).toEqual({
93
+ secrets: { a: '[Redacted]', b: '[Redacted]' },
94
+ });
95
+ });
96
+
97
+ it('should redact all array items with wildcard', () => {
98
+ const redact = createRedactor({ paths: ['users[*].password'] });
99
+ expect(
100
+ redact({
101
+ users: [
102
+ { name: 'A', password: 'p1' },
103
+ { name: 'B', password: 'p2' },
104
+ ],
105
+ }),
106
+ ).toEqual({
107
+ users: [
108
+ { name: 'A', password: '[Redacted]' },
109
+ { name: 'B', password: '[Redacted]' },
110
+ ],
111
+ });
112
+ });
113
+
114
+ it('should handle wildcard at root level', () => {
115
+ const redact = createRedactor({ paths: ['[*].secret'] });
116
+ expect(
117
+ redact([
118
+ { secret: 'a', id: 1 },
119
+ { secret: 'b', id: 2 },
120
+ ]),
121
+ ).toEqual([
122
+ { secret: '[Redacted]', id: 1 },
123
+ { secret: '[Redacted]', id: 2 },
124
+ ]);
125
+ });
126
+
127
+ it('should handle wildcard on non-object gracefully', () => {
128
+ const redact = createRedactor({ paths: ['items.*'] });
129
+ expect(redact({ items: 'string-value' })).toEqual({
130
+ items: 'string-value',
131
+ });
132
+ });
133
+
134
+ it('should handle wildcard on empty object', () => {
135
+ const redact = createRedactor({ paths: ['data.*'] });
136
+ expect(redact({ data: {} })).toEqual({ data: {} });
137
+ });
138
+ });
139
+
140
+ describe('custom censor', () => {
141
+ it('should use a custom censor string', () => {
142
+ const redact = createRedactor({
143
+ paths: ['password'],
144
+ censor: '***',
145
+ });
146
+ expect(redact({ password: 'secret' })).toEqual({ password: '***' });
147
+ });
148
+
149
+ it('should use a censor function', () => {
150
+ const redact = createRedactor({
151
+ paths: ['ccn'],
152
+ censor: (val) => '****' + String(val).slice(-4),
153
+ });
154
+ expect(redact({ ccn: '4111111111111234' })).toEqual({
155
+ ccn: '****1234',
156
+ });
157
+ });
158
+
159
+ it('should pass the original value to censor function', () => {
160
+ let received: unknown;
161
+ const redact = createRedactor({
162
+ paths: ['secret'],
163
+ censor: (val) => {
164
+ received = val;
165
+ return 'hidden';
166
+ },
167
+ });
168
+ redact({ secret: 'my-secret' });
169
+ expect(received).toBe('my-secret');
170
+ });
171
+ });
172
+
173
+ describe('pino-style paths', () => {
174
+ it('should redact typical sensitive fields', () => {
175
+ const redact = createRedactor({
176
+ paths: ['password', 'token', 'authorization', 'cookie', 'secret'],
177
+ });
178
+ const input = {
179
+ password: 'hunter2',
180
+ token: 'jwt-abc',
181
+ authorization: 'Bearer xyz',
182
+ cookie: 'sid=123',
183
+ secret: 'key',
184
+ safe: 'visible',
185
+ };
186
+ expect(redact(input)).toEqual({
187
+ password: '[Redacted]',
188
+ token: '[Redacted]',
189
+ authorization: '[Redacted]',
190
+ cookie: '[Redacted]',
191
+ secret: '[Redacted]',
192
+ safe: 'visible',
193
+ });
194
+ });
195
+
196
+ it('should redact header authorization paths', () => {
197
+ const redact = createRedactor({
198
+ paths: ['req.headers.authorization', 'req.headers.cookie'],
199
+ });
200
+ expect(
201
+ redact({
202
+ req: {
203
+ headers: {
204
+ authorization: 'Bearer token',
205
+ cookie: 'sid=abc',
206
+ 'content-type': 'application/json',
207
+ },
208
+ },
209
+ }),
210
+ ).toEqual({
211
+ req: {
212
+ headers: {
213
+ authorization: '[Redacted]',
214
+ cookie: '[Redacted]',
215
+ 'content-type': 'application/json',
216
+ },
217
+ },
218
+ });
219
+ });
220
+ });
221
+
222
+ describe('does not mutate original', () => {
223
+ it('should not mutate the original object', () => {
224
+ const redact = createRedactor({ paths: ['password'] });
225
+ const original = { password: 'secret', name: 'Alice' };
226
+ redact(original);
227
+ expect(original.password).toBe('secret');
228
+ });
229
+
230
+ it('should not mutate nested objects', () => {
231
+ const redact = createRedactor({ paths: ['user.email'] });
232
+ const original = { user: { email: 'a@b.com' } };
233
+ redact(original);
234
+ expect(original.user.email).toBe('a@b.com');
235
+ });
236
+
237
+ it('should not mutate array items', () => {
238
+ const redact = createRedactor({ paths: ['items[*].secret'] });
239
+ const original = { items: [{ secret: 'a' }, { secret: 'b' }] };
240
+ redact(original);
241
+ expect(original.items[0].secret).toBe('a');
242
+ });
243
+ });
244
+
245
+ describe('overlapping paths', () => {
246
+ it('should handle parent and child paths together', () => {
247
+ // ['user', 'user.email'] — user is a redact target, but user.email should also be handled
248
+ const redact = createRedactor({ paths: ['user', 'user.email'] });
249
+ const result = redact({
250
+ user: { email: 'a@b.com', name: 'Alice' },
251
+ }) as any;
252
+ // Both paths are in the tree. 'user' is marked redact AND has children.
253
+ // The nested email should be redacted.
254
+ expect(result.user.email).toBe('[Redacted]');
255
+ expect(result.user.name).toBe('Alice');
256
+ });
257
+
258
+ it('should handle child before parent in path list', () => {
259
+ const redact = createRedactor({ paths: ['user.email', 'user'] });
260
+ const result = redact({
261
+ user: { email: 'a@b.com', name: 'Alice' },
262
+ }) as any;
263
+ expect(result.user.email).toBe('[Redacted]');
264
+ });
265
+
266
+ it('should handle deeply overlapping paths', () => {
267
+ const redact = createRedactor({ paths: ['a', 'a.b', 'a.b.c'] });
268
+ const result = redact({ a: { b: { c: 'deep', d: 'ok' } } }) as any;
269
+ expect(result.a.b.c).toBe('[Redacted]');
270
+ expect(result.a.b.d).toBe('ok');
271
+ });
272
+
273
+ it('should handle sibling paths sharing a parent', () => {
274
+ const redact = createRedactor({ paths: ['user.email', 'user.password'] });
275
+ const result = redact({
276
+ user: { email: 'a@b.com', password: 's', name: 'A' },
277
+ }) as any;
278
+ expect(result.user.email).toBe('[Redacted]');
279
+ expect(result.user.password).toBe('[Redacted]');
280
+ expect(result.user.name).toBe('A');
281
+ });
282
+ });
283
+
284
+ describe('non-plain objects', () => {
285
+ it('should pass Date through by reference', () => {
286
+ const redact = createRedactor({ paths: ['secret'] });
287
+ const date = new Date('2025-01-01');
288
+ const result = redact({ secret: 'x', created: date }) as any;
289
+ expect(result.created).toBe(date);
290
+ expect(result.created).toBeInstanceOf(Date);
291
+ });
292
+
293
+ it('should pass class instances through by reference', () => {
294
+ class Custom {
295
+ constructor(public value: string) {}
296
+ greet() {
297
+ return this.value;
298
+ }
299
+ }
300
+ const redact = createRedactor({ paths: ['password'] });
301
+ const obj = new Custom('hello');
302
+ const result = redact({ password: 's', thing: obj }) as any;
303
+ expect(result.thing).toBe(obj);
304
+ expect(result.thing.greet()).toBe('hello');
305
+ });
306
+
307
+ it('should pass Map through by reference', () => {
308
+ const redact = createRedactor({ paths: ['secret'] });
309
+ const map = new Map([['key', 'val']]);
310
+ const result = redact({ secret: 'x', data: map }) as any;
311
+ expect(result.data).toBe(map);
312
+ });
313
+
314
+ it('should still deep-clone nested plain objects inside arrays', () => {
315
+ const redact = createRedactor({ paths: ['items[*].password'] });
316
+ const original = { items: [{ password: 'p', name: 'A' }] };
317
+ const result = redact(original) as any;
318
+ expect(result.items[0].password).toBe('[Redacted]');
319
+ expect(original.items[0].password).toBe('p');
320
+ });
321
+ });
322
+
323
+ describe('parsePath validation', () => {
324
+ it('should throw on empty path string', () => {
325
+ expect(() => createRedactor({ paths: [''] })).toThrow(/non-empty string/);
326
+ });
327
+
328
+ it('should throw on double-dot paths', () => {
329
+ expect(() => createRedactor({ paths: ['user..email'] })).toThrow(
330
+ /empty segment/,
331
+ );
332
+ });
333
+
334
+ it('should throw on unclosed bracket', () => {
335
+ expect(() => createRedactor({ paths: ['user['] })).toThrow(
336
+ /unclosed bracket/,
337
+ );
338
+ });
339
+
340
+ it('should throw on trailing garbage after bracket', () => {
341
+ expect(() => createRedactor({ paths: ['foo.]bar'] })).toThrow(/bracket/);
342
+ });
343
+
344
+ it('should accept valid paths without throwing', () => {
345
+ expect(() => createRedactor({ paths: ['a'] })).not.toThrow();
346
+ expect(() => createRedactor({ paths: ['a.b'] })).not.toThrow();
347
+ expect(() => createRedactor({ paths: ['a[*].b'] })).not.toThrow();
348
+ expect(() => createRedactor({ paths: ['a.*.b'] })).not.toThrow();
349
+ expect(() => createRedactor({ paths: ['[0].secret'] })).not.toThrow();
350
+ });
351
+ });
352
+
353
+ describe('return type preserves input type', () => {
354
+ it('should return the same type as input', () => {
355
+ const redact = createRedactor({ paths: ['password'] });
356
+ const input = { password: 's', count: 42, active: true };
357
+ const result = redact(input);
358
+ // TypeScript should infer result as { password: string; count: number; active: boolean }
359
+ expect(result.count).toBe(42);
360
+ expect(result.active).toBe(true);
361
+ });
362
+ });
363
+
364
+ describe('presets', () => {
365
+ it('should create a redactor from "default" preset', () => {
366
+ const redact = createRedactor('default');
367
+ const result = redact({
368
+ password: 'hunter2',
369
+ secret: 'key',
370
+ token: 'jwt-abc',
371
+ authorization: 'Bearer xyz',
372
+ safe: 'visible',
373
+ req: { headers: { authorization: 'Bearer xyz', cookie: 'sid=1' } },
374
+ }) as any;
375
+
376
+ expect(result.password).toBe('[Redacted]');
377
+ expect(result.secret).toBe('[Redacted]');
378
+ expect(result.token).toBe('[Redacted]');
379
+ expect(result.authorization).toBe('[Redacted]');
380
+ expect(result.safe).toBe('visible');
381
+ expect(result.req.headers.authorization).toBe('[Redacted]');
382
+ expect(result.req.headers.cookie).toBe('[Redacted]');
383
+ });
384
+
385
+ it('should create a redactor from "strict" preset', () => {
386
+ const redact = createRedactor('strict');
387
+ const result = redact({
388
+ password: 's',
389
+ accessToken: 'tok',
390
+ refreshToken: 'rtok',
391
+ clientSecret: 'cs',
392
+ safe: 'ok',
393
+ }) as any;
394
+
395
+ expect(result.password).toBe('[Redacted]');
396
+ expect(result.accessToken).toBe('[Redacted]');
397
+ expect(result.refreshToken).toBe('[Redacted]');
398
+ expect(result.clientSecret).toBe('[Redacted]');
399
+ expect(result.safe).toBe('ok');
400
+ });
401
+
402
+ it('should create a redactor from "pci-dss" preset', () => {
403
+ const redact = createRedactor('pci-dss');
404
+ const result = redact({
405
+ cardNumber: '4111111111111111',
406
+ cvv: '123',
407
+ pan: '4111111111111111',
408
+ safe: 'ok',
409
+ }) as any;
410
+
411
+ expect(result.cardNumber).toBe('[Redacted]');
412
+ expect(result.cvv).toBe('[Redacted]');
413
+ expect(result.pan).toBe('[Redacted]');
414
+ expect(result.safe).toBe('ok');
415
+ });
416
+
417
+ it('should throw on unknown preset', () => {
418
+ expect(() => createRedactor('unknown' as any)).toThrow(
419
+ /Unknown redactor preset/,
420
+ );
421
+ });
422
+
423
+ it('should export REDACT_PRESETS with all three presets', () => {
424
+ expect(REDACT_PRESETS.default).toBeDefined();
425
+ expect(REDACT_PRESETS.strict).toBeDefined();
426
+ expect(REDACT_PRESETS['pci-dss']).toBeDefined();
427
+ expect(REDACT_PRESETS.default.paths).toContain('password');
428
+ expect(REDACT_PRESETS.strict.paths).toContain('accessToken');
429
+ expect(REDACT_PRESETS['pci-dss'].paths).toContain('cvv');
430
+ });
431
+ });
432
+ });