825 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			825 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
const { test } = require('node:test')
 | 
						|
const { strict: assert } = require('node:assert')
 | 
						|
const slowRedact = require('../index.js')
 | 
						|
 | 
						|
test('basic path redaction', () => {
 | 
						|
  const obj = {
 | 
						|
    headers: {
 | 
						|
      cookie: 'secret-cookie',
 | 
						|
      authorization: 'Bearer token'
 | 
						|
    },
 | 
						|
    body: { message: 'hello' }
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({ paths: ['headers.cookie'] })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  // Original object should remain unchanged
 | 
						|
  assert.strictEqual(obj.headers.cookie, 'secret-cookie')
 | 
						|
 | 
						|
  // Result should have redacted path
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.headers.cookie, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.headers.authorization, 'Bearer token')
 | 
						|
  assert.strictEqual(parsed.body.message, 'hello')
 | 
						|
})
 | 
						|
 | 
						|
test('multiple paths redaction', () => {
 | 
						|
  const obj = {
 | 
						|
    user: { name: 'john', password: 'secret' },
 | 
						|
    session: { token: 'abc123' }
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['user.password', 'session.token']
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  // Original unchanged
 | 
						|
  assert.strictEqual(obj.user.password, 'secret')
 | 
						|
  assert.strictEqual(obj.session.token, 'abc123')
 | 
						|
 | 
						|
  // Result redacted
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.user.password, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.session.token, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.user.name, 'john')
 | 
						|
})
 | 
						|
 | 
						|
test('custom censor value', () => {
 | 
						|
  const obj = { secret: 'hidden' }
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['secret'],
 | 
						|
    censor: '***'
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.secret, '***')
 | 
						|
})
 | 
						|
 | 
						|
test('serialize: false returns object with restore method', () => {
 | 
						|
  const obj = { secret: 'hidden' }
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['secret'],
 | 
						|
    serialize: false
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  // Should be object, not string
 | 
						|
  assert.strictEqual(typeof result, 'object')
 | 
						|
  assert.strictEqual(result.secret, '[REDACTED]')
 | 
						|
 | 
						|
  // Should have restore method
 | 
						|
  assert.strictEqual(typeof result.restore, 'function')
 | 
						|
 | 
						|
  const restored = result.restore()
 | 
						|
  assert.strictEqual(restored.secret, 'hidden')
 | 
						|
})
 | 
						|
 | 
						|
