shared-validation.host.spec.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. /** Shared JSON and exact-field validation behavior. */
  2. import { describe, expect, it } from 'vitest'
  3. import { inspectorId } from '../src/shared/identity.ts'
  4. import { isJsonValue, isPlainObject, jsonByteLength, requireJsonObject } from '../src/shared/json.ts'
  5. import {
  6. exactKeys,
  7. exactObject,
  8. optionalBoolean,
  9. optionalNonNegativeNumber,
  10. optionalString,
  11. wireId,
  12. } from '../src/shared/validation.ts'
  13. describe('Inspector JSON values', () => {
  14. it('accepts every lossless JSON category and measures UTF-8 bytes', () => {
  15. const nullPrototype = Object.assign(Object.create(null) as Record<string, unknown>, { value: '好' })
  16. expect([null, 'text', true, 1, [1, 'two'], { nested: [false] }, nullPrototype].every(isJsonValue)).toBe(true)
  17. expect(jsonByteLength({ value: '好' })).toBe(Buffer.byteLength('{"value":"好"}'))
  18. expect(isPlainObject({})).toBe(true)
  19. expect(isPlainObject(nullPrototype)).toBe(true)
  20. expect(requireJsonObject({ value: 1 }, 'payload')).toEqual({ value: 1 })
  21. })
  22. it('rejects lossy primitives, cycles, exotic arrays, and accessor objects', () => {
  23. const cyclic: Record<string, unknown> = {}
  24. cyclic.self = cyclic
  25. const arrayWithField = [1]
  26. Reflect.set(arrayWithField, 'extra', true)
  27. const inheritedArray = Object.setPrototypeOf([1], null) as unknown
  28. const symbolObject = { [Symbol('field')]: true }
  29. const hidden = {}
  30. Object.defineProperty(hidden, 'value', { value: 1, enumerable: false })
  31. const accessor = {}
  32. Object.defineProperty(accessor, 'value', { get: () => 1, enumerable: true })
  33. const rejected = [
  34. undefined, () => undefined, Number.NaN, -0, cyclic, arrayWithField,
  35. inheritedArray, new Date(), symbolObject, hidden, accessor,
  36. ]
  37. for (const value of rejected) {
  38. expect(isJsonValue(value)).toBe(false)
  39. }
  40. expect(() => requireJsonObject([], 'payload')).toThrow('payload must be a JSON object')
  41. expect(() => requireJsonObject(cyclic, 'payload')).toThrow('payload must be a JSON object')
  42. expect(isPlainObject(null)).toBe(false)
  43. expect(isPlainObject([])).toBe(false)
  44. })
  45. })
  46. describe('Inspector exact-field readers', () => {
  47. it('accepts declared fields and optional values', () => {
  48. const record = { text: 'value', enabled: true, timeout: 0 }
  49. expect(exactObject(record, ['text', 'enabled', 'timeout'], 'record')).toBe(record)
  50. expect(() => { exactKeys(record, ['text', 'enabled', 'timeout'], 'record') }).not.toThrow()
  51. expect(optionalString(record, 'text')).toEqual({ text: 'value' })
  52. expect(optionalBoolean(record, 'enabled')).toEqual({ enabled: true })
  53. expect(optionalNonNegativeNumber(record, 'timeout')).toEqual({ timeout: 0 })
  54. expect(optionalString({}, 'text')).toEqual({})
  55. expect(optionalBoolean({}, 'enabled')).toEqual({})
  56. expect(optionalNonNegativeNumber({}, 'timeout')).toEqual({})
  57. expect(wireId<'ProbeId'>('probe', 'probeId')).toBe('probe')
  58. expect(inspectorId<'ProbeId'>('probe', 'probeId')).toBe('probe')
  59. })
  60. it('rejects unknown, symbolic, and wrongly typed fields', () => {
  61. expect(() => exactObject([], [], 'record')).toThrow('record must be an object')
  62. expect(() => { exactKeys({ extra: true }, [], 'record') }).toThrow('unknown field')
  63. expect(() => { exactKeys({ [Symbol('extra')]: true }, [], 'record') }).toThrow('unknown field')
  64. expect(() => wireId<'ProbeId'>(1, 'probeId')).toThrow('probeId must be a string')
  65. expect(() => inspectorId<'ProbeId'>('', 'probeId')).toThrow('1 to 256 characters')
  66. expect(() => inspectorId<'ProbeId'>('x'.repeat(257), 'probeId')).toThrow('1 to 256 characters')
  67. expect(() => optionalString({ text: 1 }, 'text')).toThrow('text must be a string')
  68. expect(() => optionalBoolean({ enabled: 1 }, 'enabled')).toThrow('enabled must be a boolean')
  69. for (const timeout of ['1', Number.NaN, -1]) {
  70. expect(() => optionalNonNegativeNumber({ timeout }, 'timeout')).toThrow('non-negative finite number')
  71. }
  72. })
  73. })