Initial Commit
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Spec-conformance tests for SZ-10 — Zod schema validation at the public
|
||||
* boundary. Pins:
|
||||
* - WireNodeSchema accepts every valid wire shape produced by serialize.
|
||||
* - WireNodeSchema rejects malformed envelopes (non-string __t, extra keys).
|
||||
* - deserialize rejects malformed JSON wire input with MalformedWireError.
|
||||
* - decodeBinary rejects malformed msgpack wire input with MalformedWireError.
|
||||
* - serialize / deserialize / decodeBinary reject invalid options with
|
||||
* InvalidOptionsError (typed by stable error code).
|
||||
*/
|
||||
import { describe, expect, expectTypeOf, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
InvalidOptionsError,
|
||||
MalformedWireError,
|
||||
} from "../../errors/serialize-error.js";
|
||||
import { registerType, unregisterType } from "../../registry/registry.js";
|
||||
import { deserialize } from "../json/json-decode.js";
|
||||
import { serialize } from "../json/json-encode.js";
|
||||
import { decodeBinary } from "../msgpack/msgpack-decode.js";
|
||||
import { encodeBinary } from "../msgpack/msgpack-encode.js";
|
||||
import {
|
||||
TaggedEnvelopeSchema,
|
||||
validateWireNode,
|
||||
WireNodeSchema,
|
||||
} from "./wire.schemas.js";
|
||||
|
||||
describe("SZ-10 — WireNodeSchema acceptance", () => {
|
||||
it("accepts JSON primitives", { timeout: 5000 }, () => {
|
||||
for (const v of [null, true, false, 1, "x"]) {
|
||||
expect(WireNodeSchema.safeParse(v).success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts arrays of nodes", { timeout: 5000 }, () => {
|
||||
expect(WireNodeSchema.safeParse([1, "x", null]).success).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts plain records", { timeout: 5000 }, () => {
|
||||
expect(WireNodeSchema.safeParse({ a: 1, b: "x" }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts a tagged envelope", { timeout: 5000 }, () => {
|
||||
expect(
|
||||
WireNodeSchema.safeParse({ __t: "__Date", v: "2024-01-01" }).success
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SZ-10 — WireNodeSchema rejection", () => {
|
||||
it("rejects undefined", { timeout: 5000 }, () => {
|
||||
expect(WireNodeSchema.safeParse(undefined).success).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects symbols and functions", { timeout: 5000 }, () => {
|
||||
expect(WireNodeSchema.safeParse(Symbol("x")).success).toBe(false);
|
||||
expect(WireNodeSchema.safeParse(() => 0).success).toBe(false);
|
||||
});
|
||||
|
||||
it("strict envelope rejects extra keys", { timeout: 5000 }, () => {
|
||||
const r = TaggedEnvelopeSchema.safeParse({
|
||||
__t: "__Date",
|
||||
v: "x",
|
||||
extra: 1,
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("validateWireNode returns ZodError on rejection", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const r = validateWireNode(undefined);
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) {
|
||||
expect(r.error.issues.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("SZ-10 — deserialize boundary validation", () => {
|
||||
it("invalid JSON throws MalformedWireError", { timeout: 5000 }, () => {
|
||||
expect(() => deserialize("{not json")).toThrowError(MalformedWireError);
|
||||
});
|
||||
|
||||
it("malformed envelope (non-string __t) throws MalformedWireError", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const bad = JSON.stringify({ __t: 42, v: null });
|
||||
expect(() => deserialize(bad)).toThrowError(MalformedWireError);
|
||||
});
|
||||
|
||||
it("MalformedWireError carries CODEC.MALFORMED_WIRE code", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
try {
|
||||
deserialize("{not json");
|
||||
} catch (e) {
|
||||
expect((e as MalformedWireError).code).toBe(
|
||||
"SERIALIZE.CODEC.MALFORMED_WIRE"
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw new Error("expected throw");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SZ-10 — decodeBinary boundary validation", () => {
|
||||
it("invalid msgpack bytes throw MalformedWireError", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// 0xc1 is reserved/never-used in msgpack and triggers a decode error.
|
||||
const bad = new Uint8Array([0xc1, 0xc1, 0xc1]);
|
||||
expect(() => decodeBinary(bad)).toThrowError(MalformedWireError);
|
||||
});
|
||||
|
||||
it("round-trips valid msgpack input", { timeout: 5000 }, () => {
|
||||
const bytes = encodeBinary({ a: 1, b: "two" });
|
||||
const back = decodeBinary(bytes);
|
||||
expect(back).toEqual({ a: 1, b: "two" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("SZ-10 — invalid options rejection", () => {
|
||||
it("serialize throws InvalidOptionsError for non-positive maxDepth", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => serialize({}, { maxDepth: -1 } as never)).toThrowError(
|
||||
InvalidOptionsError
|
||||
);
|
||||
});
|
||||
|
||||
it("serialize throws InvalidOptionsError for non-positive maxSize", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => serialize({}, { maxSize: 0 } as never)).toThrowError(
|
||||
InvalidOptionsError
|
||||
);
|
||||
});
|
||||
|
||||
it("serialize throws InvalidOptionsError for unknown option keys", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => serialize({}, { unknown: 1 } as never)).toThrowError(
|
||||
InvalidOptionsError
|
||||
);
|
||||
});
|
||||
|
||||
it("deserialize throws InvalidOptionsError for non-AbortSignal signal", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() =>
|
||||
deserialize("null", { signal: "nope" as unknown as AbortSignal })
|
||||
).toThrowError(InvalidOptionsError);
|
||||
});
|
||||
|
||||
it("encodeBinary throws InvalidOptionsError for non-positive maxSize", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(() => encodeBinary({}, { maxSize: -10 } as never)).toThrowError(
|
||||
InvalidOptionsError
|
||||
);
|
||||
});
|
||||
|
||||
it("decodeBinary throws InvalidOptionsError for unknown option keys", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const bytes = encodeBinary({ a: 1 });
|
||||
expect(() => decodeBinary(bytes, { unknown: 1 } as never)).toThrowError(
|
||||
InvalidOptionsError
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SZ-10 — error class branch coverage", () => {
|
||||
it("MalformedWireError works without a path argument", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const err = new MalformedWireError("no-path");
|
||||
expect(err.code).toBe("SERIALIZE.CODEC.MALFORMED_WIRE");
|
||||
expect(err.context?.["tag"]).toBe("no-path");
|
||||
expect(err.context?.["path"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("InvalidOptionsError works with cause", { timeout: 5000 }, () => {
|
||||
const cause = new Error("inner");
|
||||
const err = new InvalidOptionsError("invalid", cause);
|
||||
expect(err.code).toBe("SERIALIZE.OPTIONS.INVALID");
|
||||
expect(err.cause).toBe(cause);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SZ-10 — msgpack wire-schema rejection", () => {
|
||||
it("decodeBinary rejects msgpack-encoded malformed envelopes", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Build a real msgpack buffer for { __t: 1, v: 1 } using @msgpack/msgpack
|
||||
// directly — bypassing our encoder so the wire-schema rejection path
|
||||
// can be exercised independently of JSON.parse.
|
||||
// 0x82 = fixmap of 2; 0xa3 "__t" = fixstr; 0x01 = 1; 0xa1 "v" = fixstr; 0x01 = 1.
|
||||
const bytes = new Uint8Array([
|
||||
0x82, 0xa3, 0x5f, 0x5f, 0x74, 0x01, 0xa1, 0x76, 0x01,
|
||||
]);
|
||||
expect(() => decodeBinary(bytes)).toThrowError(MalformedWireError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SZ-10 — encode/decode error label fallback", () => {
|
||||
it("encode error path tags non-Error throws as SERIALIZE.CODEC.UNKNOWN", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Register a custom encode function that throws a non-Error value to
|
||||
// exercise the `: "SERIALIZE.CODEC.UNKNOWN"` branch in both codecs.
|
||||
const tag = "__SerializeEncodeBranchProbe";
|
||||
const ctor = class EncodeBranchProbe {};
|
||||
registerType({
|
||||
tag,
|
||||
ctor,
|
||||
encode: () => {
|
||||
throw "raw-string-throw";
|
||||
},
|
||||
decode: (v) => v,
|
||||
});
|
||||
try {
|
||||
const probe = new ctor();
|
||||
expect(() => serialize(probe)).toThrow();
|
||||
expect(() => encodeBinary(probe)).toThrow();
|
||||
} finally {
|
||||
unregisterType(tag);
|
||||
}
|
||||
});
|
||||
|
||||
it("decode error path tags non-Error throws as SERIALIZE.CODEC.UNKNOWN", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
// Register a custom decode function that throws a non-Error value to
|
||||
// exercise the `: "SERIALIZE.CODEC.UNKNOWN"` branch on the decode side.
|
||||
const tag = "__SerializeDecodeBranchProbe";
|
||||
const ctor = class DecodeBranchProbe {};
|
||||
registerType({
|
||||
tag,
|
||||
ctor,
|
||||
encode: () => null,
|
||||
decode: () => {
|
||||
throw "raw-string-throw";
|
||||
},
|
||||
});
|
||||
try {
|
||||
const probe = new ctor();
|
||||
const json = serialize(probe);
|
||||
expect(() => deserialize(json)).toThrow();
|
||||
const bytes = encodeBinary(probe);
|
||||
expect(() => decodeBinary(bytes)).toThrow();
|
||||
} finally {
|
||||
unregisterType(tag);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("SZ-10 — generic threading", () => {
|
||||
it("deserialize<T>() returns T at the type level", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const json = serialize({ name: "ada", age: 36 });
|
||||
const result = deserialize<{ name: string; age: number }>(json);
|
||||
expectTypeOf(result).toEqualTypeOf<{ name: string; age: number }>();
|
||||
expect(result.name).toBe("ada");
|
||||
});
|
||||
|
||||
it("decodeBinary<T>() returns T at the type level", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const bytes = encodeBinary([1, 2, 3]);
|
||||
const result = decodeBinary<number[]>(bytes);
|
||||
expectTypeOf(result).toEqualTypeOf<number[]>();
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("deserialize() defaults to unknown when T is omitted", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const result = deserialize(serialize({ a: 1 }));
|
||||
expectTypeOf(result).toEqualTypeOf<unknown>();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user