test('bracket notation paths', () => {
 | 
						|
  const obj = {
 | 
						|
    'weird-key': { 'another-weird': 'secret' },
 | 
						|
    normal: 'public'
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['["weird-key"]["another-weird"]']
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed['weird-key']['another-weird'], '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.normal, 'public')
 | 
						|
})
 | 
						|
 | 
						|
test('array paths', () => {
 | 
						|
  const obj = {
 | 
						|
    users: [
 | 
						|
      { name: 'john', password: 'secret1' },
 | 
						|
      { name: 'jane', password: 'secret2' }
 | 
						|
    ]
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['users[0].password', 'users[1].password']
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.users[0].password, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.users[1].password, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.users[0].name, 'john')
 | 
						|
  assert.strictEqual(parsed.users[1].name, 'jane')
 | 
						|
})
 | 
						|
 | 
						|
test('wildcard at end of path', () => {
 | 
						|
  const obj = {
 | 
						|
    secrets: {
 | 
						|
      key1: 'secret1',
 | 
						|
      key2: 'secret2'
 | 
						|
    },
 | 
						|
    public: 'data'
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['secrets.*']
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.secrets.key1, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.secrets.key2, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.public, 'data')
 | 
						|
})
 | 
						|
 | 
						|
test('wildcard with arrays', () => {
 | 
						|
  const obj = {
 | 
						|
    items: ['secret1', 'secret2', 'secret3']
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['items.*']
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.items[0], '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.items[1], '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.items[2], '[REDACTED]')
 | 
						|
})
 | 
						|
 | 
						|
test('intermediate wildcard', () => {
 | 
						|
  const obj = {
 | 
						|
    users: {
 | 
						|
      user1: { password: 'secret1' },
 | 
						|
      user2: { password: 'secret2' }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['users.*.password']
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.users.user1.password, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.users.user2.password, '[REDACTED]')
 | 
						|
})
 | 
						|
 | 
						|
test('censor function', () => {
 | 
						|
  const obj = { secret: 'hidden' }
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['secret'],
 | 
						|
    censor: (value, path) => `REDACTED:${path.join('.')}`
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.secret, 'REDACTED:secret')
 | 
						|
})
 | 
						|
 | 
						|
test('custom serialize function', () => {
 | 
						|
  const obj = { secret: 'hidden', public: 'data' }
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['secret'],
 | 
						|
    serialize: (obj) => `custom:${JSON.stringify(obj)}`
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  assert(result.startsWith('custom:'))
 | 
						|
  const parsed = JSON.parse(result.slice(7))
 | 
						|
  assert.strictEqual(parsed.secret, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.public, 'data')
 | 
						|
})
 | 
						|
 | 
						|
test('nested paths', () => {
 | 
						|
  const obj = {
 | 
						|
    level1: {
 | 
						|
      level2: {
 | 
						|
        level3: {
 | 
						|
          secret: 'hidden'
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['level1.level2.level3.secret']
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.level1.level2.level3.secret, '[REDACTED]')
 | 
						|
})
 | 
						|
 | 
						|
test('non-existent paths are ignored', () => {
 | 
						|
  const obj = { existing: 'value' }
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['nonexistent.path']
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.existing, 'value')
 | 
						|
  assert.strictEqual(parsed.nonexistent, undefined)
 | 
						|
})
 | 
						|
 | 
						|
test('null and undefined handling', () => {
 | 
						|
  const obj = {
 | 
						|
    nullValue: null,
 | 
						|
    undefinedValue: undefined,
 | 
						|
    nested: {
 | 
						|
      nullValue: null
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['nullValue', 'nested.nullValue']
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.nullValue, '[REDACTED]')
 | 
						|
  assert.strictEqual(parsed.nested.nullValue, '[REDACTED]')
 | 
						|
})
 | 
						|
 | 
						|
test('original object remains unchanged', () => {
 | 
						|
  const original = {
 | 
						|
    secret: 'hidden',
 | 
						|
    nested: { secret: 'hidden2' }
 | 
						|
  }
 | 
						|
  const copy = JSON.parse(JSON.stringify(original))
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['secret', 'nested.secret']
 | 
						|
  })
 | 
						|
  redact(original)
 | 
						|
 | 
						|
  // Original should be completely unchanged
 | 
						|
  assert.deepStrictEqual(original, copy)
 | 
						|
})
 | 
						|
 | 
						|
test('strict mode with primitives', () => {
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['test'],
 | 
						|
    strict: true
 | 
						|
  })
 | 
						|
 | 
						|
  const stringResult = redact('primitive')
 | 
						|
  assert.strictEqual(stringResult, '"primitive"')
 | 
						|
 | 
						|
  const numberResult = redact(42)
 | 
						|
  assert.strictEqual(numberResult, '42')
 | 
						|
})
 | 
						|
 | 
						|
// Path validation tests to match fast-redact behavior
 | 
						|
test('path validation - non-string paths should throw', () => {
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: [123] })
 | 
						|
  }, {
 | 
						|
    message: 'Paths must be (non-empty) strings'
 | 
						|
  })
 | 
						|
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: [null] })
 | 
						|
  }, {
 | 
						|
    message: 'Paths must be (non-empty) strings'
 | 
						|
  })
 | 
						|
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: [undefined] })
 | 
						|
  }, {
 | 
						|
    message: 'Paths must be (non-empty) strings'
 | 
						|
  })
 | 
						|
})
 | 
						|
 | 
						|
