import { describe, test, expect } from "vitest"; import sharp from "sharp"; import { MAX_IMAGE_DIMENSION, MAX_UPLOAD_BYTES, reencodeImage } from "./uploads"; async function makeImage(width: number, height: number): Promise { return sharp({ create: { width, height, channels: 3, background: { r: 210, g: 160, b: 40 } } }) .jpeg({ quality: 90 }) .toBuffer(); } describe("reencodeImage", () => { test("scales an oversized landscape photo down to the long-edge limit and keeps its aspect ratio", async () => { // Ein 24-MP-Handyfoto wurde bisher unveraendert gross gespeichert: das // kostete Platz auf dem Upload-Volume UND zwang next/image, fuer jede // ausgelieferte Groesse das komplette Bild neu zu dekodieren. const source = await makeImage(6000, 4000); const meta = await sharp(await reencodeImage(source, "jpg")).metadata(); expect(meta.width).toBe(MAX_IMAGE_DIMENSION); expect(meta.height).toBe(Math.round((MAX_IMAGE_DIMENSION * 4000) / 6000)); }); test("scales an oversized portrait photo by its height instead", async () => { const source = await makeImage(3000, 4500); const meta = await sharp(await reencodeImage(source, "webp")).metadata(); expect(meta.height).toBe(MAX_IMAGE_DIMENSION); expect(meta.width).toBe(Math.round((MAX_IMAGE_DIMENSION * 3000) / 4500)); }); test("leaves an image that is already small enough at its original size", async () => { const source = await makeImage(1200, 800); const meta = await sharp(await reencodeImage(source, "jpg")).metadata(); expect(meta.width).toBe(1200); expect(meta.height).toBe(800); }); test("never enlarges a small image", async () => { const source = await makeImage(320, 240); const meta = await sharp(await reencodeImage(source, "png")).metadata(); expect(meta.width).toBe(320); expect(meta.height).toBe(240); }); test("produces a file that is smaller than the oversized original", async () => { const source = await makeImage(6000, 4000); const output = await reencodeImage(source, "jpg"); expect(output.length).toBeLessThan(source.length); }); test("rejects a decompression bomb instead of decoding it", async () => { // Ein kleines File, das sich zu absurd vielen Pixeln entpackt, wuerde // beim Dekodieren den Arbeitsspeicher fuellen. sharp bekommt deshalb // eine ausdrueckliche Pixel-Obergrenze. const bomb = await sharp({ create: { width: 11000, height: 11000, channels: 3, background: { r: 0, g: 0, b: 0 } } }) .png({ compressionLevel: 9 }) .toBuffer(); await expect(reencodeImage(bomb, "jpg")).rejects.toThrow(); }, 120_000); }); describe("MAX_UPLOAD_BYTES", () => { test("accommodates a photo straight out of a modern phone camera", () => { // 8 MB lagen unter der Groesse, die aktuelle Handykameras (48 MP, // HEIC-Export nach JPEG) routinemaessig erzeugen — der Upload brach mit // "Die Datei ist zu gross" ab, obwohl das Bild serverseitig ohnehin // verkleinert und neu enkodiert wird. expect(MAX_UPLOAD_BYTES).toBeGreaterThanOrEqual(20 * 1024 * 1024); }); });