234 lines
7.7 KiB
TypeScript
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);
|
|
});
|
|
});
|