test('path validation - empty string should throw', () => {
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: [''] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path ()'
 | 
						|
  })
 | 
						|
})
 | 
						|
 | 
						|
test('path validation - double dots should throw', () => {
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['invalid..path'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (invalid..path)'
 | 
						|
  })
 | 
						|
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['a..b..c'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (a..b..c)'
 | 
						|
  })
 | 
						|
})
 | 
						|
 | 
						|
test('path validation - unmatched brackets should throw', () => {
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['invalid[unclosed'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (invalid[unclosed)'
 | 
						|
  })
 | 
						|
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['invalid]unopened'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (invalid]unopened)'
 | 
						|
  })
 | 
						|
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['nested[a[b]'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (nested[a[b])'
 | 
						|
  })
 | 
						|
})
 | 
						|
 | 
						|
test('path validation - comma-separated paths should throw', () => {
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['req,headers.cookie'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (req,headers.cookie)'
 | 
						|
  })
 | 
						|
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['user,profile,name'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (user,profile,name)'
 | 
						|
  })
 | 
						|
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['a,b'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (a,b)'
 | 
						|
  })
 | 
						|
})
 | 
						|
 | 
						|
test('path validation - mixed valid and invalid should throw', () => {
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['valid.path', 123, 'another.valid'] })
 | 
						|
  }, {
 | 
						|
    message: 'Paths must be (non-empty) strings'
 | 
						|
  })
 | 
						|
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['valid.path', 'invalid..path'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (invalid..path)'
 | 
						|
  })
 | 
						|
 | 
						|
  assert.throws(() => {
 | 
						|
    slowRedact({ paths: ['valid.path', 'req,headers.cookie'] })
 | 
						|
  }, {
 | 
						|
    message: 'Invalid redaction path (req,headers.cookie)'
 | 
						|
  })
 | 
						|
})
 | 
						|
 | 
						|
test('path validation - valid paths should work', () => {
 | 
						|
  // These should not throw
 | 
						|
  assert.doesNotThrow(() => {
 | 
						|
    slowRedact({ paths: [] })
 | 
						|
  })
 | 
						|
 | 
						|
  assert.doesNotThrow(() => {
 | 
						|
    slowRedact({ paths: ['valid.path'] })
 | 
						|
  })
 | 
						|
 | 
						|
  assert.doesNotThrow(() => {
 | 
						|
    slowRedact({ paths: ['user.password', 'data[0].secret'] })
 | 
						|
  })
 | 
						|
 | 
						|
  assert.doesNotThrow(() => {
 | 
						|
    slowRedact({ paths: ['["quoted-key"].value'] })
 | 
						|
  })
 | 
						|
 | 
						|
  assert.doesNotThrow(() => {
 | 
						|
    slowRedact({ paths: ["['single-quoted'].value"] })
 | 
						|
  })
 | 
						|
 | 
						|
  assert.doesNotThrow(() => {
 | 
						|
    slowRedact({ paths: ['array[0]', 'object.property', 'wildcard.*'] })
 | 
						|
  })
 | 
						|
})
 | 
						|
 | 
						|
// fast-redact compatibility tests
 | 
						|
