import { describe, expect, it } from "vitest"; import { canonicalUrl, contentFingerprint, htmlToText, sha256, urlFingerprint } from "./hash.js"; describe("canonicalUrl", () => { it("lowercases the host, drops fragments, default ports, tracking params, sorts the query and trims the slash", () => { expect(canonicalUrl("HTTPS://WWW.Example.COM:443/Path/?utm_source=x&b=2&a=1&fbclid=z#frag")).toBe("https://www.example.com/Path/?a=1&b=2"); expect(canonicalUrl("http://x.com:80/a/")).toBe("http://x.com/a"); expect(canonicalUrl("https://x.com/")).toBe("https://x.com/"); expect(canonicalUrl("https://x.com/a?ref=nav&source=rss&gclid=1")).toBe("https://x.com/a"); expect(canonicalUrl("https://x.com/a?page=2&ref_id=7")).toBe("https://x.com/a?page=2&ref_id=7"); // ref_id is not `ref` expect(canonicalUrl("https://x.com:8443/a")).toBe("https://x.com:8443/a"); }); it("urlFingerprint is stable across equivalent forms", () => { expect(urlFingerprint("https://X.com/a?b=1&a=2&utm_medium=m")).toBe(urlFingerprint("https://x.com/a?a=2&b=1#x")); expect(urlFingerprint("https://x.com/a/")).toBe(urlFingerprint("https://x.com/a")); // documented: the trailing slash is only trimmed when there is no query expect(urlFingerprint("https://x.com/a/?q=1")).not.toBe(urlFingerprint("https://x.com/a?q=1")); expect(urlFingerprint("https://x.com/a")).not.toBe(urlFingerprint("https://x.com/b")); expect(urlFingerprint("https://x.com/a")).toHaveLength(32); }); }); describe("contentFingerprint", () => { it("ignores comments, scripts, styles, nonces, csrf inputs and whitespace on HTML", () => { const a = contentFingerprint('
Hi there
', "text/html"); const b = contentFingerprint('Hi\nthere
', "text/html"); expect(a).toBe(b); expect(contentFingerprint("Hi there
", "text/html")).not.toBe(contentFingerprint("Hi here
", "text/html")); }); it("only collapses whitespace on non-HTML bodies", () => { expect(contentFingerprint('{"a": 1}', "application/json")).toBe(contentFingerprint('{"a": 1}', "application/json")); expect(contentFingerprint("a", "text/plain")).not.toBe(contentFingerprint("a", "text/plain")); expect(contentFingerprint("a", null)).toBe(contentFingerprint("a", null)); // unknown type treated as HTML }); it("sha256 is hex and deterministic", () => { expect(sha256("abc")).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); expect(sha256(Buffer.from("abc"))).toBe(sha256("abc")); }); }); describe("htmlToText", () => { it("drops scripts/styles/comments, breaks blocks into lines and decodes entities", () => { // block closers become line breaks; an opening tag leaves one space (the projection is for diffing, not display) expect(htmlToText("C ©