SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
3 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
3.4 KB · 50 lines typescript
Raw Blame History
1import { describe, expect, it } from "vitest";2import { canonicalUrl, contentFingerprint, htmlToText, sha256, urlFingerprint } from "./hash.js";34describe("canonicalUrl", () => {5  it("lowercases the host, drops fragments, default ports, tracking params, sorts the query and trims the slash", () => {6    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");7    expect(canonicalUrl("http://x.com:80/a/")).toBe("http://x.com/a");8    expect(canonicalUrl("https://x.com/")).toBe("https://x.com/");9    expect(canonicalUrl("https://x.com/a?ref=nav&source=rss&gclid=1")).toBe("https://x.com/a");10    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`11    expect(canonicalUrl("https://x.com:8443/a")).toBe("https://x.com:8443/a");12  });13  it("urlFingerprint is stable across equivalent forms", () => {14    expect(urlFingerprint("https://X.com/a?b=1&a=2&utm_medium=m")).toBe(urlFingerprint("https://x.com/a?a=2&b=1#x"));15    expect(urlFingerprint("https://x.com/a/")).toBe(urlFingerprint("https://x.com/a"));16    // documented: the trailing slash is only trimmed when there is no query17    expect(urlFingerprint("https://x.com/a/?q=1")).not.toBe(urlFingerprint("https://x.com/a?q=1"));18    expect(urlFingerprint("https://x.com/a")).not.toBe(urlFingerprint("https://x.com/b"));19    expect(urlFingerprint("https://x.com/a")).toHaveLength(32);20  });21});2223describe("contentFingerprint", () => {24  it("ignores comments, scripts, styles, nonces, csrf inputs and whitespace on HTML", () => {25    const a = contentFingerprint('<html><head><script nonce="n1">x()</script><style>.a{}</style></head><body data-build="1"><!-- c1 --><p>Hi   there</p><input type="hidden" name="csrf" value="a"></body></html>', "text/html");26    const b = contentFingerprint('<html><head><script nonce="n2">y()</script></head><body data-build="2"><!-- c2 --><p>Hi\nthere</p><input type="hidden" name="csrf" value="b"></body></html>', "text/html");27    expect(a).toBe(b);28    expect(contentFingerprint("<p>Hi there</p>", "text/html")).not.toBe(contentFingerprint("<p>Hi here</p>", "text/html"));29  });30  it("only collapses whitespace on non-HTML bodies", () => {31    expect(contentFingerprint('{"a": 1}', "application/json")).toBe(contentFingerprint('{"a":  1}', "application/json"));32    expect(contentFingerprint("<script>x</script>a", "text/plain")).not.toBe(contentFingerprint("a", "text/plain"));33    expect(contentFingerprint("<script>x</script>a", null)).toBe(contentFingerprint("a", null)); // unknown type treated as HTML34  });35  it("sha256 is hex and deterministic", () => {36    expect(sha256("abc")).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");37    expect(sha256(Buffer.from("abc"))).toBe(sha256("abc"));38  });39});4041describe("htmlToText", () => {42  it("drops scripts/styles/comments, breaks blocks into lines and decodes entities", () => {43    // block closers become line breaks; an opening tag leaves one space (the projection is for diffing, not display)44    expect(htmlToText("<div>A &amp; B</div><p>C&nbsp;&#169;</p><br>D")).toBe("A & B\n C ©\nD");45    expect(htmlToText("x<script>no()</script>y<style>.s{}</style><!-- c -->z")).toBe("x y z");46    expect(htmlToText("<ul><li>1</li><li>2</li></ul>")).toBe("1\n 2");47    expect(htmlToText("&lt;tag&gt; &quot;q&quot; &#39;s&apos;")).toBe("<tag> \"q\" 's'");48  });49});50