import { describe, expect, it } from "vitest"; import { canonicalPreimage, contractHash, sha256Hex } from "./hash.js"; describe("sha256Hex", () => { // FIPS 180-4 / NIST known-answer vectors. it("matches the empty-string vector", () => { expect(sha256Hex("")).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); }); it("matches the 'abc' vector", () => { expect(sha256Hex("abc")).toBe( "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" ); }); it("matches the 56-byte multi-block vector", () => { expect(sha256Hex("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq")).toBe( "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1" ); }); it("hashes UTF-8 multibyte input deterministically", () => { // "héllo→" exercises 2- and 3-byte UTF-8 sequences through TextEncoder. expect(sha256Hex("héllo→")).toBe(sha256Hex("héllo→")); expect(sha256Hex("héllo→")).toHaveLength(64); }); it("crosses the padding boundary correctly (block-length inputs)", () => { for (const n of [55, 56, 57, 63, 64, 65, 119, 120]) { expect(sha256Hex("a".repeat(n))).toMatch(/^[0-9a-f]{64}$/); } }); }); describe("canonicalPreimage", () => { it("is order-independent for object keys", () => { expect(canonicalPreimage({ b: 1, a: 2 })).toBe(canonicalPreimage({ a: 2, b: 1 })); }); it("drops undefined properties but preserves null", () => { expect(canonicalPreimage({ a: 1, b: undefined })).toBe('{"a":1}'); expect(canonicalPreimage({ a: null })).toBe('{"a":null}'); }); it("normalizes negative zero to zero", () => { expect(canonicalPreimage(-0)).toBe("0"); expect(canonicalPreimage(0)).toBe("0"); }); it("gives non-finite numbers unambiguous, distinct sentinels", () => { expect(canonicalPreimage(NaN)).toBe('"@num:nan"'); expect(canonicalPreimage(Infinity)).toBe('"@num:+inf"'); expect(canonicalPreimage(-Infinity)).toBe('"@num:-inf"'); // and they do not collapse to null the way JSON.stringify would expect(canonicalPreimage(NaN)).not.toBe(canonicalPreimage(null)); }); it("encodes nested arrays and objects canonically", () => { expect(canonicalPreimage({ xs: [1, { z: true, y: "q" }] })).toBe( '{"xs":[1,{"y":"q","z":true}]}' ); }); }); describe("contractHash", () => { it("is stable across attribute order", () => { const a = { primitives: [{ id: "button", html: "button" }], v: 2 }; const b = { v: 2, primitives: [{ html: "button", id: "button" }] }; expect(contractHash(a)).toBe(contractHash(b)); }); it("changes when contract content changes", () => { const base = { primitives: [{ id: "button" }] }; const changed = { primitives: [{ id: "button" }, { id: "input" }] }; expect(contractHash(base)).not.toBe(contractHash(changed)); }); it("ignores undefined optional fields", () => { expect(contractHash({ id: "x", note: undefined })).toBe(contractHash({ id: "x" })); }); it("returns a 64-char lowercase hex FCID", () => { expect(contractHash({ any: "body" })).toMatch(/^[0-9a-f]{64}$/); }); });