64 lines
2.3 KiB
TypeScript
64 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { canonicalize, hash } from "./hash.js";
|
|
|
|
describe("content hashing", () => {
|
|
it("canonicalizes object keys lexicographically while preserving array order", () => {
|
|
expect(canonicalize({ z: 1, a: [{ b: true, a: null }] })).toBe(
|
|
'{"a":[{"a":null,"b":true}],"z":1}',
|
|
);
|
|
});
|
|
|
|
it("hashes equivalent object key orders to the same sha256 hex", () => {
|
|
const left = hash({ z: 1, a: 2 });
|
|
const right = hash({ a: 2, z: 1 });
|
|
|
|
expect(left).toBe(right);
|
|
expect(left).toMatch(/^[a-f0-9]{64}$/);
|
|
});
|
|
|
|
it("preserves own __proto__ keys while canonicalizing objects", () => {
|
|
const withProtoKey = JSON.parse('{"__proto__":{"x":1},"a":2}') as unknown;
|
|
|
|
expect(canonicalize(withProtoKey)).toBe('{"__proto__":{"x":1},"a":2}');
|
|
expect(hash(withProtoKey)).not.toBe(hash({ a: 2 }));
|
|
});
|
|
|
|
it("rejects hidden object keys that would be ignored by JSON rendering", () => {
|
|
const withSymbol = { a: 1, [Symbol("x")]: 2 };
|
|
const withHidden = { a: 1 };
|
|
Object.defineProperty(withHidden, "hidden", { value: 2, enumerable: false });
|
|
|
|
expect(() => canonicalize(withSymbol)).toThrow(/non-enumerable or symbol/);
|
|
expect(() => canonicalize(withHidden)).toThrow(/non-enumerable or symbol/);
|
|
});
|
|
|
|
it("rejects non-index array object keys that would be ignored by JSON rendering", () => {
|
|
const withStringKey = [1] as number[] & { extra?: number };
|
|
withStringKey.extra = 2;
|
|
const withSymbol = [1] as unknown[];
|
|
Object.defineProperty(withSymbol, Symbol("x"), { value: 2, enumerable: true });
|
|
|
|
expect(() => canonicalize(withStringKey)).toThrow(/non-index array/);
|
|
expect(() => canonicalize(withSymbol)).toThrow(/non-index array/);
|
|
});
|
|
|
|
it("renders the shortest round-trippable number literals without plus signs", () => {
|
|
expect(canonicalize([100, 1000, 11000, 123000, 1e20, 1e21, 0.000001, 0.0000001])).toBe(
|
|
"[100,1e3,11e3,123e3,1e20,1e21,1e-6,1e-7]",
|
|
);
|
|
});
|
|
|
|
it("rejects values that are not JSON-safe", () => {
|
|
const sparse = Array<number>(3);
|
|
sparse[0] = 1;
|
|
sparse[2] = 3;
|
|
|
|
expect(() => canonicalize({ date: new Date("2026-05-09T00:00:00Z") })).toThrow(
|
|
/non-plain object/,
|
|
);
|
|
expect(() => canonicalize({ missing: undefined })).toThrow(/undefined/);
|
|
expect(() => canonicalize(sparse)).toThrow(/sparse array/);
|
|
});
|
|
});
|