import { describe, test, expect } from "vitest"; import path from "node:path"; import { UPLOAD_DIR, extensionForFormat, contentTypeForExtension, generateUploadFilename, isValidUploadPath, resolveUploadPath, detectVideoExtension, isValidVideoUploadPath, } from "./uploads"; describe("extensionForFormat", () => { test("maps sharp's jpeg format to the jpg extension", () => { expect(extensionForFormat("jpeg")).toBe("jpg"); }); test("passes png/webp/avif through unchanged", () => { expect(extensionForFormat("png")).toBe("png"); expect(extensionForFormat("webp")).toBe("webp"); expect(extensionForFormat("avif")).toBe("avif"); }); test("rejects an unsupported format (e.g. gif/svg/tiff)", () => { expect(extensionForFormat("gif")).toBeNull(); expect(extensionForFormat("svg")).toBeNull(); expect(extensionForFormat("tiff")).toBeNull(); }); test("rejects an undefined format", () => { expect(extensionForFormat(undefined)).toBeNull(); }); }); describe("contentTypeForExtension", () => { test("returns the correct MIME type for each allowed extension", () => { expect(contentTypeForExtension("jpg")).toBe("image/jpeg"); expect(contentTypeForExtension("png")).toBe("image/png"); expect(contentTypeForExtension("webp")).toBe("image/webp"); expect(contentTypeForExtension("avif")).toBe("image/avif"); }); test("returns the correct MIME type for the video extensions", () => { expect(contentTypeForExtension("mp4")).toBe("video/mp4"); expect(contentTypeForExtension("webm")).toBe("video/webm"); }); test("returns null for an extension outside the fixed allow-list", () => { // Verhindert, dass eine Datei je als text/html o.ae. ausgeliefert wird — // siehe app/uploads/[...path]/route.ts. expect(contentTypeForExtension("html")).toBeNull(); expect(contentTypeForExtension("svg")).toBeNull(); expect(contentTypeForExtension("")).toBeNull(); }); }); describe("detectVideoExtension", () => { function bufferWithFtypAt4(): Buffer { // Reale MP4-Dateien haben eine variable Box-Groesse in den ersten 4 // Bytes, gefolgt von der literalen ASCII-Kennung "ftyp" — die konkreten // ersten 4 Bytes sind fuer die Erkennung irrelevant. return Buffer.concat([Buffer.from([0, 0, 0, 24]), Buffer.from("ftypisom", "ascii")]); } test("recognizes an MP4 file via its ftyp box", () => { expect(detectVideoExtension(bufferWithFtypAt4())).toBe("mp4"); }); test("recognizes a WebM file via its EBML header", () => { expect(detectVideoExtension(Buffer.from([0x1a, 0x45, 0xdf, 0xa3, 0x01, 0x02]))).toBe("webm"); }); test("rejects a buffer that is neither MP4 nor WebM (e.g. a JPEG or arbitrary bytes)", () => { expect(detectVideoExtension(Buffer.from([0xff, 0xd8, 0xff, 0xe0]))).toBeNull(); expect(detectVideoExtension(Buffer.from("not a video", "ascii"))).toBeNull(); }); test("rejects a buffer too short to contain either signature", () => { expect(detectVideoExtension(Buffer.from([0x1a, 0x45]))).toBeNull(); expect(detectVideoExtension(Buffer.alloc(0))).toBeNull(); }); }); describe("generateUploadFilename", () => { test("produces a 32-char lowercase hex name with the given extension", () => { const name = generateUploadFilename("webp"); expect(name).toMatch(/^[a-f0-9]{32}\.webp$/); }); test("never reuses the same name across calls (collision-resistant)", () => { const names = new Set(Array.from({ length: 50 }, () => generateUploadFilename("jpg"))); expect(names.size).toBe(50); }); }); describe("isValidUploadPath", () => { test("accepts a path in exactly the shape the upload route produces", () => { expect(isValidUploadPath(`/uploads/${generateUploadFilename("png")}`)).toBe(true); }); test("rejects a path traversal attempt", () => { expect(isValidUploadPath("/uploads/../../etc/passwd")).toBe(false); }); test("rejects an absolute external URL (not an upload-endpoint path)", () => { expect(isValidUploadPath("https://attacker.example/x.png")).toBe(false); }); test("rejects a javascript: pseudo-protocol value", () => { expect(isValidUploadPath("javascript:alert(1)")).toBe(false); }); test("rejects a client-chosen filename instead of the generated random one", () => { expect(isValidUploadPath("/uploads/my-photo.png")).toBe(false); }); test("rejects an extension outside the allow-list", () => { expect(isValidUploadPath("/uploads/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.svg")).toBe(false); }); test("rejects an empty string", () => { expect(isValidUploadPath("")).toBe(false); }); }); describe("isValidVideoUploadPath", () => { test("accepts a path in exactly the shape the video upload route produces", () => { expect(isValidVideoUploadPath(`/uploads/${generateUploadFilename("mp4")}`)).toBe(true); expect(isValidVideoUploadPath(`/uploads/${generateUploadFilename("webm")}`)).toBe(true); }); test("rejects an image path (different endpoint's output)", () => { expect(isValidVideoUploadPath(`/uploads/${generateUploadFilename("png")}`)).toBe(false); }); test("rejects a path traversal attempt", () => { expect(isValidVideoUploadPath("/uploads/../../etc/passwd")).toBe(false); }); test("rejects an absolute external URL (not an upload-endpoint path)", () => { expect(isValidVideoUploadPath("https://attacker.example/x.mp4")).toBe(false); }); test("rejects a client-chosen filename instead of the generated random one", () => { expect(isValidVideoUploadPath("/uploads/my-video.mp4")).toBe(false); }); test("rejects an empty string", () => { expect(isValidVideoUploadPath("")).toBe(false); }); }); describe("resolveUploadPath", () => { test("resolves a plain filename to a path inside UPLOAD_DIR", () => { const resolved = resolveUploadPath(["abc123.png"]); expect(resolved).toBe(path.join(UPLOAD_DIR, "abc123.png")); }); test("resolves nested-looking but safe segments inside UPLOAD_DIR", () => { const resolved = resolveUploadPath(["sub", "abc123.png"]); expect(resolved).toBe(path.join(UPLOAD_DIR, "sub", "abc123.png")); }); test("rejects a '..' segment (path traversal attempt)", () => { expect(resolveUploadPath(["..", "..", "etc", "passwd"])).toBeNull(); }); test("rejects a segment that is a single '.'", () => { expect(resolveUploadPath([".", "abc123.png"])).toBeNull(); }); test("rejects an empty segment", () => { expect(resolveUploadPath([""])).toBeNull(); }); test("rejects an encoded traversal baked into one segment", () => { // Next.js liefert Catch-all-Segmente bereits prozent-dekodiert an — ein // Segment kann trotzdem woertlich ".." enthalten, wenn jemand die Route // direkt (nicht ueber generierte Links) aufruft. expect(resolveUploadPath(["a", "..", "..", "b"])).toBeNull(); }); });