/** * Final adversarial gap-closing integration tests for the SnapOtter image API. * * Targets 10 specific gap categories identified after auditing all existing * adversarial, edge-case, concurrent, and security test files: * * 1. Concurrent requests: 10 simultaneous uploads with pixel-level output verification * 2. Memory pressure: sequential processing of 6.7MB stress-large.jpg with timing checks * 3. Zero-byte files: Buffer.alloc(0) through untested tools (beautify, optimize-for-web, etc.) * 4. Corrupted magic bytes: PNG header on JPEG data, JPEG header on PNG data cross-injection * 5. Wrong extension: .jpg containing PNG data verified through download + Sharp metadata * 6. Unicode filenames: combining marks, BOM, supplementary plane, mixed-direction * 7. Extreme dimensions: 1x1 pixel through untested tools (beautify, replace-color, split, etc.) * 8. Pipeline edge cases: 50-step deep chain, non-existent tool mid-chain, all-same-tool pipeline * 9. Batch limits: 50+ images, batch with only corrupted files, batch with large + tiny mix * 10. Simultaneous batch + single + pipeline requests with cross-contamination checks */ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { apiToolPath } from "@snapotter/shared"; import sharp from "sharp"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { fixtures, readFixture } from "../../fixtures/index.js"; import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp, } from "../test-server.js"; // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const PNG_200x150 = readFixture(fixtures.image.base.png200); const PNG_1x1 = readFixture(fixtures.image.edge.px1); const JPG_100x100 = readFixture(fixtures.image.base.jpg100); const WEBP_50x50 = readFixture(fixtures.image.base.webp50); const STRESS_LARGE = readFixture(fixtures.image.stressLarge); // --------------------------------------------------------------------------- // 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: apiToolPath(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: `${apiToolPath(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 }>; }, imgContentType = "image/png", ) { const { body, contentType } = createMultipartPayload([ { name: "file", filename, content: image, contentType: imgContentType }, { 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: apiToolPath(toolId), headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, body, }; } // ########################################################################### // 1. CONCURRENT REQUESTS: 10 SIMULTANEOUS TO SAME TOOL WITH PIXEL VERIFICATION // Prior tests verify job IDs and URLs are unique but only spot-check pixel // dimensions on a few. This verifies ALL 10 downloads have correct pixels. // ########################################################################### describe("Concurrent: 10 simultaneous uploads to compress with output integrity", () => { it("10 concurrent compress requests with different quality levels produce distinct file sizes", async () => { const qualities = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; // Use JPEG input so quality settings produce meaningful size differences // (PNG is lossless, so quality has no effect on output size) const results = await Promise.all( qualities.map((quality, i) => app.inject( buildToolRequest( "compress", JPG_100x100, `compress-q${quality}-${i}.jpg`, { quality }, "image/jpeg", ), ), ), ); // All 10 must succeed for (const res of results) { expect(res.statusCode).toBe(200); } // Unique job IDs const jobIds = results.map((r) => JSON.parse(r.body).jobId); expect(new Set(jobIds).size).toBe(10); // Download ALL 10 outputs and verify they are valid images const downloadedSizes: number[] = []; for (const res of results) { const downloadUrl = JSON.parse(res.body).downloadUrl; const dlRes = await app.inject({ method: "GET", url: downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.width).toBeGreaterThan(0); expect(meta.height).toBeGreaterThan(0); downloadedSizes.push(dlRes.rawPayload.length); } // Lower quality JPEG outputs should be smaller than higher quality. // q=10 must produce a smaller file than q=100. expect(downloadedSizes[0]).toBeLessThanOrEqual(downloadedSizes[9]); }, 120_000); it("10 concurrent rotate requests with different angles produce valid rotated outputs", async () => { const angles = [0, 36, 72, 108, 144, 180, 216, 252, 288, 324]; const results = await Promise.all( angles.map((angle, i) => app.inject(buildToolRequest("rotate", PNG_200x150, `rotate-${angle}-${i}.png`, { angle })), ), ); for (const res of results) { expect(res.statusCode).toBe(200); } // All job IDs unique const jobIds = results.map((r) => JSON.parse(r.body).jobId); expect(new Set(jobIds).size).toBe(10); // Verify 180-degree rotation: width/height should stay the same const dl180 = await app.inject({ method: "GET", url: JSON.parse(results[5].body).downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); const meta180 = await sharp(dl180.rawPayload).metadata(); expect(meta180.width).toBe(200); expect(meta180.height).toBe(150); }, 120_000); }); // ########################################################################### // 2. MEMORY PRESSURE: SEQUENTIAL STRESS-LARGE.JPG WITH TIMING DEGRADATION // Coverage-gaps has 50 sequential uploads. This tests 10 with strict timing // ratio enforcement and verifies output correctness of each. // ########################################################################### describe("Memory pressure: 10 sequential stress-large.jpg with strict timing checks", () => { it("processes stress-large.jpg through compress 10 times with bounded response time growth", async () => { const responseTimes: number[] = []; const outputSizes: number[] = []; for (let i = 0; i < 10; i++) { const start = Date.now(); const res = await postTool("compress", [ { name: "file", filename: `stress-compress-${i}.jpg`, content: STRESS_LARGE, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); const elapsed = Date.now() - start; responseTimes.push(elapsed); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.processedSize).toBeLessThan(json.originalSize); outputSizes.push(json.processedSize); } // All output sizes should be identical (same input + same settings = deterministic) const uniqueSizes = new Set(outputSizes); expect(uniqueSizes.size).toBe(1); // Timing: last 3 requests should not take more than 3x the average of the first 3 const first3Avg = responseTimes.slice(0, 3).reduce((a, b) => a + b, 0) / 3; const last3Avg = responseTimes.slice(7, 10).reduce((a, b) => a + b, 0) / 3; expect(last3Avg).toBeLessThan(first3Avg * 3); }, 300_000); it("processes stress-large.jpg through border without crash", async () => { const res = await postTool("border", [ { name: "file", filename: "stress-border.jpg", content: STRESS_LARGE, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ borderWidth: 20 }) }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); // Adding a border makes the image larger expect(json.processedSize).toBeGreaterThan(0); }, 60_000); }); // ########################################################################### // 3. ZERO-BYTE FILES: THROUGH UNTESTED TOOLS // Prior tests cover resize, compress, convert, rotate, crop, border, // strip-metadata, text-overlay, color-palette, favicon. These hit the rest. // ########################################################################### describe("Zero-byte files through previously untested tools", () => { it("rejects zero-byte file to beautify with 400", async () => { const res = await postTool("beautify", [ { 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(); }); it("rejects zero-byte file to optimize-for-web with 400", async () => { const res = await postTool("optimize-for-web", [ { name: "file", filename: "empty.jpg", content: Buffer.alloc(0), contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({}) }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects zero-byte file to sharpening with 400", async () => { const res = await postTool("sharpening", [ { name: "file", filename: "empty.png", content: Buffer.alloc(0), contentType: "image/png", }, { name: "settings", content: JSON.stringify({ method: "adaptive" }) }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects zero-byte file to rotate with 400", async () => { const res = await postTool("rotate", [ { name: "file", filename: "empty.webp", content: Buffer.alloc(0), contentType: "image/webp", }, { name: "settings", content: JSON.stringify({ angle: 90 }) }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects zero-byte file to info with 400", async () => { const res = await postTool("info", [ { name: "file", filename: "empty.png", content: Buffer.alloc(0), contentType: "image/png", }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("handles zero-byte file to image-to-base64 gracefully", async () => { // image-to-base64 converts the raw buffer to base64 without Sharp decoding, // so it may accept an empty file (returning empty base64) rather than 400. const res = await postTool("image-to-base64", [ { name: "file", filename: "empty.png", content: Buffer.alloc(0), contentType: "image/png", }, { name: "settings", content: JSON.stringify({}) }, ]); // Either rejected (400) or accepted with empty result (200) expect([200, 400]).toContain(res.statusCode); }); }); // ########################################################################### // 4. CORRUPTED MAGIC BYTES: CROSS-FORMAT HEADER INJECTION // Existing tests cover JPEG-magic+garbage and PNG-magic+garbage. These test // the specific cross-injection: PNG header bytes on real JPEG data and // JPEG header bytes on real PNG data. // ########################################################################### describe("Corrupted magic bytes: cross-format header injection", () => { it("handles PNG magic bytes prepended to real JPEG data gracefully", async () => { // PNG signature (8 bytes) prepended to actual JPEG data const pngHeaderOnJpeg = Buffer.concat([ Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), JPG_100x100, ]); const res = await postTool("resize", [ { name: "file", filename: "png-header-jpeg-data.png", content: pngHeaderOnJpeg, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); // Sharp may read the PNG header, fail to parse IHDR, and reject. // Or it may skip to the JPEG data. Either way, must not crash. expect([200, 400, 422]).toContain(res.statusCode); }); it("handles JPEG SOI marker prepended to real PNG data gracefully", async () => { // JPEG start-of-image (FF D8 FF E0) prepended to actual PNG data const jpegHeaderOnPng = Buffer.concat([Buffer.from([0xff, 0xd8, 0xff, 0xe0]), PNG_200x150]); const res = await postTool("resize", [ { name: "file", filename: "jpeg-header-png-data.jpg", content: jpegHeaderOnPng, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 100 }) }, ]); // Sharp should try to decode as JPEG, fail, and return error gracefully expect([200, 400, 422]).toContain(res.statusCode); }); it("handles WebP RIFF header prepended to real JPEG data gracefully", async () => { // WebP starts with "RIFF" + size + "WEBP" const webpHeaderOnJpeg = Buffer.concat([ Buffer.from("RIFF"), Buffer.from([0x00, 0x00, 0x00, 0x00]), Buffer.from("WEBP"), JPG_100x100, ]); const res = await postTool("compress", [ { name: "file", filename: "webp-header-jpeg-data.webp", content: webpHeaderOnJpeg, contentType: "image/webp", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); expect([200, 400, 422]).toContain(res.statusCode); }); it("handles GIF magic followed by real PNG data gracefully", async () => { // GIF89a header on PNG data const gifHeaderOnPng = Buffer.concat([Buffer.from("GIF89a"), PNG_200x150]); const res = await postTool("resize", [ { name: "file", filename: "gif-header-png-data.gif", content: gifHeaderOnPng, contentType: "image/gif", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); expect([200, 400, 422]).toContain(res.statusCode); }); it("handles truncated PNG signature (only 4 of 8 bytes) with real image after", async () => { // Partial PNG signature + real JPEG const partialPngOnJpeg = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47]), JPG_100x100]); const res = await postTool("compress", [ { name: "file", filename: "partial-png-jpeg.png", content: partialPngOnJpeg, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 70 }) }, ]); expect([200, 400, 422]).toContain(res.statusCode); }); }); // ########################################################################### // 5. WRONG EXTENSION: .jpg CONTAINING PNG DATA WITH DOWNLOAD VERIFICATION // Prior tests check status codes but never download + verify format metadata. // ########################################################################### describe("Wrong extension: format detection verified through download", () => { it("processes .jpg file containing PNG data and output has correct dimensions", async () => { const res = await postTool("resize", [ { name: "file", filename: "actually-png.jpg", content: PNG_200x150, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 75 }) }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); // Download and verify const dlRes = await app.inject({ method: "GET", url: json.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.width).toBe(75); }); it("processes .png file containing JPEG data and output has correct dimensions", async () => { const res = await postTool("resize", [ { name: "file", filename: "actually-jpeg.png", content: JPG_100x100, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 40 }) }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); const dlRes = await app.inject({ method: "GET", url: json.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.width).toBe(40); }); it("processes .gif file containing WebP data through compress", async () => { const res = await postTool("compress", [ { name: "file", filename: "actually-webp.gif", content: WEBP_50x50, contentType: "image/gif", }, { name: "settings", content: JSON.stringify({ quality: 60 }) }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); const dlRes = await app.inject({ method: "GET", url: json.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.width).toBe(50); expect(meta.height).toBe(50); }); }); // ########################################################################### // 6. UNICODE FILENAMES: PREVIOUSLY UNTESTED UNICODE PATTERNS // Prior tests cover: emoji, CJK, RTL, path traversal, null bytes, ZWJ, // spaces, special chars, long names. These test remaining patterns. // ########################################################################### describe("Unicode filenames: remaining untested patterns", () => { it("handles filename with Unicode BOM prefix: \\uFEFFphoto.png", async () => { const res = await postTool("resize", [ { name: "file", filename: "photo.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 80 }) }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.downloadUrl).toBeDefined(); }); it("handles filename with combining diacritical marks: café.png (e + acute)", async () => { const res = await postTool("compress", [ { name: "file", filename: "café.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 80 }) }, ]); expect(res.statusCode).toBe(200); }); it("handles filename with supplementary plane characters: \u{1D11E}.png (musical symbol)", async () => { const res = await postTool("resize", [ { name: "file", filename: "\u{1D11E}.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 100 }) }, ]); expect(res.statusCode).toBe(200); }); it("handles filename with mixed RTL and LTR with numbers: img_123_ملف.png", async () => { const res = await postTool("rotate", [ { name: "file", filename: "img_123_ملف.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: 90 }) }, ]); expect(res.statusCode).toBe(200); }); it("handles filename with Thai script: ภาพ.png", async () => { const res = await postTool("compress", [ { name: "file", filename: "ภาพ.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 70 }) }, ]); expect(res.statusCode).toBe(200); }); it("handles filename with Cyrillic script: фото.png (foto)", async () => { const res = await postTool("resize", [ { name: "file", filename: "фото.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); expect(res.statusCode).toBe(200); }); }); // ########################################################################### // 7. EXTREME DIMENSIONS: 1x1 PIXEL THROUGH UNTESTED TOOLS // Prior tests cover: resize, crop, rotate, compress, convert, border, // strip-metadata, adjust-colors, favicon, color-palette, text-overlay, // watermark-text, sharpening, image-to-base64. These hit the remaining ones. // ########################################################################### describe("1x1 pixel image through previously untested tools", () => { it("beautifies a 1x1 pixel image without crash", async () => { const res = await postTool("beautify", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({}) }, ]); expect([200, 422]).toContain(res.statusCode); }); it("optimizes a 1x1 pixel image for web without crash", async () => { const res = await postTool("optimize-for-web", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({}) }, ]); expect([200, 422]).toContain(res.statusCode); }); it("replaces color in a 1x1 pixel image without crash", async () => { const res = await postTool("replace-color", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ targetColor: "#000000", replacementColor: "#FF0000", tolerance: 50, }), }, ]); // May succeed or reject due to tiny image expect([200, 400, 422]).toContain(res.statusCode); }); it("gets info for a 1x1 pixel image", async () => { const res = await postTool("info", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.width).toBe(1); expect(json.height).toBe(1); }); it("converts 1x1 pixel PNG to JPEG without crash", async () => { const res = await postTool("convert", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ format: "jpg" }) }, ]); expect([200, 400]).toContain(res.statusCode); }); it("adds text overlay to 1x1 pixel image without crash", async () => { const res = await postTool("text-overlay", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ text: "X", fontSize: 10 }), }, ]); expect([200, 422]).toContain(res.statusCode); }); it("processes 1x1 pixel through pipeline of 3 steps without crash", async () => { const res = await executePipeline(PNG_1x1, "tiny-pipeline.png", { steps: [ { toolId: "resize", settings: { width: 100, height: 100 } }, { toolId: "compress", settings: { quality: 80 } }, { toolId: "border", settings: { borderWidth: 5 } }, ], }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(3); }); }); // ########################################################################### // 8. PIPELINE EDGE CASES: DEEP CHAINS, MID-CHAIN FAILURES, ALL-SAME // Prior tests cover up to 25 steps, unknown tools, and format conversions. // These test 50-step chains, failure recovery, and degenerate pipelines. // ########################################################################### describe("Pipeline edge cases: deep chains and degenerate cases", () => { it("handles pipeline with 50 compress steps (deep chain)", async () => { const steps = Array.from({ length: 50 }, () => ({ toolId: "compress", settings: { quality: 99 }, })); const res = await executePipeline(PNG_200x150, "deep-50.png", { steps }); // Should succeed (no MAX_PIPELINE_STEPS in test env) or be rejected expect([200, 400]).toContain(res.statusCode); if (res.statusCode === 200) { const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(50); } }, 120_000); it("pipeline fails at step 3 (unknown tool) after 2 valid steps", async () => { const res = await executePipeline(PNG_200x150, "mid-fail.png", { steps: [ { toolId: "resize", settings: { width: 100 } }, { toolId: "compress", settings: { quality: 80 } }, { toolId: "nonexistent-tool-xyz" }, ], }); // Should fail with clear error about the unknown tool expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("pipeline with all border steps (10 consecutive borders)", async () => { const steps = Array.from({ length: 10 }, () => ({ toolId: "border", settings: { borderWidth: 2 }, })); const res = await executePipeline(PNG_200x150, "all-borders.png", { steps }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(10); // The processed image has more pixels due to borders, but the PNG // compression may produce a smaller file. Just verify it produces output. expect(json.processedSize).toBeGreaterThan(0); }, 60_000); it("pipeline with alternating format conversions: PNG -> WebP -> PNG -> WebP", async () => { const res = await executePipeline(PNG_200x150, "format-flip.png", { steps: [ { toolId: "convert", settings: { format: "webp" } }, { toolId: "convert", settings: { format: "png" } }, { toolId: "convert", settings: { format: "webp" } }, { toolId: "convert", settings: { format: "png" } }, ], }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(4); }); it("pipeline where step 2 expects impossibly small crop after resize", async () => { // Resize to 5x5, then crop 1x1 from offset 10,10 (out of bounds) const res = await executePipeline(PNG_200x150, "impossible-crop.png", { steps: [ { toolId: "resize", settings: { width: 5, height: 5 } }, { toolId: "crop", settings: { left: 10, top: 10, width: 1, height: 1 } }, ], }); // Crop offset exceeds 5x5 dimensions, should fail at step 2 expect([200, 422]).toContain(res.statusCode); }); it("pipeline with single step that is an empty settings object", async () => { // Compress with empty settings should use defaults const res = await executePipeline(PNG_200x150, "empty-settings.png", { steps: [{ toolId: "compress", settings: {} }], }); // Should either succeed with default quality or fail gracefully expect([200, 400, 422]).toContain(res.statusCode); }); }); // ########################################################################### // 9. BATCH LIMITS: LARGE COUNTS AND EDGE CASES // Prior tests cover 11, 15, 20, 50 images. These test specific edge cases // not previously covered. // ########################################################################### describe("Batch limits: additional edge cases", () => { it("batch with only corrupted files returns 422 with all errors", async () => { const garbage1 = Buffer.from( Array.from({ length: 512 }, () => Math.floor(Math.random() * 256)), ); const garbage2 = Buffer.from( Array.from({ length: 256 }, () => Math.floor(Math.random() * 256)), ); const garbage3 = Buffer.from("definitely not an image file at all"); const res = await postBatch("resize", [ { name: "file", filename: "bad1.jpg", contentType: "image/jpeg", content: garbage1 }, { name: "file", filename: "bad2.png", contentType: "image/png", content: garbage2 }, { name: "file", filename: "bad3.webp", contentType: "image/webp", content: garbage3 }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); // All files failed -- should return 422 expect(res.statusCode).toBe(422); const json = JSON.parse(res.body); expect(json.error).toMatch(/all files failed/i); expect(json.errors).toBeDefined(); expect(json.errors.length).toBe(3); }); it("batch with mix of 1x1 pixel + large stress file processes both", async () => { const res = await postBatch("resize", [ { name: "file", filename: "tiny.png", contentType: "image/png", content: PNG_1x1, }, { name: "file", filename: "huge.jpg", contentType: "image/jpeg", content: STRESS_LARGE, }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); expect(res.statusCode).toBe(200); expect(res.headers["content-type"]).toBe("application/zip"); const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string)); expect(Object.keys(fileResults).length).toBe(2); }, 120_000); it("batch with single valid file among 2 zero-byte files", async () => { const res = await postBatch("compress", [ { name: "file", filename: "empty1.png", contentType: "image/png", content: Buffer.alloc(0), }, { name: "file", filename: "valid.png", contentType: "image/png", content: PNG_200x150, }, { name: "file", filename: "empty2.jpg", contentType: "image/jpeg", content: Buffer.alloc(0), }, { name: "settings", content: JSON.stringify({ quality: 70 }) }, ]); // Zero-byte files are skipped during parsing, only 1 valid file remains expect(res.statusCode).toBe(200); expect(res.headers["content-type"]).toBe("application/zip"); }); it("batch through convert tool with mixed formats", async () => { const res = await postBatch("convert", [ { name: "file", filename: "photo.png", contentType: "image/png", content: PNG_200x150, }, { name: "file", filename: "photo.jpg", contentType: "image/jpeg", content: JPG_100x100, }, { name: "file", filename: "photo.webp", contentType: "image/webp", content: WEBP_50x50, }, { name: "settings", content: JSON.stringify({ format: "webp" }) }, ]); expect(res.statusCode).toBe(200); expect(res.headers["content-type"]).toBe("application/zip"); const fileResults = JSON.parse(decodeURIComponent(res.headers["x-file-results"] as string)); expect(Object.keys(fileResults).length).toBe(3); }); }); // ########################################################################### // 10. SIMULTANEOUS BATCH + SINGLE + PIPELINE WITH CROSS-CONTAMINATION CHECKS // Prior tests run batch+single or batch+single+pipeline but never verify // the actual output content across all three. This test downloads and // verifies pixel dimensions. // ########################################################################### describe("Simultaneous batch + single + pipeline with output verification", () => { it("runs all three concurrently and verifies each output independently", async () => { // Single: resize to 60px const singleReq = app.inject( buildToolRequest("resize", PNG_200x150, "single-verify.png", { width: 60 }), ); // Batch: compress 2 images at quality 40 const batchPayload = createMultipartPayload([ { name: "file", filename: "batch-v1.png", content: PNG_200x150, contentType: "image/png" }, { name: "file", filename: "batch-v2.jpg", content: JPG_100x100, contentType: "image/jpeg" }, { name: "settings", content: JSON.stringify({ quality: 40 }) }, ]); const batchReq = app.inject({ method: "POST", url: "/api/v1/tools/image/compress/batch", headers: { "content-type": batchPayload.contentType, authorization: `Bearer ${adminToken}`, }, body: batchPayload.body, }); // Pipeline: resize 80px + rotate 90deg const pipelinePayload = createMultipartPayload([ { name: "file", filename: "pipe-verify.png", content: PNG_200x150, contentType: "image/png" }, { name: "pipeline", content: JSON.stringify({ steps: [ { toolId: "resize", settings: { width: 80 } }, { toolId: "rotate", settings: { angle: 90 } }, ], }), }, ]); const pipelineReq = app.inject({ method: "POST", url: "/api/v1/pipeline/execute", headers: { "content-type": pipelinePayload.contentType, authorization: `Bearer ${adminToken}`, }, body: pipelinePayload.body, }); const [singleRes, batchRes, pipelineRes] = await Promise.all([ singleReq, batchReq, pipelineReq, ]); // All three must succeed expect(singleRes.statusCode).toBe(200); expect(batchRes.statusCode).toBe(200); expect(pipelineRes.statusCode).toBe(200); // Verify single: download and check width = 60 const singleBody = JSON.parse(singleRes.body); const singleDl = await app.inject({ method: "GET", url: singleBody.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(singleDl.statusCode).toBe(200); const singleMeta = await sharp(singleDl.rawPayload).metadata(); expect(singleMeta.width).toBe(60); // Verify batch: returns ZIP with 2 files expect(batchRes.headers["content-type"]).toBe("application/zip"); const fileResults = JSON.parse( decodeURIComponent(batchRes.headers["x-file-results"] as string), ); expect(Object.keys(fileResults).length).toBe(2); // Verify pipeline: download and check rotated dimensions const pipeBody = JSON.parse(pipelineRes.body); expect(pipeBody.stepsCompleted).toBe(2); const pipeDl = await app.inject({ method: "GET", url: pipeBody.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(pipeDl.statusCode).toBe(200); const pipeMeta = await sharp(pipeDl.rawPayload).metadata(); // Resized to 80px width, then rotated 90 degrees swaps dimensions // Height after resize maintains aspect ratio: 80 * (150/200) = 60 // After 90deg rotation: width=60, height=80 expect(pipeMeta.width).toBe(60); expect(pipeMeta.height).toBe(80); // Cross-contamination check: all job IDs must be unique expect(singleBody.jobId).not.toBe(pipeBody.jobId); }, 120_000); it("fires 3 batches + 3 singles + 2 pipelines simultaneously", async () => { const reqs = [ // 3 single requests to different tools app.inject(buildToolRequest("resize", PNG_200x150, "s1.png", { width: 50 })), app.inject(buildToolRequest("compress", PNG_200x150, "s2.png", { quality: 30 })), app.inject(buildToolRequest("rotate", PNG_200x150, "s3.png", { angle: 180 })), ]; // 3 batch requests for (let i = 0; i < 3; i++) { const payload = createMultipartPayload([ { name: "file", filename: `b${i}-1.png`, content: PNG_200x150, contentType: "image/png" }, { name: "file", filename: `b${i}-2.jpg`, content: JPG_100x100, contentType: "image/jpeg" }, { name: "settings", content: JSON.stringify({ width: 30 + i * 10 }) }, ]); reqs.push( app.inject({ method: "POST", url: "/api/v1/tools/image/resize/batch", headers: { "content-type": payload.contentType, authorization: `Bearer ${adminToken}`, }, body: payload.body, }), ); } // 2 pipeline requests for (let i = 0; i < 2; i++) { const payload = createMultipartPayload([ { name: "file", filename: `p${i}.png`, content: PNG_200x150, contentType: "image/png" }, { name: "pipeline", content: JSON.stringify({ steps: [ { toolId: "resize", settings: { width: 60 + i * 20 } }, { toolId: "compress", settings: { quality: 50 + i * 20 } }, ], }), }, ]); reqs.push( app.inject({ method: "POST", url: "/api/v1/pipeline/execute", headers: { "content-type": payload.contentType, authorization: `Bearer ${adminToken}`, }, body: payload.body, }), ); } const results = await Promise.all(reqs); // All 8 requests must succeed for (const res of results) { expect(res.statusCode).toBe(200); } // Single request job IDs must be unique among themselves const singleJobIds = results.slice(0, 3).map((r) => JSON.parse(r.body).jobId); expect(new Set(singleJobIds).size).toBe(3); // Pipeline job IDs must be unique const pipeJobIds = results.slice(6, 8).map((r) => JSON.parse(r.body).jobId); expect(new Set(pipeJobIds).size).toBe(2); // No cross-contamination between single and pipeline job IDs for (const pipeId of pipeJobIds) { expect(singleJobIds).not.toContain(pipeId); } }, 120_000); }); // ########################################################################### // SERVER STABILITY AFTER ALL GAP-CLOSING TESTS // ########################################################################### describe("Server stability after final gap-closing tests", () => { it("server remains healthy after all tests", 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"); // Sanity: a normal 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(); }); });