Files
suaveplan/packages/foundation/data/serialize/src/codec/msgpack/msgpack.test.ts
2026-05-27 23:47:53 +01:00

234 lines
7.7 KiB
TypeScript

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<string, number>;
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<string>;
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<string, Date>;
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<string, unknown> = {};
obj["self"] = obj;
expect(() => encodeBinary(obj)).toThrowError(CircularReferenceError);
});
it("max depth exceeded throws MaxDepthExceededError", {
timeout: 5000,
}, () => {
const deep: Record<string, unknown> = {};
let cur = deep;
for (let i = 0; i < 10; i++) {
cur["child"] = {};
cur = cur["child"] as Record<string, unknown>;
}
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<string, unknown> = { 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);
});
});