json.spec.ts 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. inspectSessionFormatVersion,
  4. sessionFormatCount,
  5. sessionFormatSafeInteger,
  6. snapshotSessionFormatHeader,
  7. snapshotSessionFormatJson,
  8. } from '../src/index.ts'
  9. describe('lossless Session format JSON snapshots', () => {
  10. it.each([
  11. ['negative zero', -0],
  12. ['non-finite number', Number.POSITIVE_INFINITY],
  13. ['undefined member', { value: undefined }],
  14. ['sparse array', Array(1)],
  15. ['symbol member', { [Symbol('hidden')]: true }],
  16. ['non-enumerable member', Object.defineProperty({}, 'hidden', { value: true })],
  17. ['array property', Object.assign([], { extra: true })],
  18. ])('refuses %s that JSON cannot preserve', (_name, value) => {
  19. expect(() => snapshotSessionFormatJson(value, 'payload')).toThrow('payload is not lossless JSON')
  20. })
  21. it('detaches, freezes, and retains repeated non-cyclic values and __proto__ keys', () => {
  22. const shared = { value: 1 }
  23. const source = JSON.parse('{"__proto__":{"safe":true}}') as Record<string, unknown>
  24. source['values'] = [shared, shared]
  25. const snapshot = snapshotSessionFormatJson(source) as Record<string, unknown>
  26. expect(snapshot).toEqual(source)
  27. expect(snapshot).not.toBe(source)
  28. expect(Object.isFrozen(snapshot)).toBe(true)
  29. expect(Object.isFrozen(snapshot['values'])).toBe(true)
  30. expect(Object.getPrototypeOf(snapshot)).toBe(Object.prototype)
  31. })
  32. it('refuses invalid scalar coordinates, cycles, and custom prototypes', () => {
  33. const cyclic: { self?: unknown } = {}
  34. cyclic.self = cyclic
  35. class RecordValue { value = 1 }
  36. class ArrayValue extends Array<number> {}
  37. expect(() => sessionFormatCount(-1, 'count')).toThrow(/non-negative/)
  38. expect(() => sessionFormatSafeInteger(1.5, 'integer')).toThrow(/safe integer/)
  39. expect(() => inspectSessionFormatVersion([])).toThrow(/header/)
  40. expect(() => snapshotSessionFormatJson(cyclic)).toThrow(/not lossless JSON/)
  41. expect(() => snapshotSessionFormatJson(new RecordValue())).toThrow(/not lossless JSON/)
  42. expect(() => snapshotSessionFormatJson(new ArrayValue(1))).toThrow(/not lossless JSON/)
  43. })
  44. it('refuses a non-object header snapshot', () => {
  45. expect(() => snapshotSessionFormatHeader(null as never)).toThrow(/header|object/)
  46. expect(() => snapshotSessionFormatHeader({
  47. version: 1, id: 'missing-seeded', createdAt: 1, delegationDepth: 0,
  48. } as never)).toThrow(/isSeeded/)
  49. })
  50. })