import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { inspectStream, parseContentLength, PayloadInspector, PermanentArchiveError } from "./inspector.js"; const PDF = new TextEncoder().encode("%PDF-1.7\nhello world"); function streamOf(...chunks: Uint8Array[]): ReadableStream { return new ReadableStream({ start(controller) { for (const chunk of chunks) controller.enqueue(chunk); controller.close(); }, }); } async function drain(stream: ReadableStream): Promise { let total = 0; for await (const chunk of stream) total += chunk.byteLength; return total; } describe("PayloadInspector", () => { it("hashes, counts and sniffs across chunk boundaries", () => { const inspector = new PayloadInspector(PDF.byteLength); inspector.update(PDF.subarray(0, 2)); inspector.update(PDF.subarray(2)); expect(inspector.finish()).toEqual({ sha256: createHash("sha256").update(PDF).digest("hex"), byteSize: PDF.byteLength, sniffed: { mime: "application/pdf", mimeClass: "document", extension: "pdf" }, }); }); it("rejects an oversized declared length before reading", () => { expect(() => new PayloadInspector(21 * 1024 * 1024)).toThrow(PermanentArchiveError); }); it("rejects streams exceeding the cap without Content-Length", () => { const inspector = new PayloadInspector(null, 10); expect(() => inspector.update(new Uint8Array(11))).toThrow(/exceeds/); }); it("rejects Content-Length lies in both directions", () => { const longer = new PayloadInspector(4); expect(() => longer.update(new Uint8Array(5))).toThrow(/longer/); const shorter = new PayloadInspector(10); shorter.update(new Uint8Array(4)); expect(() => shorter.finish()).toThrow(/shorter/); }); it("rejects empty payloads", () => { expect(() => new PayloadInspector(null).finish()).toThrow(/empty/); }); }); describe("inspectStream", () => { it("passes bytes through untouched and resolves the result", async () => { const { stream, result } = inspectStream(streamOf(PDF.subarray(0, 5), PDF.subarray(5)), PDF.byteLength); expect(await drain(stream)).toBe(PDF.byteLength); expect((await result).byteSize).toBe(PDF.byteLength); }); it("errors the stream (aborting the upload) when the source lies about length", async () => { const { stream, result } = inspectStream(streamOf(PDF), PDF.byteLength + 1); await expect(drain(stream)).rejects.toThrow(PermanentArchiveError); await expect(result).rejects.toThrow(/shorter/); }); it("errors mid-stream once the cap is crossed", async () => { const { stream } = inspectStream(streamOf(new Uint8Array(6), new Uint8Array(6)), null, 10); await expect(drain(stream)).rejects.toThrow(/exceeds/); }); }); describe("parseContentLength", () => { it("parses digits and treats absence as unknown", () => { expect(parseContentLength("42")).toBe(42); expect(parseContentLength(null)).toBeNull(); expect(() => parseContentLength("-1")).toThrow(PermanentArchiveError); }); });