/** * Adversarial coverage gap tests for the SnapOtter image API. * * Fills the remaining gaps after analyzing all existing adversarial, edge-case, * concurrent, and pipeline test files (342 tests total). Focuses on scenarios * that are either missing entirely or only partially covered: * * 1. Concurrent response content integrity (download + verify different outputs) * 2. Memory stress: 50 sequential large uploads with memory monitoring * 3. Zero-byte files with various MIME types through additional tools * 4. Format mismatch: GIF data with .webp extension, animated GIF through resize * 5. Extreme output dimensions: resize to 10000x1 and 1x10000 * 6. Pipeline step limit enforcement (with MAX_PIPELINE_STEPS configured) * 7. Batch with 50+ images (beyond configured MAX_BATCH_SIZE=10) * 8. Multipart with missing/malformed boundary * 9. Multipart with extra unexpected part fields * 10. Request body edge cases: empty multipart, form-urlencoded, no content-type */ import { readFileSync } from "node:fs"; import { join } from "node:path"; import sharp from "sharp"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp } from "./test-server.js"; // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const FIXTURES = join(__dirname, "..", "fixtures"); const PNG_200x150 = readFileSync(join(FIXTURES, "test-200x150.png")); const PNG_1x1 = readFileSync(join(FIXTURES, "test-1x1.png")); const JPG_100x100 = readFileSync(join(FIXTURES, "test-100x100.jpg")); const WEBP_50x50 = readFileSync(join(FIXTURES, "test-50x50.webp")); const ANIMATED_GIF = readFileSync(join(FIXTURES, "animated.gif")); const STRESS_LARGE = readFileSync(join(FIXTURES, "content", "stress-large.jpg")); // --------------------------------------------------------------------------- // Shared state // --------------------------------------------------------------------------- let testApp: TestApp; let app: TestApp["app"]; let adminToken: string; beforeAll(async () => { testApp = await buildTestApp(); app = testApp.app; adminToken = await loginAsAdmin(app); }, 30_000); afterAll(async () => { await testApp.cleanup(); }, 10_000); /** Helper to POST a multipart payload to a tool endpoint. */ function postTool( toolId: string, fields: Array<{ name: string; filename?: string; contentType?: string; content: Buffer | string; }>, ) { const { body, contentType } = createMultipartPayload(fields); return app.inject({ method: "POST", url: `/api/v1/tools/${toolId}`, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, body, }); } /** Helper to POST a batch request. */ function postBatch( toolId: string, fields: Array<{ name: string; filename?: string; contentType?: string; content: Buffer | string; }>, ) { const { body, contentType } = createMultipartPayload(fields); return app.inject({ method: "POST", url: `/api/v1/tools/${toolId}/batch`, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, body, }); } /** Helper to POST a pipeline execution request. */ function executePipeline( image: Buffer, filename: string, pipeline: { steps: Array<{ toolId: string; settings?: Record }>; }, ) { const { body, contentType } = createMultipartPayload([ { name: "file", filename, content: image, contentType: "image/png" }, { name: "pipeline", content: JSON.stringify(pipeline) }, ]); return app.inject({ method: "POST", url: "/api/v1/pipeline/execute", headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, body, }); } /** Helper to build an inject config for a tool request. */ function buildToolRequest( toolId: string, image: Buffer, filename: string, settings: Record, imgContentType = "image/png", ) { const { body, contentType } = createMultipartPayload([ { name: "file", filename, content: image, contentType: imgContentType }, { name: "settings", content: JSON.stringify(settings) }, ]); return { method: "POST" as const, url: `/api/v1/tools/${toolId}`, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, body, }; } // ########################################################################### // 1. CONCURRENT RESPONSE CONTENT INTEGRITY VERIFICATION // Existing tests verify unique job IDs but never download the actual // output files to confirm no data mixing. These tests do. // ########################################################################### describe("Concurrent response content integrity -- download verification", () => { it("10 simultaneous resize requests produce correctly-sized output files", async () => { const widths = [20, 40, 60, 80, 100, 120, 140, 160, 180, 199]; const results = await Promise.all( widths.map((width, i) => app.inject(buildToolRequest("resize", PNG_200x150, `integrity-${i}.png`, { width })), ), ); // All must succeed with unique IDs for (const res of results) { expect(res.statusCode).toBe(200); } const jobIds = results.map((r) => JSON.parse(r.body).jobId); expect(new Set(jobIds).size).toBe(10); // Download each output and verify the actual pixel width matches for (let i = 0; i < results.length; i++) { const downloadUrl = JSON.parse(results[i].body).downloadUrl; const dlRes = await app.inject({ method: "GET", url: downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const metadata = await sharp(dlRes.rawPayload).metadata(); expect(metadata.width).toBe(widths[i]); } }, 120_000); it("10 simultaneous uploads to DIFFERENT tools all produce correct tool-specific outputs", async () => { const requests = [ buildToolRequest("resize", PNG_200x150, "mt-resize-1.png", { width: 50 }), buildToolRequest("resize", PNG_200x150, "mt-resize-2.png", { width: 100 }), buildToolRequest("rotate", PNG_200x150, "mt-rotate-1.png", { angle: 90 }), buildToolRequest("rotate", PNG_200x150, "mt-rotate-2.png", { angle: 180 }), buildToolRequest("compress", PNG_200x150, "mt-compress-1.png", { quality: 30 }), buildToolRequest("compress", PNG_200x150, "mt-compress-2.png", { quality: 90 }), buildToolRequest("border", PNG_200x150, "mt-border-1.png", { borderWidth: 5 }), buildToolRequest("border", PNG_200x150, "mt-border-2.png", { borderWidth: 20 }), buildToolRequest("convert", PNG_200x150, "mt-convert-1.png", { format: "webp" }), buildToolRequest("convert", PNG_200x150, "mt-convert-2.png", { format: "jpg" }), ]; const results = await Promise.all(requests.map((req) => app.inject(req))); // All succeed for (const res of results) { expect(res.statusCode).toBe(200); } // All unique job IDs const jobIds = results.map((r) => JSON.parse(r.body).jobId); expect(new Set(jobIds).size).toBe(10); // Download the resize outputs and verify pixel widths const resize1Url = JSON.parse(results[0].body).downloadUrl; const resize2Url = JSON.parse(results[1].body).downloadUrl; const [dl1, dl2] = await Promise.all([ app.inject({ method: "GET", url: resize1Url, headers: { authorization: `Bearer ${adminToken}` }, }), app.inject({ method: "GET", url: resize2Url, headers: { authorization: `Bearer ${adminToken}` }, }), ]); const meta1 = await sharp(dl1.rawPayload).metadata(); const meta2 = await sharp(dl2.rawPayload).metadata(); expect(meta1.width).toBe(50); expect(meta2.width).toBe(100); // Download a rotate output and verify dimensions swapped const rotate90Url = JSON.parse(results[2].body).downloadUrl; const dlRotate = await app.inject({ method: "GET", url: rotate90Url, headers: { authorization: `Bearer ${adminToken}` }, }); const metaRotate = await sharp(dlRotate.rawPayload).metadata(); // 200x150 rotated 90 degrees becomes 150x200 expect(metaRotate.width).toBe(150); expect(metaRotate.height).toBe(200); }, 120_000); }); // ########################################################################### // 2. MEMORY STRESS: 50 SEQUENTIAL LARGE FILE UPLOADS // Existing tests only do 5--10 sequential large uploads. This exercises // 50 and checks for memory growth and response time degradation. // ########################################################################### describe("Memory stress -- 50 sequential large-file uploads", () => { it("processes stress-large.jpg 50 times sequentially without memory blowup or degradation", async () => { const memBefore = process.memoryUsage(); const responseTimes: number[] = []; for (let i = 0; i < 50; i++) { const start = Date.now(); const res = await postTool("resize", [ { name: "file", filename: `mem-stress-${i}.jpg`, content: STRESS_LARGE, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 200 }) }, ]); const elapsed = Date.now() - start; responseTimes.push(elapsed); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.jobId).toBeDefined(); expect(json.processedSize).toBeLessThan(json.originalSize); } const memAfter = process.memoryUsage(); // All 50 completed successfully (verified in the loop above) // Memory growth check: RSS should not grow by more than 500MB // (50 * 6.7MB = 335MB raw data, but Sharp streams so actual growth // should be much less if buffers are properly released) const rssGrowthMB = (memAfter.rss - memBefore.rss) / (1024 * 1024); expect(rssGrowthMB).toBeLessThan(500); // Response time degradation check: the last 10 requests should not // take more than 5x the average of the first 10 const firstTenAvg = responseTimes.slice(0, 10).reduce((a, b) => a + b, 0) / 10; const lastTenAvg = responseTimes.slice(40, 50).reduce((a, b) => a + b, 0) / 10; // Allow generous margin -- the key is no exponential blowup expect(lastTenAvg).toBeLessThan(firstTenAvg * 5); // Server still responsive const healthRes = await app.inject({ method: "GET", url: "/api/v1/health" }); expect(healthRes.statusCode).toBe(200); }, 600_000); }); // ########################################################################### // 3. ZERO-BYTE FILES -- ADDITIONAL TOOLS AND MIME VARIANTS // Covers tools and MIME combinations not tested in existing files. // ########################################################################### describe("Zero-byte files with various MIME types through additional tools", () => { it("rejects zero-byte file with image/gif MIME to resize", async () => { const res = await postTool("resize", [ { name: "file", filename: "empty.gif", content: Buffer.alloc(0), contentType: "image/gif", }, { name: "settings", content: JSON.stringify({ width: 100 }) }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects zero-byte file with image/avif MIME to compress", async () => { const res = await postTool("compress", [ { name: "file", filename: "empty.avif", content: Buffer.alloc(0), contentType: "image/avif", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects zero-byte file with image/tiff MIME to convert", async () => { const res = await postTool("convert", [ { name: "file", filename: "empty.tiff", content: Buffer.alloc(0), contentType: "image/tiff", }, { name: "settings", content: JSON.stringify({ format: "png" }) }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects zero-byte file to text-overlay", async () => { const res = await postTool("text-overlay", [ { name: "file", filename: "empty.png", content: Buffer.alloc(0), contentType: "image/png", }, { name: "settings", content: JSON.stringify({ text: "Hello", fontSize: 24 }), }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects zero-byte file to color-palette", async () => { const res = await postTool("color-palette", [ { name: "file", filename: "empty.jpg", content: Buffer.alloc(0), contentType: "image/jpeg", }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects zero-byte file to favicon", async () => { const res = await postTool("favicon", [ { name: "file", filename: "empty.png", content: Buffer.alloc(0), contentType: "image/png", }, { name: "settings", content: JSON.stringify({}) }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); }); // ########################################################################### // 4. FORMAT MISMATCH: GIF DATA WITH WRONG EXTENSION, ANIMATED GIF // Existing tests cover PNG-as-JPG, JPEG-as-PNG, JPEG-as-WebP, WebP-as-PNG. // Missing: GIF data with .webp extension, animated GIF through tools. // ########################################################################### describe("Format mismatch -- GIF data and animated GIF handling", () => { it("handles GIF data uploaded with .webp extension", async () => { const res = await postTool("resize", [ { name: "file", filename: "actually-a-gif.webp", content: ANIMATED_GIF, contentType: "image/webp", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); // Sharp auto-detects GIF via magic bytes expect([200, 400, 422]).toContain(res.statusCode); }); it("handles GIF data uploaded with .jpg extension", async () => { const res = await postTool("resize", [ { name: "file", filename: "actually-a-gif.jpg", content: ANIMATED_GIF, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); // Sharp detects the real GIF format expect([200, 400, 422]).toContain(res.statusCode); }); it("handles WebP data uploaded with .gif extension", async () => { const res = await postTool("resize", [ { name: "file", filename: "actually-webp.gif", content: WEBP_50x50, contentType: "image/gif", }, { name: "settings", content: JSON.stringify({ width: 30 }) }, ]); // Sharp detects WebP via magic bytes expect(res.statusCode).toBe(200); }); it("handles JPEG data uploaded with .gif extension", async () => { const res = await postTool("compress", [ { name: "file", filename: "actually-jpeg.gif", content: JPG_100x100, contentType: "image/gif", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); // Sharp detects JPEG via magic bytes expect(res.statusCode).toBe(200); }); it("resizes an animated GIF without crashing", async () => { const res = await postTool("resize", [ { name: "file", filename: "animated.gif", content: ANIMATED_GIF, contentType: "image/gif", }, { name: "settings", content: JSON.stringify({ width: 30 }) }, ]); // Animated GIF may lose frames during processing but must not crash expect([200, 400, 422]).toContain(res.statusCode); }); it("compresses an animated GIF without crashing", async () => { const res = await postTool("compress", [ { name: "file", filename: "animated.gif", content: ANIMATED_GIF, contentType: "image/gif", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); expect([200, 400, 422]).toContain(res.statusCode); }); it("gets info for an animated GIF", async () => { const res = await postTool("info", [ { name: "file", filename: "animated.gif", content: ANIMATED_GIF, contentType: "image/gif", }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.format).toBe("gif"); expect(json.width).toBeGreaterThan(0); expect(json.height).toBeGreaterThan(0); }); }); // ########################################################################### // 5. EXTREME OUTPUT DIMENSIONS: RESIZE TO 10000x1 AND 1x10000 // Existing tests create 1x1000 and 1000x1 INPUT images but never test // RESIZING a normal image to extreme aspect ratio OUTPUT dimensions. // ########################################################################### describe("Extreme output dimensions -- resize to extreme aspect ratios", () => { it("resizes 200x150 image to 10000x1 (very wide output)", async () => { const res = await postTool("resize", [ { name: "file", filename: "to-wide.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 10000, height: 1 }), }, ]); // Sharp may succeed with fit:fill or reject at processing. Must not crash. expect([200, 400, 422]).toContain(res.statusCode); }, 60_000); it("resizes 200x150 image to 1x10000 (very tall output)", async () => { const res = await postTool("resize", [ { name: "file", filename: "to-tall.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 1, height: 10000 }), }, ]); expect([200, 400, 422]).toContain(res.statusCode); }, 60_000); it("resizes 1x1 image to 10000x10000 (massive upscale)", async () => { const res = await postTool("resize", [ { name: "file", filename: "massive-upscale.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 10000, height: 10000 }), }, ]); // May be rejected by MAX_MEGAPIXELS (100MP limit, 10000x10000 = 100MP exactly) // or succeed. Must not crash. expect([200, 400, 422]).toContain(res.statusCode); }, 120_000); it("resizes to width=1 only (auto height)", async () => { const res = await postTool("resize", [ { name: "file", filename: "width-1.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 1 }) }, ]); expect(res.statusCode).toBe(200); const dlUrl = JSON.parse(res.body).downloadUrl; const dlRes = await app.inject({ method: "GET", url: dlUrl, headers: { authorization: `Bearer ${adminToken}` }, }); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.width).toBe(1); }); it("resizes to height=1 only (auto width)", async () => { const res = await postTool("resize", [ { name: "file", filename: "height-1.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ height: 1 }) }, ]); expect(res.statusCode).toBe(200); const dlUrl = JSON.parse(res.body).downloadUrl; const dlRes = await app.inject({ method: "GET", url: dlUrl, headers: { authorization: `Bearer ${adminToken}` }, }); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.height).toBe(1); }); }); // ########################################################################### // 6. PIPELINE WITH CONFLICTING OPS AND FORMAT DEPENDENCY // Existing tests cover resize-then-crop-larger, but not the specific // scenario of step 2 depending on step 1 output format. // ########################################################################### describe("Pipeline -- format-dependent step chains", () => { it("convert to JPEG (lossy) then compress -- compression applies to JPEG data", async () => { const res = await executePipeline(PNG_200x150, "fmt-dep-1.png", { steps: [ { toolId: "convert", settings: { format: "jpg" } }, { toolId: "compress", settings: { quality: 10 } }, ], }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(2); // Low quality JPEG compression on an already-JPEG buffer should reduce size expect(json.processedSize).toBeLessThan(json.originalSize); }); it("resize to 10x10 then crop 5x5 from center -- succeeds with small output", async () => { const res = await executePipeline(PNG_200x150, "resize-crop-ok.png", { steps: [ { toolId: "resize", settings: { width: 10, height: 10 } }, { toolId: "crop", settings: { left: 2, top: 2, width: 5, height: 5 } }, ], }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(2); // Download and verify dimensions const dlRes = await app.inject({ method: "GET", url: json.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.width).toBe(5); expect(meta.height).toBe(5); }); it("resize to 10x10 then crop 100x100 -- fails at crop step", async () => { const res = await executePipeline(PNG_200x150, "resize-crop-fail.png", { steps: [ { toolId: "resize", settings: { width: 10, height: 10 } }, { toolId: "crop", settings: { left: 0, top: 0, width: 100, height: 100 } }, ], }); // Crop exceeds resized dimensions expect([200, 422]).toContain(res.statusCode); }); it("convert PNG to AVIF then rotate -- AVIF intermediate works", async () => { const res = await executePipeline(PNG_200x150, "avif-rotate.png", { steps: [ { toolId: "convert", settings: { format: "avif" } }, { toolId: "rotate", settings: { angle: 90 } }, ], }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(2); }); it("triple format conversion: PNG -> JPEG -> WebP -> AVIF", async () => { const res = await executePipeline(PNG_200x150, "triple-convert.png", { steps: [ { toolId: "convert", settings: { format: "jpg" } }, { toolId: "convert", settings: { format: "webp" } }, { toolId: "convert", settings: { format: "avif" } }, ], }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(3); }); }); // ########################################################################### // 7. BATCH LIMITS -- EXCEEDING MAX_BATCH_SIZE WITH LARGE COUNT // Existing tests go up to 11 images (limit is 10). This tests larger counts // to ensure the server handles the rejection gracefully. // ########################################################################### describe("Batch limits -- large file counts beyond MAX_BATCH_SIZE", () => { it("rejects batch with 20 images (double the MAX_BATCH_SIZE=10)", async () => { const fields = Array.from({ length: 20 }, (_, i) => ({ name: "file", filename: `batch-20-${i}.png`, contentType: "image/png", content: PNG_1x1, // Use tiny images to keep payload small })); fields.push({ name: "settings", filename: undefined as unknown as string, contentType: undefined as unknown as string, content: JSON.stringify({ width: 10 }) as unknown as Buffer, }); const res = await postBatch("resize", fields); // @fastify/multipart enforces file limit at parser level expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects batch with 50 images (5x the limit)", async () => { const fields = Array.from({ length: 50 }, (_, i) => ({ name: "file", filename: `batch-50-${i}.png`, contentType: "image/png", content: PNG_1x1, })); fields.push({ name: "settings", filename: undefined as unknown as string, contentType: undefined as unknown as string, content: JSON.stringify({ width: 10 }) as unknown as Buffer, }); const res = await postBatch("resize", fields); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("batch + simultaneous single request -- no corruption when batch is rejected", async () => { // Fire a batch that exceeds the limit alongside a valid single request const oversizedFields = Array.from({ length: 15 }, (_, i) => ({ name: "file", filename: `over-${i}.png`, contentType: "image/png", content: PNG_1x1, })); oversizedFields.push({ name: "settings", filename: undefined as unknown as string, contentType: undefined as unknown as string, content: JSON.stringify({ width: 10 }) as unknown as Buffer, }); const [batchRes, singleRes] = await Promise.all([ postBatch("resize", oversizedFields), app.inject(buildToolRequest("resize", PNG_200x150, "single-ok.png", { width: 80 })), ]); // Batch should be rejected expect(batchRes.statusCode).toBe(400); // Single request must succeed unaffected expect(singleRes.statusCode).toBe(200); const singleBody = JSON.parse(singleRes.body); expect(singleBody.jobId).toBeDefined(); expect(singleBody.downloadUrl).toContain("resize"); }, 60_000); }); // ########################################################################### // 8. MULTIPART EDGE CASES -- MISSING BOUNDARY, EMPTY BODY, MALFORMED // ########################################################################### describe("Multipart edge cases -- malformed requests", () => { it("rejects request with content-type multipart/form-data but no boundary", async () => { const res = await app.inject({ method: "POST", url: "/api/v1/tools/resize", headers: { "content-type": "multipart/form-data", authorization: `Bearer ${adminToken}`, }, body: Buffer.from("some random data"), }); // Fastify/multipart should reject malformed multipart expect([400, 415]).toContain(res.statusCode); }); it("rejects request with empty body and multipart content-type", async () => { const res = await app.inject({ method: "POST", url: "/api/v1/tools/resize", headers: { "content-type": "multipart/form-data; boundary=----TestBoundary", authorization: `Bearer ${adminToken}`, }, body: Buffer.alloc(0), }); expect([400, 415]).toContain(res.statusCode); }); it("rejects request with form-urlencoded content-type", async () => { const res = await app.inject({ method: "POST", url: "/api/v1/tools/resize", headers: { "content-type": "application/x-www-form-urlencoded", authorization: `Bearer ${adminToken}`, }, body: "width=100&height=100", }); expect([400, 415]).toContain(res.statusCode); }); it("rejects request with no content-type header", async () => { const res = await app.inject({ method: "POST", url: "/api/v1/tools/resize", headers: { authorization: `Bearer ${adminToken}`, }, body: Buffer.from("some data"), }); expect([400, 415]).toContain(res.statusCode); }); }); // ########################################################################### // 9. MULTIPART WITH EXTRA UNEXPECTED FIELDS // Existing tests check extra JSON keys in settings. This tests extra // multipart parts that the server should ignore or handle. // ########################################################################### describe("Multipart with extra unexpected fields", () => { it("ignores extra text fields alongside valid file and settings", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png" }, { name: "settings", content: JSON.stringify({ width: 100 }) }, { name: "extra_field_1", content: "this should be ignored" }, { name: "another_extra", content: "also ignored" }, { name: "metadata", content: JSON.stringify({ foo: "bar" }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/resize", headers: { "content-type": contentType, authorization: `Bearer ${adminToken}` }, body, }); // Extra text fields should be ignored; the valid file+settings should process expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.jobId).toBeDefined(); }); it("ignores duplicate settings fields (first one wins)", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png" }, { name: "settings", content: JSON.stringify({ width: 50 }) }, { name: "settings", content: JSON.stringify({ width: 150 }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/resize", headers: { "content-type": contentType, authorization: `Bearer ${adminToken}` }, body, }); // Should succeed with one of the settings (implementation-defined which) expect(res.statusCode).toBe(200); }); it("rejects extra file-like field with non-standard name (treated as second file)", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png" }, { name: "settings", content: JSON.stringify({ width: 100 }) }, { name: "attachment", filename: "extra.txt", content: Buffer.from("text content"), contentType: "text/plain", }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/resize", headers: { "content-type": contentType, authorization: `Bearer ${adminToken}` }, body, }); // The tool factory counts any file-like part (with filename) as a file upload. // Two file parts trigger the "one image at a time" rejection. expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toMatch(/too many files/i); }); }); // ########################################################################### // 10. CORRUPTED FILE HEADERS -- ADDITIONAL SCENARIOS // Fill remaining gaps: valid magic bytes + truncated/corrupted data for // GIF and WebP formats, random binary with correct extension. // ########################################################################### describe("Corrupted file data -- additional formats", () => { it("handles GIF magic bytes (GIF89a) followed by garbage", async () => { const corruptedGif = Buffer.concat([ Buffer.from("GIF89a"), Buffer.from(Array.from({ length: 100 }, () => Math.floor(Math.random() * 256))), ]); const res = await postTool("resize", [ { name: "file", filename: "corrupt.gif", content: corruptedGif, contentType: "image/gif", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); // Should fail gracefully expect([400, 422]).toContain(res.statusCode); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("handles TIFF magic bytes followed by garbage (little-endian)", async () => { // TIFF little-endian: 49 49 2A 00 const corruptedTiff = Buffer.concat([ Buffer.from([0x49, 0x49, 0x2a, 0x00]), Buffer.from(Array.from({ length: 200 }, () => Math.floor(Math.random() * 256))), ]); const res = await postTool("resize", [ { name: "file", filename: "corrupt.tiff", content: corruptedTiff, contentType: "image/tiff", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); expect([400, 422]).toContain(res.statusCode); }); it("handles random binary data with .jpg extension (content sniffing rejects it)", async () => { // Pure random data -- no valid magic bytes at all const randomData = Buffer.from( Array.from({ length: 4096 }, () => Math.floor(Math.random() * 256)), ); const res = await postTool("resize", [ { name: "file", filename: "random.jpg", content: randomData, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 100 }) }, ]); expect([400, 422]).toContain(res.statusCode); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("handles a valid PNG truncated at exactly the IHDR chunk", async () => { // PNG signature (8 bytes) + IHDR length (4 bytes) + "IHDR" (4 bytes) + partial IHDR data // Total: 8 + 4 + 4 + 9 = 25 bytes (IHDR is 13 bytes data, we cut at 9) const truncatedAtIHDR = PNG_200x150.subarray(0, 25); const res = await postTool("resize", [ { name: "file", filename: "truncated-ihdr.png", content: truncatedAtIHDR, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); // Has valid PNG signature but incomplete IHDR -- Sharp may reject expect([200, 400, 422]).toContain(res.statusCode); }); it("handles a file that is all 0xFF bytes with .png extension", async () => { const allOnes = Buffer.alloc(2048, 0xff); const res = await postTool("compress", [ { name: "file", filename: "all-ff.png", content: allOnes, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); // 0xFF 0xFF... looks like start of JPEG (0xFF 0xD8...) but is actually garbage expect([400, 422]).toContain(res.statusCode); }); }); // ########################################################################### // 11. UNICODE FILENAMES -- REMAINING UNTESTED PATTERNS // Covers RTL Arabic filename, @#$% special chars, picture frame emoji. // ########################################################################### describe("Unicode filenames -- final coverage", () => { it("handles RTL-only filename: صورة.jpg (Arabic for 'image')", async () => { const res = await postTool("compress", [ { name: "file", filename: "صورة.jpg", content: JPG_100x100, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ quality: 70 }) }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.downloadUrl).toBeDefined(); }); it("handles image@#$%.jpg filename with special characters", async () => { const res = await postTool("resize", [ { name: "file", filename: "image@#$%.jpg", content: JPG_100x100, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); // Should either succeed or be sanitized expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.downloadUrl).toBeDefined(); }); it("handles picture frame emoji filename: \u{1F5BC}\u{FE0F}_test.jpg", async () => { const res = await postTool("rotate", [ { name: "file", filename: "\u{1F5BC}\u{FE0F}_test.jpg", content: JPG_100x100, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ angle: 90 }) }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.downloadUrl).toBeDefined(); }); it("handles filename with mixed scripts: テスト画像.jpg (Japanese for 'test image')", async () => { const res = await postTool("compress", [ { name: "file", filename: "テスト画像.jpg", content: JPG_100x100, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ quality: 60 }) }, ]); expect(res.statusCode).toBe(200); }); it("handles 'my test image.jpg' with spaces through compress", async () => { const res = await postTool("compress", [ { name: "file", filename: "my test image.jpg", content: JPG_100x100, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); expect(res.statusCode).toBe(200); }); it("handles filename with 300+ chars through compress (beyond 255 limit)", async () => { const longName = `${"z".repeat(300)}.png`; const res = await postTool("compress", [ { name: "file", filename: longName, content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); // sanitizeFilename truncates but does not crash expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.downloadUrl).toBeDefined(); }); }); // ########################################################################### // 12. 1x1 PIXEL IMAGE THROUGH REMAINING UNTESTED TOOLS // ########################################################################### describe("1x1 pixel image through remaining tools", () => { it("applies adjust-colors to 1x1 image", async () => { const res = await postTool("adjust-colors", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ brightness: 50, contrast: 50 }), }, ]); expect(res.statusCode).toBe(200); }); it("strips metadata from 1x1 image (no-op)", async () => { const res = await postTool("strip-metadata", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ stripAll: true }) }, ]); expect(res.statusCode).toBe(200); }); it("generates favicon from 1x1 image (upscales)", async () => { const res = await postTool("favicon", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({}) }, ]); expect(res.statusCode).toBe(200); }); it("extracts color palette from 1x1 image (single color)", async () => { const res = await postTool("color-palette", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, ]); expect(res.statusCode).toBe(200); }); it("converts 1x1 PNG to WebP", async () => { const res = await postTool("convert", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ format: "webp" }) }, ]); expect([200, 400]).toContain(res.statusCode); }); it("converts 1x1 PNG to AVIF", async () => { const res = await postTool("convert", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ format: "avif" }) }, ]); expect([200, 400]).toContain(res.statusCode); }); }); // ########################################################################### // 13. SETTINGS WITH TYPE MISMATCHES AND EDGE VALUES // ########################################################################### describe("Settings with type mismatches and edge values", () => { it("rejects resize with width as array", async () => { const res = await postTool("resize", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: [100] }) }, ]); expect(res.statusCode).toBe(400); }); it("rejects resize with width as object", async () => { const res = await postTool("resize", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: { value: 100 } }) }, ]); expect(res.statusCode).toBe(400); }); it("rejects rotate with angle as boolean", async () => { const res = await postTool("rotate", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: false }) }, ]); expect(res.statusCode).toBe(400); }); it("rejects compress quality as empty string", async () => { const res = await postTool("compress", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: '{"quality": ""}' }, ]); expect(res.statusCode).toBe(400); }); it("rejects convert with truly unsupported format", async () => { const res = await postTool("convert", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ format: "hevc" }) }, ]); // "hevc" is not in the z.enum list of supported formats expect(res.statusCode).toBe(400); }); it("handles settings with unicode key names", async () => { const res = await postTool("resize", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 100, 幅: 200 }), }, ]); // Zod strips unknown keys; the recognized "width: 100" should process expect(res.statusCode).toBe(200); }); }); // ########################################################################### // 14. CONCURRENT BATCH + SINGLE REQUEST SIMULTANEOUSLY // ########################################################################### describe("Concurrent batch + single request -- extended scenarios", () => { it("runs batch compress + single resize + single rotate simultaneously", async () => { const batchPayload = createMultipartPayload([ { name: "file", filename: "b1.png", content: PNG_200x150, contentType: "image/png" }, { name: "file", filename: "b2.jpg", content: JPG_100x100, contentType: "image/jpeg" }, { name: "file", filename: "b3.webp", content: WEBP_50x50, contentType: "image/webp" }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); const [batchRes, resizeRes, rotateRes] = await Promise.all([ app.inject({ method: "POST", url: "/api/v1/tools/compress/batch", headers: { "content-type": batchPayload.contentType, authorization: `Bearer ${adminToken}`, }, body: batchPayload.body, }), app.inject(buildToolRequest("resize", PNG_200x150, "single-resize.png", { width: 60 })), app.inject( buildToolRequest("rotate", JPG_100x100, "single-rotate.jpg", { angle: 270 }, "image/jpeg"), ), ]); // All must succeed expect(batchRes.statusCode).toBe(200); expect(batchRes.headers["content-type"]).toBe("application/zip"); expect(resizeRes.statusCode).toBe(200); const resizeBody = JSON.parse(resizeRes.body); expect(resizeBody.downloadUrl).toContain("resize"); expect(rotateRes.statusCode).toBe(200); const rotateBody = JSON.parse(rotateRes.body); expect(rotateBody.downloadUrl).toContain("rotate"); // Job IDs must be distinct expect(resizeBody.jobId).not.toBe(rotateBody.jobId); }, 60_000); }); // ########################################################################### // 15. SERVER STABILITY AFTER COMPREHENSIVE COVERAGE GAP TESTS // ########################################################################### describe("Server stability -- health check after all coverage gap tests", () => { it("server remains responsive and healthy", async () => { const healthRes = await app.inject({ method: "GET", url: "/api/v1/health", }); expect(healthRes.statusCode).toBe(200); const json = JSON.parse(healthRes.body); expect(json.status).toBe("healthy"); // Verify a normal tool request still works const normalRes = await postTool("resize", [ { name: "file", filename: "final-sanity.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 100 }) }, ]); expect(normalRes.statusCode).toBe(200); expect(JSON.parse(normalRes.body).jobId).toBeDefined(); }); });