import { Encoder } from "@msgpack/msgpack"; import { afterEach, describe, expect, it } from "@suaveplan/testing/runner"; import { CircularReferenceError, MaxDepthExceededError, MaxSizeExceededError, UnsupportedTypeError, } from "../../errors/serialize-error.js"; import { registerType, unregisterType } from "../../registry/registry.js"; import { decodeBinary } from "./msgpack-decode.js"; import { encodeBinary } from "./msgpack-encode.js"; const CUSTOM_TAG = "__TestVec"; class Vec2 { public constructor( public x: number, public y: number ) {} } afterEach(() => { unregisterType(CUSTOM_TAG); }); describe("encodeBinary / decodeBinary: primitives", () => { it("string round-trips", { timeout: 5000 }, () => { expect(decodeBinary(encodeBinary("hello"))).toBe("hello"); }); it("integer round-trips", { timeout: 5000 }, () => { expect(decodeBinary(encodeBinary(42))).toBe(42); }); it("boolean round-trips", { timeout: 5000 }, () => { expect(decodeBinary(encodeBinary(false))).toBe(false); }); it("null round-trips", { timeout: 5000 }, () => { expect(decodeBinary(encodeBinary(null))).toBe(null); }); it("undefined round-trips", { timeout: 5000 }, () => { expect(decodeBinary(encodeBinary(undefined))).toBeUndefined(); }); it("NaN round-trips", { timeout: 5000 }, () => { expect( Number.isNaN(decodeBinary(encodeBinary(Number.NaN)) as number) ).toBe(true); }); it("+Infinity round-trips", { timeout: 5000 }, () => { expect(decodeBinary(encodeBinary(Number.POSITIVE_INFINITY))).toBe( Number.POSITIVE_INFINITY ); }); it("-Infinity round-trips", { timeout: 5000 }, () => { expect(decodeBinary(encodeBinary(Number.NEGATIVE_INFINITY))).toBe( Number.NEGATIVE_INFINITY ); }); }); describe("encodeBinary / decodeBinary: built-in types", () => { it("Date round-trips", { timeout: 5000 }, () => { const d = new Date("2024-03-15T10:00:00.000Z"); const result = decodeBinary(encodeBinary(d)) as Date; expect(result).toBeInstanceOf(Date); expect(result.toISOString()).toBe(d.toISOString()); }); it("Map round-trips", { timeout: 5000 }, () => { const m = new Map([ ["a", 1], ["b", 2], ]); const result = decodeBinary(encodeBinary(m)) as Map; expect(result).toBeInstanceOf(Map); expect(result.get("b")).toBe(2); }); it("Set round-trips", { timeout: 5000 }, () => { const s = new Set(["x", "y", "z"]); const result = decodeBinary(encodeBinary(s)) as Set; expect(result).toBeInstanceOf(Set); expect(result.has("y")).toBe(true); }); it("BigInt round-trips", { timeout: 5000 }, () => { const n = 9007199254740993n; expect(decodeBinary(encodeBinary(n))).toBe(n); }); it("Uint8Array round-trips", { timeout: 5000 }, () => { const arr = new Uint8Array([10, 20, 30]); const result = decodeBinary(encodeBinary(arr)) as Uint8Array; expect(result).toBeInstanceOf(Uint8Array); expect(Array.from(result)).toEqual([10, 20, 30]); }); it("URL round-trips", { timeout: 5000 }, () => { const url = new URL("https://suaveplan.io/api?v=1"); const result = decodeBinary(encodeBinary(url)) as URL; expect(result).toBeInstanceOf(URL); expect(result.href).toBe(url.href); }); it("RegExp round-trips", { timeout: 5000 }, () => { const re = /test/im; const result = decodeBinary(encodeBinary(re)) as RegExp; expect(result).toBeInstanceOf(RegExp); expect(result.source).toBe("test"); expect(result.flags).toBe("im"); }); }); describe("encodeBinary / decodeBinary: nested structures", () => { it("plain object round-trips", { timeout: 5000 }, () => { const obj = { x: 1, y: "two", z: true }; expect(decodeBinary(encodeBinary(obj))).toEqual(obj); }); it("array round-trips", { timeout: 5000 }, () => { const arr = [1, "two", null, false]; expect(decodeBinary(encodeBinary(arr))).toEqual(arr); }); it("nested map of dates", { timeout: 5000 }, () => { const m = new Map([["ts", new Date("2024-01-01")]]); const result = decodeBinary(encodeBinary(m)) as Map; expect(result.get("ts")).toBeInstanceOf(Date); }); }); describe("encodeBinary: structural equivalence with JSON codec", () => { it("produces structurally equivalent output", { timeout: 5000 }, () => { const obj = { name: "test", date: new Date("2024-01-01T00:00:00.000Z"), count: 42, }; const fromBinary = decodeBinary(encodeBinary(obj)) as typeof obj; expect(fromBinary.name).toBe("test"); expect(fromBinary.date).toBeInstanceOf(Date); expect(fromBinary.count).toBe(42); }); }); describe("encodeBinary: error conditions", () => { it("circular reference throws CircularReferenceError", { timeout: 5000, }, () => { const obj: Record = {}; obj["self"] = obj; expect(() => encodeBinary(obj)).toThrowError(CircularReferenceError); }); it("max depth exceeded throws MaxDepthExceededError", { timeout: 5000, }, () => { const deep: Record = {}; let cur = deep; for (let i = 0; i < 10; i++) { cur["child"] = {}; cur = cur["child"] as Record; } expect(() => encodeBinary(deep, { maxDepth: 3 })).toThrowError( MaxDepthExceededError ); }); it("max size exceeded throws MaxSizeExceededError", { timeout: 5000, }, () => { const obj = { data: "x".repeat(1000) }; expect(() => encodeBinary(obj, { maxSize: 10 })).toThrowError( MaxSizeExceededError ); }); it("aborted signal throws immediately", { timeout: 5000 }, () => { const controller = new AbortController(); controller.abort(new Error("cancelled")); expect(() => encodeBinary({ a: 1 }, { signal: controller.signal }) ).toThrow(); }); }); describe("decodeBinary: error conditions", () => { it("unknown tag throws UnsupportedTypeError", { timeout: 5000 }, () => { // Manually craft msgpack with unknown tag const enc = new Encoder(); const bytes = enc.encode({ __t: "__NonExistentTag999", v: null }); expect(() => decodeBinary(bytes)).toThrowError(UnsupportedTypeError); }); }); describe("encodeBinary: function and symbol values are skipped", () => { it("function value in object is omitted", { timeout: 5000 }, () => { const obj: Record = { a: 1, fn: () => 42 }; const result = decodeBinary(encodeBinary(obj)) as Record< string, unknown >; expect(result["a"]).toBe(1); expect("fn" in result).toBe(false); }); }); describe("encodeBinary: custom registered type", () => { it("custom type round-trips via binary", { timeout: 5000 }, () => { registerType({ tag: CUSTOM_TAG, ctor: Vec2, encode: (v) => { const vec = v as Vec2; return [vec.x, vec.y]; }, decode: (enc) => { const [x, y] = enc as [number, number]; return new Vec2(x, y); }, }); const v = new Vec2(5, 10); const result = decodeBinary(encodeBinary(v)) as Vec2; expect(result).toBeInstanceOf(Vec2); expect(result.x).toBe(5); expect(result.y).toBe(10); }); });