test('censor function receives path as array (fast-redact compatibility)', () => {
 | 
						|
  const obj = {
 | 
						|
    headers: {
 | 
						|
      authorization: 'Bearer token',
 | 
						|
      'x-api-key': 'secret-key'
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const pathsReceived = []
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['headers.authorization', 'headers["x-api-key"]'],
 | 
						|
    censor: (value, path) => {
 | 
						|
      pathsReceived.push(path)
 | 
						|
      assert(Array.isArray(path), 'Path should be an array')
 | 
						|
      return '[REDACTED]'
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  redact(obj)
 | 
						|
 | 
						|
  // Verify paths are arrays
 | 
						|
  assert.strictEqual(pathsReceived.length, 2)
 | 
						|
  assert.deepStrictEqual(pathsReceived[0], ['headers', 'authorization'])
 | 
						|
  assert.deepStrictEqual(pathsReceived[1], ['headers', 'x-api-key'])
 | 
						|
})
 | 
						|
 | 
						|
test('censor function with nested paths receives correct array', () => {
 | 
						|
  const obj = {
 | 
						|
    user: {
 | 
						|
      profile: {
 | 
						|
        credentials: {
 | 
						|
          password: 'secret123'
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  let receivedPath
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['user.profile.credentials.password'],
 | 
						|
    censor: (value, path) => {
 | 
						|
      receivedPath = path
 | 
						|
      assert.strictEqual(value, 'secret123')
 | 
						|
      assert(Array.isArray(path))
 | 
						|
      return '[REDACTED]'
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  redact(obj)
 | 
						|
 | 
						|
  assert.deepStrictEqual(receivedPath, ['user', 'profile', 'credentials', 'password'])
 | 
						|
})
 | 
						|
 | 
						|
test('censor function with wildcards receives correct array paths', () => {
 | 
						|
  const obj = {
 | 
						|
    users: {
 | 
						|
      user1: { password: 'secret1' },
 | 
						|
      user2: { password: 'secret2' }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const pathsReceived = []
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['users.*.password'],
 | 
						|
    censor: (value, path) => {
 | 
						|
      pathsReceived.push([...path]) // copy the array
 | 
						|
      assert(Array.isArray(path))
 | 
						|
      return '[REDACTED]'
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  redact(obj)
 | 
						|
 | 
						|
  assert.strictEqual(pathsReceived.length, 2)
 | 
						|
  assert.deepStrictEqual(pathsReceived[0], ['users', 'user1', 'password'])
 | 
						|
  assert.deepStrictEqual(pathsReceived[1], ['users', 'user2', 'password'])
 | 
						|
})
 | 
						|
 | 
						|
test('censor function with array wildcard receives correct array paths', () => {
 | 
						|
  const obj = {
 | 
						|
    items: [
 | 
						|
      { secret: 'value1' },
 | 
						|
      { secret: 'value2' }
 | 
						|
    ]
 | 
						|
  }
 | 
						|
 | 
						|
  const pathsReceived = []
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['items.*.secret'],
 | 
						|
    censor: (value, path) => {
 | 
						|
      pathsReceived.push([...path])
 | 
						|
      assert(Array.isArray(path))
 | 
						|
      return '[REDACTED]'
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  redact(obj)
 | 
						|
 | 
						|
  assert.strictEqual(pathsReceived.length, 2)
 | 
						|
  assert.deepStrictEqual(pathsReceived[0], ['items', '0', 'secret'])
 | 
						|
  assert.deepStrictEqual(pathsReceived[1], ['items', '1', 'secret'])
 | 
						|
})
 | 
						|
 | 
						|
test('censor function with end wildcard receives correct array paths', () => {
 | 
						|
  const obj = {
 | 
						|
    secrets: {
 | 
						|
      key1: 'secret1',
 | 
						|
      key2: 'secret2'
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const pathsReceived = []
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['secrets.*'],
 | 
						|
    censor: (value, path) => {
 | 
						|
      pathsReceived.push([...path])
 | 
						|
      assert(Array.isArray(path))
 | 
						|
      return '[REDACTED]'
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  redact(obj)
 | 
						|
 | 
						|
  assert.strictEqual(pathsReceived.length, 2)
 | 
						|
  // Sort paths for consistent testing since object iteration order isn't guaranteed
 | 
						|
  pathsReceived.sort((a, b) => a[1].localeCompare(b[1]))
 | 
						|
  assert.deepStrictEqual(pathsReceived[0], ['secrets', 'key1'])
 | 
						|
  assert.deepStrictEqual(pathsReceived[1], ['secrets', 'key2'])
 | 
						|
})
 | 
						|
 | 
						|
test('type safety: accessing properties on primitive values should not throw', () => {
 | 
						|
  // Test case from GitHub issue #5
 | 
						|
  const redactor = slowRedact({ paths: ['headers.authorization'] })
 | 
						|
  const data = {
 | 
						|
    headers: 123 // primitive value
 | 
						|
  }
 | 
						|
 | 
						|
  assert.doesNotThrow(() => {
 | 
						|
    const result = redactor(data)
 | 
						|
    const parsed = JSON.parse(result)
 | 
						|
    assert.strictEqual(parsed.headers, 123) // Should remain unchanged
 | 
						|
  })
 | 
						|
 | 
						|
  // Test wildcards with primitives
 | 
						|
  const redactor2 = slowRedact({ paths: ['data.*.nested'] })
 | 
						|
  const data2 = {
 | 
						|
    data: {
 | 
						|
      item1: 123, // primitive, trying to access .nested on it
 | 
						|
      item2: { nested: 'secret' }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  assert.doesNotThrow(() => {
 | 
						|
    const result2 = redactor2(data2)
 | 
						|
    const parsed2 = JSON.parse(result2)
 | 
						|
    assert.strictEqual(parsed2.data.item1, 123) // Primitive unchanged
 | 
						|
    assert.strictEqual(parsed2.data.item2.nested, '[REDACTED]') // Object property redacted
 | 
						|
  })
 | 
						|
 | 
						|
  // Test deep nested access on primitives
 | 
						|
  const redactor3 = slowRedact({ paths: ['user.name.first.charAt'] })
 | 
						|
  const data3 = {
 | 
						|
    user: {
 | 
						|
      name: 'John' // string primitive
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  assert.doesNotThrow(() => {
 | 
						|
    const result3 = redactor3(data3)
 | 
						|
    const parsed3 = JSON.parse(result3)
 | 
						|
    assert.strictEqual(parsed3.user.name, 'John') // Should remain unchanged
 | 
						|
  })
 | 
						|
})
 | 
						|
 | 
						|
// Remove option tests
 | 
						|
test('remove option: basic key removal', () => {
 | 
						|
  const obj = { username: 'john', password: 'secret123' }
 | 
						|
  const redact = slowRedact({ paths: ['password'], remove: true })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  // Original object should remain unchanged
 | 
						|
  assert.strictEqual(obj.password, 'secret123')
 | 
						|
 | 
						|
  // Result should have password completely removed
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.username, 'john')
 | 
						|
  assert.strictEqual('password' in parsed, false)
 | 
						|
  assert.strictEqual(parsed.password, undefined)
 | 
						|
})
 | 
						|
 | 
						|
test('remove option: multiple paths removal', () => {
 | 
						|
  const obj = {
 | 
						|
    user: { name: 'john', password: 'secret' },
 | 
						|
    session: { token: 'abc123', id: 'session1' }
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['user.password', 'session.token'],
 | 
						|
    remove: true
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  // Original unchanged
 | 
						|
  assert.strictEqual(obj.user.password, 'secret')
 | 
						|
  assert.strictEqual(obj.session.token, 'abc123')
 | 
						|
 | 
						|
  // Result has keys completely removed
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.user.name, 'john')
 | 
						|
  assert.strictEqual(parsed.session.id, 'session1')
 | 
						|
  assert.strictEqual('password' in parsed.user, false)
 | 
						|
  assert.strictEqual('token' in parsed.session, false)
 | 
						|
})
 | 
						|
 | 
						|
test('remove option: wildcard removal', () => {
 | 
						|
  const obj = {
 | 
						|
    secrets: {
 | 
						|
      key1: 'secret1',
 | 
						|
      key2: 'secret2'
 | 
						|
    },
 | 
						|
    public: 'data'
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['secrets.*'],
 | 
						|
    remove: true
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.public, 'data')
 | 
						|
  assert.deepStrictEqual(parsed.secrets, {}) // All keys removed
 | 
						|
})
 | 
						|
 | 
						|
test('remove option: array wildcard removal', () => {
 | 
						|
  const obj = {
 | 
						|
    items: ['secret1', 'secret2', 'secret3'],
 | 
						|
    meta: 'data'
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['items.*'],
 | 
						|
    remove: true
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.meta, 'data')
 | 
						|
  // Array items set to undefined are omitted by JSON.stringify
 | 
						|
  assert.deepStrictEqual(parsed.items, [null, null, null])
 | 
						|
})
 | 
						|
 | 
						|
test('remove option: intermediate wildcard removal', () => {
 | 
						|
  const obj = {
 | 
						|
    users: {
 | 
						|
      user1: { password: 'secret1', name: 'john' },
 | 
						|
      user2: { password: 'secret2', name: 'jane' }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['users.*.password'],
 | 
						|
    remove: true
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.users.user1.name, 'john')
 | 
						|
  assert.strictEqual(parsed.users.user2.name, 'jane')
 | 
						|
  assert.strictEqual('password' in parsed.users.user1, false)
 | 
						|
  assert.strictEqual('password' in parsed.users.user2, false)
 | 
						|
})
 | 
						|
 | 
						|
test('remove option: serialize false returns object with removed keys', () => {
 | 
						|
  const obj = { secret: 'hidden', public: 'data' }
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['secret'],
 | 
						|
    remove: true,
 | 
						|
    serialize: false
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  // Should be object, not string
 | 
						|
  assert.strictEqual(typeof result, 'object')
 | 
						|
  assert.strictEqual(result.public, 'data')
 | 
						|
  assert.strictEqual('secret' in result, false)
 | 
						|
 | 
						|
  // Should have restore method
 | 
						|
  assert.strictEqual(typeof result.restore, 'function')
 | 
						|
 | 
						|
  const restored = result.restore()
 | 
						|
  assert.strictEqual(restored.secret, 'hidden')
 | 
						|
})
 | 
						|
 | 
						|
test('remove option: non-existent paths are ignored', () => {
 | 
						|
  const obj = { existing: 'value' }
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['nonexistent.path'],
 | 
						|
    remove: true
 | 
						|
  })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed.existing, 'value')
 | 
						|
  assert.strictEqual(parsed.nonexistent, undefined)
 | 
						|
})
 | 
						|
 | 
						|
// Test for Issue #13: Empty string bracket notation paths not being redacted correctly
 | 
						|
test('empty string bracket notation path', () => {
 | 
						|
  const obj = { '': { c: 'sensitive-data' } }
 | 
						|
  const redact = slowRedact({ paths: ["[''].c"] })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  // Original object should remain unchanged
 | 
						|
  assert.strictEqual(obj[''].c, 'sensitive-data')
 | 
						|
 | 
						|
  // Result should have redacted path
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed[''].c, '[REDACTED]')
 | 
						|
})
 | 
						|
 | 
						|
test('empty string bracket notation with double quotes', () => {
 | 
						|
  const obj = { '': { c: 'sensitive-data' } }
 | 
						|
  const redact = slowRedact({ paths: ['[""].c'] })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  // Original object should remain unchanged
 | 
						|
  assert.strictEqual(obj[''].c, 'sensitive-data')
 | 
						|
 | 
						|
  // Result should have redacted path
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed[''].c, '[REDACTED]')
 | 
						|
})
 | 
						|
 | 
						|
test('empty string key with nested bracket notation', () => {
 | 
						|
  const obj = { '': { '': { secret: 'value' } } }
 | 
						|
  const redact = slowRedact({ paths: ["[''][''].secret"] })
 | 
						|
  const result = redact(obj)
 | 
						|
 | 
						|
  // Original object should remain unchanged
 | 
						|
  assert.strictEqual(obj[''][''].secret, 'value')
 | 
						|
 | 
						|
  // Result should have redacted path
 | 
						|
  const parsed = JSON.parse(result)
 | 
						|
  assert.strictEqual(parsed[''][''].secret, '[REDACTED]')
 | 
						|
})
 | 
						|
 | 
						|
// Test for Pino issue #2313: censor should only be called when path exists
 | 
						|
test('censor function not called for non-existent paths', () => {
 | 
						|
  let censorCallCount = 0
 | 
						|
  const censorCalls = []
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['a.b.c', 'req.authorization', 'url'],
 | 
						|
    serialize: false,
 | 
						|
    censor (value, path) {
 | 
						|
      censorCallCount++
 | 
						|
      censorCalls.push({ value, path: path.slice() })
 | 
						|
      return '***'
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  // Test case 1: { req: { id: 'test' } }
 | 
						|
  // req.authorization doesn't exist, censor should not be called for it
 | 
						|
  censorCallCount = 0
 | 
						|
  censorCalls.length = 0
 | 
						|
  redact({ req: { id: 'test' } })
 | 
						|
 | 
						|
  // Should not have been called for any path since none exist
 | 
						|
  assert.strictEqual(censorCallCount, 0, 'censor should not be called when paths do not exist')
 | 
						|
 | 
						|
  // Test case 2: { a: { d: 'test' } }
 | 
						|
  // a.b.c doesn't exist (a.d exists, but not a.b.c)
 | 
						|
  censorCallCount = 0
 | 
						|
  redact({ a: { d: 'test' } })
 | 
						|
  assert.strictEqual(censorCallCount, 0)
 | 
						|
 | 
						|
  // Test case 3: paths that do exist should still call censor
 | 
						|
  censorCallCount = 0
 | 
						|
  censorCalls.length = 0
 | 
						|
  const result = redact({ req: { authorization: 'bearer token' } })
 | 
						|
  assert.strictEqual(censorCallCount, 1, 'censor should be called when path exists')
 | 
						|
  assert.deepStrictEqual(censorCalls[0].path, ['req', 'authorization'])
 | 
						|
  assert.strictEqual(censorCalls[0].value, 'bearer token')
 | 
						|
  assert.strictEqual(result.req.authorization, '***')
 | 
						|
})
 | 
						|
 | 
						|
test('censor function not called for non-existent nested paths', () => {
 | 
						|
  let censorCallCount = 0
 | 
						|
 | 
						|
  const redact = slowRedact({
 | 
						|
    paths: ['headers.authorization'],
 | 
						|
    serialize: false,
 | 
						|
    censor (value, path) {
 | 
						|
      censorCallCount++
 | 
						|
      return '[REDACTED]'
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  // headers exists but authorization doesn't
 | 
						|
  censorCallCount = 0
 | 
						|
  const result1 = redact({ headers: { 'content-type': 'application/json' } })
 | 
						|
  assert.strictEqual(censorCallCount, 0)
 | 
						|
  assert.deepStrictEqual(result1.headers, { 'content-type': 'application/json' })
 | 
						|
 | 
						|
  // headers doesn't exist at all
 | 
						|
  censorCallCount = 0
 | 
						|
  const result2 = redact({ body: 'data' })
 | 
						|
  assert.strictEqual(censorCallCount, 0)
 | 
						|
  assert.strictEqual(result2.body, 'data')
 | 
						|
  assert.strictEqual(typeof result2.restore, 'function')
 | 
						|
 | 
						|
  // headers.authorization exists - should call censor
 | 
						|
  censorCallCount = 0
 | 
						|
  const result3 = redact({ headers: { authorization: 'Bearer token' } })
 | 
						|
  assert.strictEqual(censorCallCount, 1)
 | 
						|
  assert.strictEqual(result3.headers.authorization, '[REDACTED]')
 | 
						|
})
 |