Files
community-rule/tests/unit/createFlowUploadValidation.test.ts
T

165 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(
'<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>',
);
const XML_SVG = new TextEncoder().encode(
'<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg"/>',
);
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();
});
});