import { describe, expect, it } from "vitest";
import { buildMarkerSignature, byteLength, compactHtml, markerOf, signatureTooLong, SIGNATURE_LIMIT } from "../signatureHtml";
describe("signature compaction", () => {
it("strips office cruft and non-essential styles but keeps colors and links", () => {
const src = `
`;
const out = compactHtml(src);
expect(out).not.toContain("mso-");
expect(out).not.toContain("class=");
expect(out).not.toContain("John Coffey");
expect(out).toContain('href="https://linuxexpert.org"');
expect(out).toContain('width="100"');
expect(out.length).toBeLessThan(src.length / 2);
});
it("builds marker signatures within the limit", () => {
const big = `${"x".repeat(1000)}
`;
const m = buildMarkerSignature("blob123", big);
expect(m.htmlSignature.length).toBeLessThanOrEqual(SIGNATURE_LIMIT);
expect(m.textSignature.length).toBeLessThanOrEqual(SIGNATURE_LIMIT);
expect(markerOf(m.htmlSignature)).toEqual({ blobId: "blob123", type: "text/html" });
expect(markerOf("plain
")).toBeNull();
});
});
/**
* Stalwart's cap is `value.len() < 2048` on a Rust string — 2047 bytes of
* UTF-8. Measuring with JavaScript's `.length` counts UTF-16 units instead,
* which agrees only for ASCII: an accent is one unit and two bytes, CJK three,
* an emoji two units and four. Every check has to weigh the encoded form or a
* signature we judged to fit comes back rejected.
*/
describe("signature size is measured in bytes", () => {
const sigOf = (html: string) => buildMarkerSignature("blob123", html);
it("counts multi-byte characters at their encoded size", () => {
expect(byteLength("hello")).toBe(5);
expect(byteLength("Grüße")).toBe(7); // two 2-byte characters
expect(byteLength("日本語")).toBe(9); // three 3-byte characters
expect(byteLength("🎉")).toBe(4); // one 4-byte character, two UTF-16 units
});
it("spots a signature that fits in characters but not in bytes", () => {
// Comfortably under the limit counted as characters, well over it as bytes.
const cjk = "日".repeat(1200);
expect(cjk.length).toBeLessThan(SIGNATURE_LIMIT);
expect(signatureTooLong(cjk, cjk)).toBe(true);
});
it("keeps a marker signature within the byte limit for non-ASCII text", () => {
for (const filler of ["ü", "日", "🎉", "x"]) {
const m = sigOf(`${filler.repeat(3000)}
`);
expect(byteLength(m.htmlSignature), `html for ${filler}`).toBeLessThanOrEqual(SIGNATURE_LIMIT);
expect(byteLength(m.textSignature), `text for ${filler}`).toBeLessThanOrEqual(SIGNATURE_LIMIT);
expect(markerOf(m.htmlSignature)).toEqual({ blobId: "blob123", type: "text/html" });
}
});
it("never truncates through a surrogate pair", () => {
const m = sigOf(`${"🎉".repeat(3000)}
`);
// A split pair leaves a lone surrogate, which encodes as U+FFFD.
expect(m.htmlSignature).not.toContain("�");
expect(m.textSignature).not.toContain("�");
expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(m.textSignature)).toBe(false);
});
it("never truncates through an HTML entity", () => {
// Escaping turns each of these into a 5-character entity; cutting the
// rendered string could leave "&am" behind.
const m = sigOf(`${"a & b ".repeat(400)}
`);
expect(m.htmlSignature).not.toMatch(/&[a-z]*$/i);
expect(m.htmlSignature.replace(/&(amp|lt|gt|quot|#39);/g, "")).not.toContain("&");
});
it("leaves a signature that already fits completely alone", () => {
const m = sigOf("Grüße, John
");
expect(m.textSignature).toBe("Grüße, John");
expect(m.htmlSignature).not.toContain("…");
});
});