import { describe, it, expect } from "vitest"; import { classifyMediaKind, formatBytes, isSupportedAudioMime, isSupportedVideoMime, isUnsupportedImageMime, resolveDeliveryKind, sniffUnsupportedImage, sniffUnsupportedVideo, validateForWhatsApp, WA_LIMITS, WA_MAX_BYTES, } from "./whatsapp-media"; const MB = 1024 * 1024; // Helper: build a 12-byte ISOBMFF header with the chosen brand. function isobmffHeader(brand: string): Uint8Array { const buf = new Uint8Array(12); buf[0] = 0x00; buf[1] = 0x00; buf[2] = 0x00; buf[3] = 0x18; buf[4] = 0x66; // 'f' buf[5] = 0x74; // 't' buf[6] = 0x79; // 'y' buf[7] = 0x70; // 'p' for (let i = 0; i < 4; i++) { buf[8 + i] = brand.charCodeAt(i) || 0x20; } return buf; } describe("classifyMediaKind — coarse mime → kind by top-level category", () => { it("classifies image/* as image", () => { expect(classifyMediaKind("image/jpeg")).toBe("image"); expect(classifyMediaKind("image/png")).toBe("image"); expect(classifyMediaKind("image/webp")).toBe("image"); }); it("classifies video/* as video", () => { expect(classifyMediaKind("video/mp4")).toBe("video"); expect(classifyMediaKind("video/3gpp")).toBe("video"); }); it("classifies audio/* as audio", () => { expect(classifyMediaKind("audio/aac")).toBe("audio"); expect(classifyMediaKind("audio/ogg")).toBe("audio"); expect(classifyMediaKind("audio/mpeg")).toBe("audio"); }); it("classifies anything else as document", () => { expect(classifyMediaKind("application/pdf")).toBe("document"); expect(classifyMediaKind("text/plain")).toBe("document"); expect(classifyMediaKind("application/octet-stream")).toBe("document"); expect(classifyMediaKind("")).toBe("document"); }); }); describe("isUnsupportedImageMime", () => { it("recognises HEIC, HEIF, AVIF (case-insensitive)", () => { expect(isUnsupportedImageMime("image/heic")).toBe(true); expect(isUnsupportedImageMime("image/heif")).toBe(true); expect(isUnsupportedImageMime("IMAGE/HEIC")).toBe(true); expect(isUnsupportedImageMime("image/heic-sequence")).toBe(true); expect(isUnsupportedImageMime("image/avif")).toBe(true); }); it("does not flag normal image formats", () => { expect(isUnsupportedImageMime("image/jpeg")).toBe(false); expect(isUnsupportedImageMime("image/png")).toBe(false); expect(isUnsupportedImageMime("image/webp")).toBe(false); expect(isUnsupportedImageMime("image/gif")).toBe(false); }); }); describe("isSupportedVideoMime / allow-list", () => { it("accepts MP4 and 3GPP only", () => { expect(isSupportedVideoMime("video/mp4")).toBe(true); expect(isSupportedVideoMime("video/3gpp")).toBe(true); expect(isSupportedVideoMime("video/3gpp2")).toBe(true); expect(isSupportedVideoMime("VIDEO/MP4")).toBe(true); }); it("rejects QuickTime / WebM / Matroska / AVI / OGG", () => { expect(isSupportedVideoMime("video/quicktime")).toBe(false); expect(isSupportedVideoMime("video/webm")).toBe(false); expect(isSupportedVideoMime("video/x-matroska")).toBe(false); expect(isSupportedVideoMime("video/x-msvideo")).toBe(false); expect(isSupportedVideoMime("video/ogg")).toBe(false); }); }); describe("isSupportedAudioMime / allow-list", () => { it("accepts the common formats", () => { expect(isSupportedAudioMime("audio/mpeg")).toBe(true); expect(isSupportedAudioMime("audio/mp4")).toBe(true); expect(isSupportedAudioMime("audio/aac")).toBe(true); expect(isSupportedAudioMime("audio/ogg")).toBe(true); expect(isSupportedAudioMime("audio/amr")).toBe(true); expect(isSupportedAudioMime("audio/wav")).toBe(true); }); it("rejects niche formats", () => { expect(isSupportedAudioMime("audio/x-flac")).toBe(false); expect(isSupportedAudioMime("audio/x-aiff")).toBe(false); }); }); describe("sniffUnsupportedImage / HEIF/AVIF magic-byte detection", () => { it("flags every HEIF/AVIF brand the bot's Sharp can't decode", () => { for (const brand of ["heic", "heix", "hevc", "heim", "heis", "mif1", "msf1", "avif", "avis"]) { expect(sniffUnsupportedImage(isobmffHeader(brand))).toBe(true); } }); it("brand match is case-insensitive", () => { expect(sniffUnsupportedImage(isobmffHeader("HEIC"))).toBe(true); expect(sniffUnsupportedImage(isobmffHeader("Avif"))).toBe(true); }); it("does NOT flag JPEG/PNG headers (no 'ftyp' marker)", () => { const jpeg = new Uint8Array([0xff, 0xd8, 0xff, 0xe0, 0, 0x10, 0x4a, 0x46, 0x49, 0x46, 0, 1]); const png = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 0x0d]); expect(sniffUnsupportedImage(jpeg)).toBe(false); expect(sniffUnsupportedImage(png)).toBe(false); }); it("does NOT flag unrelated ftyp brands (mp42, isom)", () => { expect(sniffUnsupportedImage(isobmffHeader("mp42"))).toBe(false); expect(sniffUnsupportedImage(isobmffHeader("isom"))).toBe(false); }); it("returns false for tiny / truncated buffers", () => { expect(sniffUnsupportedImage(new Uint8Array(0))).toBe(false); expect(sniffUnsupportedImage(new Uint8Array([0x66, 0x74, 0x79, 0x70]))).toBe(false); }); }); describe("sniffUnsupportedVideo / catch MOV-as-MP4", () => { it("flags QuickTime ('qt ') even when mime says video/mp4", () => { expect(sniffUnsupportedVideo(isobmffHeader("qt "))).toBe(true); }); it("does NOT flag genuine MP4 brands", () => { for (const brand of ["mp42", "mp41", "isom", "iso2", "iso5", "m4v "]) { expect(sniffUnsupportedVideo(isobmffHeader(brand))).toBe(false); } }); it("does NOT flag 3GP brands (3gp4 / 3gp5)", () => { expect(sniffUnsupportedVideo(isobmffHeader("3gp4"))).toBe(false); expect(sniffUnsupportedVideo(isobmffHeader("3gp5"))).toBe(false); }); it("returns false for non-ISOBMFF data — mime allow-list catches those", () => { const webm = new Uint8Array([0x1a, 0x45, 0xdf, 0xa3, 0, 0, 0, 0, 0, 0, 0, 0]); expect(sniffUnsupportedVideo(webm)).toBe(false); }); it("returns false for tiny buffers", () => { expect(sniffUnsupportedVideo(new Uint8Array(0))).toBe(false); expect(sniffUnsupportedVideo(new Uint8Array(8))).toBe(false); }); }); describe("resolveDeliveryKind — the cross-package contract", () => { it("supported native types stay in their own kind", () => { expect(resolveDeliveryKind("image/jpeg")).toBe("image"); expect(resolveDeliveryKind("image/png")).toBe("image"); expect(resolveDeliveryKind("video/mp4")).toBe("video"); expect(resolveDeliveryKind("video/3gpp")).toBe("video"); expect(resolveDeliveryKind("audio/mpeg")).toBe("audio"); expect(resolveDeliveryKind("audio/mp4")).toBe("audio"); }); it("HEIC by mime → document (no inline preview)", () => { expect(resolveDeliveryKind("image/heic")).toBe("document"); expect(resolveDeliveryKind("image/avif")).toBe("document"); }); it("HEIC by bytes (lying mime image/jpeg) → document", () => { expect(resolveDeliveryKind("image/jpeg", isobmffHeader("heic"))).toBe("document"); expect(resolveDeliveryKind("image/jpeg", isobmffHeader("avif"))).toBe("document"); }); it("MOV by mime (video/quicktime) → document", () => { expect(resolveDeliveryKind("video/quicktime")).toBe("document"); expect(resolveDeliveryKind("video/webm")).toBe("document"); }); it("MOV by bytes (lying mime video/mp4) → document", () => { expect(resolveDeliveryKind("video/mp4", isobmffHeader("qt "))).toBe("document"); }); it("FLAC / niche audio → document", () => { expect(resolveDeliveryKind("audio/x-flac")).toBe("document"); }); it("genuine MP4 / 3GP bytes do NOT get demoted", () => { expect(resolveDeliveryKind("video/mp4", isobmffHeader("mp42"))).toBe("video"); expect(resolveDeliveryKind("video/3gpp", isobmffHeader("3gp4"))).toBe("video"); }); it("any unrelated mime → document", () => { expect(resolveDeliveryKind("application/pdf")).toBe("document"); expect(resolveDeliveryKind("text/plain")).toBe("document"); expect(resolveDeliveryKind("application/octet-stream")).toBe("document"); }); }); describe("validateForWhatsApp — applies the cap of the RESOLVED kind", () => { it("accepts an image just under the 5 MB cap", () => { const r = validateForWhatsApp("image/jpeg", 5 * MB - 1); expect(r.ok).toBe(true); if (r.ok) { expect(r.kind).toBe("image"); expect(r.limitBytes).toBe(WA_LIMITS.image); } }); it("rejects a 6 MB image with a useful WhatsApp-flavoured message", () => { const r = validateForWhatsApp("image/jpeg", 6 * MB); expect(r.ok).toBe(false); if (!r.ok) { expect(r.kind).toBe("image"); expect(r.error).toMatch(/Image too large/); expect(r.error).toMatch(/6\.0 MB/); expect(r.error).toMatch(/5\.0 MB/); } }); it("HEIC at 30 MB is allowed: routes to document (100 MB cap)", () => { const r = validateForWhatsApp("image/heic", 30 * MB); expect(r.ok).toBe(true); if (r.ok) { // Resolved to document, so the 100 MB doc cap applies — not the // 5 MB image cap that would have rejected the 30 MB HEIC. expect(r.kind).toBe("document"); expect(r.limitBytes).toBe(WA_LIMITS.document); } }); it("HEIC at 110 MB still rejects: exceeds the document cap", () => { const r = validateForWhatsApp("image/heic", 110 * MB); expect(r.ok).toBe(false); if (!r.ok) { expect(r.kind).toBe("document"); expect(r.error).toMatch(/Document too large/); } }); it("MOV at 50 MB allowed (would be 16 MB cap as video, 100 MB as document)", () => { const r = validateForWhatsApp("video/quicktime", 50 * MB); expect(r.ok).toBe(true); if (r.ok) expect(r.kind).toBe("document"); }); it("genuine MP4 at 17 MB rejects: exceeds the 16 MB video cap", () => { const r = validateForWhatsApp("video/mp4", 17 * MB); expect(r.ok).toBe(false); if (!r.ok) expect(r.kind).toBe("video"); }); it("genuine MP4 byte-sniff path (mp42 brand) keeps it as video", () => { // 17 MB but bytes confirm mp42 → still capped at 16 MB. const r = validateForWhatsApp("video/mp4", 17 * MB, isobmffHeader("mp42")); expect(r.ok).toBe(false); if (!r.ok) expect(r.kind).toBe("video"); }); it("MOV pretending to be mp4 demotes to document (50 MB allowed)", () => { const r = validateForWhatsApp("video/mp4", 50 * MB, isobmffHeader("qt ")); expect(r.ok).toBe(true); if (r.ok) expect(r.kind).toBe("document"); }); it("rejects empty / zero-byte uploads regardless of kind", () => { expect(validateForWhatsApp("image/png", 0).ok).toBe(false); expect(validateForWhatsApp("application/pdf", 0).ok).toBe(false); }); it("a 60 MB unknown-mime upload is treated as a document and accepted", () => { const r = validateForWhatsApp("application/octet-stream", 60 * MB); expect(r.ok).toBe(true); if (r.ok) expect(r.kind).toBe("document"); }); it("FLAC audio routes to document path (unsupported audio mime)", () => { const r = validateForWhatsApp("audio/x-flac", 50 * MB); expect(r.ok).toBe(true); if (r.ok) expect(r.kind).toBe("document"); }); }); describe("WA_MAX_BYTES is the largest single-kind cap", () => { it("equals the document cap (100 MB)", () => { expect(WA_MAX_BYTES).toBe(WA_LIMITS.document); expect(WA_MAX_BYTES).toBe(100 * MB); }); it("is at least as large as every per-kind limit", () => { for (const limit of Object.values(WA_LIMITS)) { expect(WA_MAX_BYTES).toBeGreaterThanOrEqual(limit); } }); }); describe("formatBytes", () => { it("renders bytes < 1 KB plainly", () => { expect(formatBytes(0)).toBe("0 B"); expect(formatBytes(512)).toBe("512 B"); }); it("renders KB rounded to no decimals", () => { expect(formatBytes(1024)).toBe("1 KB"); expect(formatBytes(102_400)).toBe("100 KB"); }); it("renders MB to one decimal", () => { expect(formatBytes(5 * MB)).toBe("5.0 MB"); expect(formatBytes(5_400_000)).toBe("5.1 MB"); }); });