// @vitest-environment node import { unzipSync } from "fflate"; import { describe, expect, it } from "vitest"; import { buildSanitizationReport, createBatchArchive, createBatchReport, serializeReport, type ImageScanResult, type MetadataFinding, } from "../../src/privacy"; const sensitive: MetadataFinding = { id: "gps-1", category: "location", risk: "sensitive", source: "EXIF", label: "GPS Latitude", value: "48.1 N", }; describe("sanitization and batch reports", () => { it("marks a complete sensitive-free output verified and records removals", () => { const source = result({ findings: [sensitive] }); const output = result({ id: "clean", name: "clean.png", findings: [] }); const report = buildSanitizationReport( source, output, "clean.png", "image/png", 20, comparison(true), ); expect(report.status).toBe("verified"); expect(report.removed).toEqual([sensitive]); expect(report.preserved).toEqual([]); expect(report.orientationNormalized).toBe(true); expect(report.disclaimer).toMatch(/not an anonymity guarantee/iu); }); it("fails a partial or sensitive output re-scan", () => { const source = result({ findings: [sensitive] }); const output = result({ id: "clean", findings: [sensitive], coverage: { projectScanner: "partial", secondaryScanner: "complete", notes: [], }, }); const report = buildSanitizationReport( source, output, "clean.png", "image/png", 20, comparison(false), ); expect(report.status).toBe("failed"); expect(report.preserved).toEqual([sensitive]); expect(report.incomplete).not.toEqual([]); }); it("reports an unknown browser-generated output chunk as a warning", () => { const source = result(); const output = result({ id: "clean", findings: [ { id: "private-1", category: "unknown", risk: "context", source: "PNG", label: "Private chunk deBG", value: "16 bytes", }, ], blocks: [{ kind: "deBG", offset: 40, length: 16 }], }); const report = buildSanitizationReport( source, output, "clean.png", "image/png", 20, comparison(true), ); expect(report.status).toBe("warning"); expect(report.generated).toHaveLength(1); expect(report.incomplete.join(" ")).toContain("deBG"); }); it("fails a changed decoded sample for lossless PNG", () => { const report = buildSanitizationReport( result(), result({ id: "clean" }), "clean.png", "image/png", 20, comparison(false), ); expect(report.status).toBe("failed"); expect(report.incomplete.join(" ")).toMatch(/pixel sample/iu); }); it("produces deterministic, safe JSON and a stored batch ZIP", async () => { const source = result({ name: "../private.png", findings: [sensitive] }); const output = result({ id: "clean", name: "private.clean.png" }); const report = buildSanitizationReport( source, output, "../same.png", "image/png", 3, comparison(true), ); const assets = [ { blob: new Blob([Uint8Array.of(1, 2, 3)]), report }, { blob: new Blob([Uint8Array.of(4, 5, 6)]), report }, ]; const generatedAt = "2026-09-01T00:00:00.000Z"; const batch = createBatchReport([source], assets, generatedAt); const json = serializeReport(batch); expect(JSON.parse(json)).toMatchObject({ schemaVersion: 1, generatedAt }); expect(json).toContain("report may itself contain sensitive"); const archive = await createBatchArchive([source], assets, generatedAt); const entries = unzipSync(new Uint8Array(await archive.arrayBuffer())); expect(Object.keys(entries).sort()).toEqual([ "images/_same-2.png", "images/_same.png", "privacy-tools-report.json", ]); expect( JSON.parse( new TextDecoder().decode(entries["privacy-tools-report.json"]), ), ).toMatchObject({ schemaVersion: 1, generatedAt }); }); it("bounds archive entry count before reading output blobs", async () => { const source = result(); const output = result({ id: "clean" }); const report = buildSanitizationReport( source, output, "clean.png", "image/png", 1, comparison(true), ); const asset = { blob: new Blob([Uint8Array.of(0)]), report }; await expect( createBatchArchive( [source], Array.from({ length: 101 }, () => asset), ), ).rejects.toThrow(/Archive image count/iu); }); }); function comparison(identical: boolean) { return { method: "oriented-256px-sample" as const, sourceDigest: "source-sample", outputDigest: identical ? "source-sample" : "output-sample", identical, note: "test comparison", }; } function result(overrides: Partial = {}): ImageScanResult { return { id: "source", name: "source.png", safeName: "source.png", size: 100, lastModified: 0, sha256: "a".repeat(64), identity: { claimedType: "image/png", extension: "png", detectedKind: "png", detectedType: "image/png", typeMatch: "match", }, width: 2, height: 1, orientation: 1, animated: false, multiImage: false, deepSupported: true, cleanable: true, findings: [], blocks: [], warnings: [], coverage: { projectScanner: "complete", secondaryScanner: "complete", notes: [], }, ...overrides, }; }