import { describe, expect, it, vi } from "vitest"; import { CreateFlowUploadValidationError, maxBytesForPurpose, validateCreateFlowUploadBytes, validateCreateFlowUploadFile, } from "../../lib/create/createFlowUploadValidation"; import { uploadCreateFlowFile } from "../../lib/create/uploadToServer"; /** 1×1 PNG (valid IHDR). */ const PNG_1X1 = Uint8Array.from( Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", "base64", ), ); /** SOF0 1×1 JPEG (header-decodable; not necessarily a complete scan). */ const JPEG_1X1_SOF = Uint8Array.from([ 0xff, 0xd8, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xff, 0xd9, ]); const GIF_1X1 = Uint8Array.from([ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x3b, ]); /** VP8X 1×1 WebP. */ const WEBP_1X1 = Uint8Array.from([ 0x52, 0x49, 0x46, 0x46, 0x16, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x58, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]); const PDF_STUB = new TextEncoder().encode("%PDF-1.4\n%%EOF\n"); const SVG_MARKUP = new TextEncoder().encode( '', ); const XML_SVG = new TextEncoder().encode( '', ); describe("validateCreateFlowUploadBytes", () => { it("accepts a real PNG as a community avatar", () => { const result = validateCreateFlowUploadBytes("communityAvatar", PNG_1X1); expect(result).toEqual({ ok: true, kind: "png", mimeType: "image/png", }); }); it("accepts JPEG, GIF, and WebP headers", () => { expect(validateCreateFlowUploadBytes("communityAvatar", JPEG_1X1_SOF).ok).toBe( true, ); expect(validateCreateFlowUploadBytes("communityAvatar", GIF_1X1).ok).toBe( true, ); expect(validateCreateFlowUploadBytes("communityAvatar", WEBP_1X1).ok).toBe( true, ); }); it("rejects an empty buffer", () => { expect(validateCreateFlowUploadBytes("communityAvatar", new Uint8Array())).toEqual( { ok: false, reason: "empty" }, ); }); it("rejects a file larger than the purpose cap", () => { const oversized = new Uint8Array(maxBytesForPurpose("communityAvatar") + 1); oversized.set(PNG_1X1.subarray(0, 8), 0); expect(validateCreateFlowUploadBytes("communityAvatar", oversized)).toEqual({ ok: false, reason: "tooLarge", }); expect(maxBytesForPurpose("customMethodAttachment")).toBeGreaterThan( maxBytesForPurpose("communityAvatar"), ); }); it("rejects text spoofed as PNG", () => { expect( validateCreateFlowUploadBytes( "communityAvatar", new TextEncoder().encode("hello world"), ), ).toEqual({ ok: false, reason: "invalidType" }); }); it("rejects SVG markup, including xml-prefixed SVG", () => { expect(validateCreateFlowUploadBytes("communityAvatar", SVG_MARKUP)).toEqual( { ok: false, reason: "svg" }, ); expect(validateCreateFlowUploadBytes("customMethodAttachment", XML_SVG)).toEqual( { ok: false, reason: "svg" }, ); }); it("rejects a PNG signature that cannot be decoded", () => { const truncated = PNG_1X1.subarray(0, 8); expect(validateCreateFlowUploadBytes("communityAvatar", truncated)).toEqual({ ok: false, reason: "undecodable", }); }); it("rejects PDF for community avatars and allows it for attachments", () => { expect(validateCreateFlowUploadBytes("communityAvatar", PDF_STUB)).toEqual({ ok: false, reason: "invalidType", }); expect(validateCreateFlowUploadBytes("customMethodAttachment", PDF_STUB)).toEqual( { ok: true, kind: "pdf", mimeType: "application/pdf", }, ); }); }); describe("validateCreateFlowUploadFile", () => { it("rejects empty files before reading bytes", async () => { const file = new File([], "empty.png", { type: "image/png" }); await expect( validateCreateFlowUploadFile(file, "communityAvatar"), ).resolves.toEqual({ ok: false, reason: "empty" }); }); it("rejects SVG by name even when the MIME is spoofed", async () => { const file = new File([PNG_1X1], "logo.svg", { type: "image/png" }); await expect( validateCreateFlowUploadFile(file, "communityAvatar"), ).resolves.toEqual({ ok: false, reason: "svg" }); }); it("rejects a 17MB file by size without treating it as a type error", async () => { const file = new File([new Uint8Array([0x89, 0x50])], "huge.png", { type: "image/png", }); Object.defineProperty(file, "size", { value: 17 * 1024 * 1024 }); await expect( validateCreateFlowUploadFile(file, "communityAvatar"), ).resolves.toEqual({ ok: false, reason: "tooLarge" }); }); }); describe("uploadCreateFlowFile", () => { it("does not POST when client validation fails", async () => { const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); const file = new File(["not an image"], "photo.png", { type: "image/png" }); await expect( uploadCreateFlowFile(file, "communityAvatar"), ).rejects.toBeInstanceOf(CreateFlowUploadValidationError); expect(fetchMock).not.toHaveBeenCalled(); vi.unstubAllGlobals(); }); });