Restore the community photo after reload and reject empty, oversized, SVG, and spoofed uploads.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user