/** * Adversarial matrix integration tests for the SnapOtter image API. * * Covers gaps not addressed by adversarial.test.ts, adversarial-extended.test.ts, * concurrent.test.ts, or edge-cases.test.ts: * * - Memory pressure: 10 sequential large-file uploads, back-to-back tool chain * - Corrupted headers through non-resize tools (compress, crop, convert, rotate) * - Wrong content-type headers (application/json with binary data) * - Unicode filenames: emoji-only, very long (200+ chars) * - Extreme dimensions: resize to 1x1, crop with 0 dimensions, rotate 1x1 by 45 * - Pipeline: 21+ steps (over limit), same step 10x same settings, up-then-down * - Batch: all identical files, mixed valid/invalid through multiple tools * - Parameter boundaries: quality -1/999, width "abc"/NaN, angle -360/720/999 * - Concurrent: 10 resize with large file, data integrity verification */ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { apiToolPath } from "@snapotter/shared"; 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 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 }>; }, ) { 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: apiToolPath(toolId), headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, body, }; } // ═══════════════════════════════════════════════════════════════════════════ // MEMORY PRESSURE -- 10 SEQUENTIAL LARGE FILE UPLOADS // ═══════════════════════════════════════════════════════════════════════════ describe("Memory pressure -- 10 sequential large-file resize uploads", () => { it("processes stress-large.jpg 10 times sequentially without degradation", async () => { const results: Array<{ statusCode: number; jobId: string }> = []; for (let i = 0; i < 10; i++) { const res = await postTool("resize", [ { name: "file", filename: `stress-seq-${i}.jpg`, content: STRESS_LARGE, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 300 }) }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.jobId).toBeDefined(); results.push({ statusCode: res.statusCode, jobId: json.jobId }); } // All 10 must produce unique job IDs const jobIds = results.map((r) => r.jobId); expect(new Set(jobIds).size).toBe(10); // Verify server is still responsive after 10 large uploads const healthRes = await app.inject({ method: "GET", url: "/api/v1/health", }); expect(healthRes.statusCode).toBe(200); }, 300_000); }); // ═══════════════════════════════════════════════════════════════════════════ // MEMORY PRESSURE -- BACK-TO-BACK TOOL CHAIN WITH LARGE FILE // ═══════════════════════════════════════════════════════════════════════════ describe("Memory pressure -- large file through resize, compress, rotate back-to-back", () => { it("processes stress-large.jpg through resize then compress then rotate sequentially", async () => { const resizeRes = await postTool("resize", [ { name: "file", filename: "chain-resize.jpg", content: STRESS_LARGE, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 500 }) }, ]); expect(resizeRes.statusCode).toBe(200); expect(JSON.parse(resizeRes.body).processedSize).toBeLessThan( JSON.parse(resizeRes.body).originalSize, ); const compressRes = await postTool("compress", [ { name: "file", filename: "chain-compress.jpg", content: STRESS_LARGE, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ quality: 30 }) }, ]); expect(compressRes.statusCode).toBe(200); expect(JSON.parse(compressRes.body).processedSize).toBeLessThan( JSON.parse(compressRes.body).originalSize, ); const rotateRes = await postTool("rotate", [ { name: "file", filename: "chain-rotate.jpg", content: STRESS_LARGE, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ angle: 180 }) }, ]); expect(rotateRes.statusCode).toBe(200); expect(JSON.parse(rotateRes.body).jobId).toBeDefined(); }, 180_000); }); // ═══════════════════════════════════════════════════════════════════════════ // CORRUPTED HEADERS THROUGH NON-RESIZE TOOLS // ═══════════════════════════════════════════════════════════════════════════ describe("Corrupted headers through non-resize tools", () => { it("rejects random binary garbage through compress gracefully", async () => { const garbage = Buffer.from( Array.from({ length: 4096 }, () => Math.floor(Math.random() * 256)), ); const res = await postTool("compress", [ { name: "file", filename: "garbage.jpg", content: garbage, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); expect([400, 422]).toContain(res.statusCode); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); it("rejects random binary garbage through crop gracefully", async () => { const garbage = Buffer.from( Array.from({ length: 2048 }, () => Math.floor(Math.random() * 256)), ); const res = await postTool("crop", [ { name: "file", filename: "garbage.png", content: garbage, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ left: 0, top: 0, width: 10, height: 10 }), }, ]); expect([400, 422]).toContain(res.statusCode); }); it("rejects random binary garbage through convert gracefully", async () => { const garbage = Buffer.from( Array.from({ length: 2048 }, () => Math.floor(Math.random() * 256)), ); const res = await postTool("convert", [ { name: "file", filename: "garbage.png", content: garbage, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ format: "webp" }) }, ]); expect([400, 422]).toContain(res.statusCode); }); it("handles .jpg extension with PNG magic bytes through compress", async () => { // PNG data sent as .jpg -- Sharp should auto-detect and process correctly const res = await postTool("compress", [ { name: "file", filename: "really-a-png.jpg", content: PNG_200x150, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ quality: 60 }) }, ]); // Sharp detects via magic bytes, not extension expect(res.statusCode).toBe(200); }); it("handles .png extension with JPEG magic bytes through crop", async () => { // JPEG data sent as .png -- Sharp should auto-detect and process correctly const res = await postTool("crop", [ { name: "file", filename: "really-a-jpeg.png", content: JPG_100x100, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ left: 0, top: 0, width: 50, height: 50 }), }, ]); expect(res.statusCode).toBe(200); }); it("handles .png extension with JPEG content through rotate", async () => { const res = await postTool("rotate", [ { name: "file", filename: "mislabeled.png", content: JPG_100x100, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: 90 }) }, ]); expect(res.statusCode).toBe(200); }); }); // ═══════════════════════════════════════════════════════════════════════════ // WRONG CONTENT-TYPE HEADER MISMATCHES // ═══════════════════════════════════════════════════════════════════════════ describe("Wrong content-type header -- application/json with binary data", () => { it("processes image when part content-type is application/json", async () => { // The multipart part claims to be JSON but contains a real PNG image const res = await postTool("resize", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "application/json", }, { name: "settings", content: JSON.stringify({ width: 100 }) }, ]); // Sharp detects format from magic bytes, not content-type expect(res.statusCode).toBe(200); }); it("processes image when part content-type is image/png but data is JPEG", async () => { const res = await postTool("resize", [ { name: "file", filename: "test.jpg", content: JPG_100x100, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); // Sharp detects actual JPEG format expect(res.statusCode).toBe(200); }); it("processes image when part content-type is video/mp4 (completely wrong)", async () => { const res = await postTool("compress", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "video/mp4", }, { name: "settings", content: JSON.stringify({ quality: 80 }) }, ]); // Sharp ignores content-type and reads magic bytes expect(res.statusCode).toBe(200); }); }); // ═══════════════════════════════════════════════════════════════════════════ // UNICODE FILENAMES -- ADDITIONAL PATTERNS // ═══════════════════════════════════════════════════════════════════════════ describe("Unicode filenames -- additional adversarial patterns", () => { it("handles emoji-only filename: \u{1F3A8}.jpg", async () => { const res = await postTool("resize", [ { name: "file", filename: "\u{1F3A8}.jpg", content: JPG_100x100, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 50 }) }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.downloadUrl).toBeDefined(); }); it("handles multiple emoji in filename: \u{1F4F7}\u{1F30D}\u{2728}.png", async () => { const res = await postTool("resize", [ { name: "file", filename: "\u{1F4F7}\u{1F30D}\u{2728}.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 80 }) }, ]); expect(res.statusCode).toBe(200); }); it("handles very long filename (250+ chars)", async () => { const longBase = "a".repeat(250); const longName = `${longBase}.png`; const res = await postTool("compress", [ { name: "file", filename: longName, content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 70 }) }, ]); // sanitizeFilename may truncate but should not crash expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.downloadUrl).toBeDefined(); }); it("handles filename with tab and newline characters", async () => { const res = await postTool("resize", [ { name: "file", filename: "tab\there\nnewline.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 80 }) }, ]); // Whitespace should be sanitized or stripped expect([200, 400]).toContain(res.statusCode); }); it("handles filename with unicode zero-width characters", async () => { // Zero-width joiner, zero-width space const res = await postTool("resize", [ { name: "file", filename: "test​‍image.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 80 }) }, ]); expect(res.statusCode).toBe(200); }); }); // ═══════════════════════════════════════════════════════════════════════════ // EXTREME DIMENSIONS -- RESIZE TO 1x1 OUTPUT // ═══════════════════════════════════════════════════════════════════════════ describe("Extreme dimensions -- resize to 1x1 output", () => { it("resizes a 200x150 image down to 1x1", async () => { const res = await postTool("resize", [ { name: "file", filename: "shrink-to-pixel.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 1, height: 1 }), }, ]); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.processedSize).toBeLessThan(json.originalSize); }); it("resizes a JPEG image down to 1x1", async () => { const res = await postTool("resize", [ { name: "file", filename: "shrink-jpg.jpg", content: JPG_100x100, contentType: "image/jpeg", }, { name: "settings", content: JSON.stringify({ width: 1, height: 1 }), }, ]); expect(res.statusCode).toBe(200); }); }); // ═══════════════════════════════════════════════════════════════════════════ // EXTREME DIMENSIONS -- CROP EDGE CASES // ═══════════════════════════════════════════════════════════════════════════ describe("Extreme dimensions -- crop edge cases", () => { it("rejects crop with 0 width", async () => { const res = await postTool("crop", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ left: 0, top: 0, width: 0, height: 10 }), }, ]); // z.number().positive() rejects 0 expect(res.statusCode).toBe(400); }); it("rejects crop with 0 height", async () => { const res = await postTool("crop", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ left: 0, top: 0, width: 10, height: 0 }), }, ]); expect(res.statusCode).toBe(400); }); it("rejects crop with negative width", async () => { const res = await postTool("crop", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ left: 0, top: 0, width: -10, height: 10 }), }, ]); expect(res.statusCode).toBe(400); }); it("rejects crop with negative left offset", async () => { const res = await postTool("crop", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ left: -5, top: 0, width: 10, height: 10 }), }, ]); // z.number().min(0) rejects negative left expect(res.statusCode).toBe(400); }); it("crops a 1x1 region from a normal image", async () => { const res = await postTool("crop", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ left: 50, top: 50, width: 1, height: 1 }), }, ]); expect([200, 422]).toContain(res.statusCode); }); }); // ═══════════════════════════════════════════════════════════════════════════ // EXTREME DIMENSIONS -- ROTATE 1x1 BY NON-ORTHOGONAL ANGLE // ═══════════════════════════════════════════════════════════════════════════ describe("Extreme dimensions -- rotate 1x1 by non-orthogonal angles", () => { it("rotates a 1x1 image by 45 degrees", async () => { const res = await postTool("rotate", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: 45 }) }, ]); // A 45-degree rotation of a 1x1 image produces a slightly larger image // with transparent corners. Must not crash. expect([200, 422]).toContain(res.statusCode); }); it("rotates a 1x1 image by 1 degree", async () => { const res = await postTool("rotate", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: 1 }) }, ]); expect([200, 422]).toContain(res.statusCode); }); it("rotates a 1x1 image by 359 degrees", async () => { const res = await postTool("rotate", [ { name: "file", filename: "tiny.png", content: PNG_1x1, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: 359 }) }, ]); expect([200, 422]).toContain(res.statusCode); }); }); // ═══════════════════════════════════════════════════════════════════════════ // PARAMETER BOUNDARY -- QUALITY VALUES // ═══════════════════════════════════════════════════════════════════════════ describe("Parameter boundary -- compress quality values", () => { it("rejects quality of -1", async () => { const res = await postTool("compress", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: -1 }) }, ]); // z.number().min(1) rejects -1 expect(res.statusCode).toBe(400); }); it("rejects quality of 999", async () => { const res = await postTool("compress", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 999 }) }, ]); // z.number().max(100) rejects 999 expect(res.statusCode).toBe(400); }); it("accepts quality at exact minimum (1)", async () => { const res = await postTool("compress", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 1 }) }, ]); expect(res.statusCode).toBe(200); }); it("accepts quality at midpoint (50)", async () => { const res = await postTool("compress", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); expect(res.statusCode).toBe(200); }); it("accepts quality at 99", async () => { const res = await postTool("compress", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ quality: 99 }) }, ]); expect(res.statusCode).toBe(200); }); }); // ═══════════════════════════════════════════════════════════════════════════ // PARAMETER BOUNDARY -- WIDTH / HEIGHT VALUES // ═══════════════════════════════════════════════════════════════════════════ describe("Parameter boundary -- resize width/height values", () => { it("accepts width of 1 (minimum positive integer)", async () => { const res = await postTool("resize", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 1 }) }, ]); expect(res.statusCode).toBe(200); }); it("rejects width of -1", async () => { const res = await postTool("resize", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: -1 }) }, ]); expect(res.statusCode).toBe(400); }); it("rejects width as string 'abc'", async () => { const res = await postTool("resize", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: '{"width": "abc"}' }, ]); // Zod z.number() rejects string expect(res.statusCode).toBe(400); }); it("rejects height as string 'NaN'", async () => { const res = await postTool("resize", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: '{"height": "NaN"}' }, ]); expect(res.statusCode).toBe(400); }); it("handles width of 99999 without crashing", async () => { const res = await postTool("resize", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ width: 99999 }) }, ]); // May succeed (Sharp allows large dims), fail at processing, or be // rejected by validation. Must not crash. // Note: Sharp will actually attempt to upscale, which can take significant // time on a small CPU. The PROCESSING_TIMEOUT_S config may kill it. expect([200, 400, 422]).toContain(res.statusCode); }, 120_000); }); // ═══════════════════════════════════════════════════════════════════════════ // PARAMETER BOUNDARY -- ROTATION ANGLE VALUES // ═══════════════════════════════════════════════════════════════════════════ describe("Parameter boundary -- rotation angle values", () => { it("handles angle of -360 (full negative rotation)", async () => { const res = await postTool("rotate", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: -360 }) }, ]); // -360 is effectively 0 degrees; Sharp allows negative angles expect([200, 400]).toContain(res.statusCode); }); it("handles angle of -1 (slight counter-clockwise rotation)", async () => { const res = await postTool("rotate", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: -1 }) }, ]); // Sharp supports negative angles expect([200, 400]).toContain(res.statusCode); }); it("handles angle of 360 (full rotation, equivalent to 0)", async () => { const res = await postTool("rotate", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: 360 }) }, ]); expect([200, 400]).toContain(res.statusCode); }); it("handles angle of 720 (double full rotation)", async () => { const res = await postTool("rotate", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: 720 }) }, ]); // Sharp may normalize or process as-is expect([200, 400]).toContain(res.statusCode); }); it("handles angle of 999 without crashing", async () => { const res = await postTool("rotate", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: JSON.stringify({ angle: 999 }) }, ]); // Sharp treats this as 999 mod 360 = 279 degrees effectively expect([200, 400]).toContain(res.statusCode); }); it("rejects angle as string 'abc'", async () => { const res = await postTool("rotate", [ { name: "file", filename: "test.png", content: PNG_200x150, contentType: "image/png", }, { name: "settings", content: '{"angle": "abc"}' }, ]); // z.number() rejects string expect(res.statusCode).toBe(400); }); }); // ═══════════════════════════════════════════════════════════════════════════ // PIPELINE -- 21+ STEPS (OVER LIMIT) // ═══════════════════════════════════════════════════════════════════════════ describe("Pipeline -- exceeding step limits", () => { it("handles pipeline with 20 steps (many repeated resize operations)", async () => { // MAX_PIPELINE_STEPS defaults to 0 (unlimited) in test env, so this should succeed const steps = Array.from({ length: 20 }, () => ({ toolId: "resize", settings: { percentage: 99 }, })); const res = await executePipeline(PNG_200x150, "twenty-steps.png", { steps }); // With no MAX_PIPELINE_STEPS set, this should succeed expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(20); }, 120_000); it("handles pipeline with 25 steps (deep chain)", async () => { // If there is a limit set, this tests it. If unlimited, it should succeed. const steps = Array.from({ length: 25 }, () => ({ toolId: "compress", settings: { quality: 99 }, })); const res = await executePipeline(PNG_200x150, "twentyfive-steps.png", { steps }); // No MAX_PIPELINE_STEPS in test config, so should succeed expect([200, 400]).toContain(res.statusCode); if (res.statusCode === 200) { const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(25); } else { // If rejected, should have a clear error about step limits const json = JSON.parse(res.body); expect(json.error).toMatch(/step|pipeline|max/i); } }, 120_000); }); // ═══════════════════════════════════════════════════════════════════════════ // PIPELINE -- SAME STEP REPEATED WITH IDENTICAL SETTINGS // ═══════════════════════════════════════════════════════════════════════════ describe("Pipeline -- same step repeated 10 times with identical settings", () => { it("handles 10 identical compress steps (quality=80) in a pipeline", async () => { const steps = Array.from({ length: 10 }, () => ({ toolId: "compress", settings: { quality: 80 }, })); const res = await executePipeline(PNG_200x150, "repeat-compress.png", { steps }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(10); }, 60_000); it("handles 10 identical rotate steps (angle=90) -- net result 2.5 full rotations", async () => { const steps = Array.from({ length: 10 }, () => ({ toolId: "rotate", settings: { angle: 90 }, })); const res = await executePipeline(PNG_200x150, "repeat-rotate.png", { steps }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(10); }, 60_000); }); // ═══════════════════════════════════════════════════════════════════════════ // PIPELINE -- CONFLICTING OPERATIONS (RESIZE UP THEN DOWN) // ═══════════════════════════════════════════════════════════════════════════ describe("Pipeline -- conflicting operations", () => { it("handles resize up (500px) then resize down (50px)", async () => { const res = await executePipeline(PNG_200x150, "up-then-down.png", { steps: [ { toolId: "resize", settings: { width: 500 } }, { toolId: "resize", settings: { width: 50 } }, ], }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(2); // Final size should be small since we downsized to 50px expect(json.processedSize).toBeLessThan(json.originalSize); }); it("handles resize down (10px) then resize up (1000px)", async () => { const res = await executePipeline(PNG_200x150, "down-then-up.png", { steps: [ { toolId: "resize", settings: { width: 10 } }, { toolId: "resize", settings: { width: 1000 } }, ], }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(2); }); it("handles convert to webp, then convert to jpeg (format ping-pong)", async () => { const res = await executePipeline(PNG_200x150, "format-pingpong.png", { steps: [ { toolId: "convert", settings: { format: "webp" } }, { toolId: "convert", settings: { format: "jpg" } }, ], }); expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.stepsCompleted).toBe(2); }); }); // ═══════════════════════════════════════════════════════════════════════════ // BATCH -- ALL IDENTICAL FILES // ═══════════════════════════════════════════════════════════════════════════ describe("Batch -- all identical files", () => { it("handles batch with 5 identical PNG files", async () => { const res = await postBatch("resize", [ { name: "file", filename: "same-1.png", contentType: "image/png", content: PNG_200x150 }, { name: "file", filename: "same-2.png", contentType: "image/png", content: PNG_200x150 }, { name: "file", filename: "same-3.png", contentType: "image/png", content: PNG_200x150 }, { name: "file", filename: "same-4.png", contentType: "image/png", content: PNG_200x150 }, { name: "file", filename: "same-5.png", contentType: "image/png", content: PNG_200x150 }, { 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(5); }); it("handles batch with all identical files and same filename", async () => { // All files have the exact same name -- deduplication should produce unique names in ZIP const res = await postBatch("compress", [ { name: "file", filename: "photo.png", contentType: "image/png", content: PNG_200x150 }, { name: "file", filename: "photo.png", contentType: "image/png", content: PNG_200x150 }, { name: "file", filename: "photo.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ quality: 60 }) }, ]); 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)); const names = Object.values(fileResults); // All three entries should have unique names in the ZIP expect(new Set(names).size).toBe(3); }); }); // ═══════════════════════════════════════════════════════════════════════════ // BATCH -- MIXED VALID/INVALID THROUGH COMPRESS AND ROTATE // ═══════════════════════════════════════════════════════════════════════════ describe("Batch -- mixed valid/invalid through compress and rotate", () => { it("handles batch compress with mix of valid images and garbage", async () => { const garbage = Buffer.from("not a real image at all, just text"); const res = await postBatch("compress", [ { name: "file", filename: "valid.png", contentType: "image/png", content: PNG_200x150 }, { name: "file", filename: "garbage.jpg", contentType: "image/jpeg", content: garbage }, { name: "file", filename: "valid.jpg", contentType: "image/jpeg", content: JPG_100x100 }, { name: "settings", content: JSON.stringify({ quality: 50 }) }, ]); // Batch processes valid files and skips invalid ones expect(res.statusCode).toBe(200); expect(res.headers["content-type"]).toBe("application/zip"); }); it("handles batch rotate with mix of valid images and empty files", async () => { const res = await postBatch("rotate", [ { name: "file", filename: "valid.png", contentType: "image/png", content: PNG_200x150 }, { name: "file", filename: "empty.png", contentType: "image/png", content: Buffer.alloc(0) }, { name: "file", filename: "valid.jpg", contentType: "image/jpeg", content: JPG_100x100 }, { name: "settings", content: JSON.stringify({ angle: 90 }) }, ]); // Empty files are skipped during parsing; remaining valid files succeed expect(res.statusCode).toBe(200); expect(res.headers["content-type"]).toBe("application/zip"); }); }); // ═══════════════════════════════════════════════════════════════════════════ // CONCURRENT -- 10 RESIZE WITH LARGE FILE, DATA INTEGRITY // ═══════════════════════════════════════════════════════════════════════════ describe("Concurrent -- 10 simultaneous resize with large file", () => { it("fires 10 simultaneous resize requests with stress-large.jpg -- all return 200 with valid data", async () => { const results = await Promise.all( Array.from({ length: 10 }, (_, i) => app.inject( buildToolRequest( "resize", STRESS_LARGE, `concurrent-large-${i}.jpg`, { width: 100 + i * 20 }, "image/jpeg", ), ), ), ); for (const res of results) { expect(res.statusCode).toBe(200); const json = JSON.parse(res.body); expect(json.jobId).toBeDefined(); expect(json.downloadUrl).toBeDefined(); // All processed files should be smaller than original since we downsize expect(json.processedSize).toBeLessThan(json.originalSize); } // All 10 must produce unique job IDs (no race condition corruption) const jobIds = results.map((r) => JSON.parse(r.body).jobId); expect(new Set(jobIds).size).toBe(10); // All download URLs must be unique const urls = results.map((r) => JSON.parse(r.body).downloadUrl); expect(new Set(urls).size).toBe(10); }, 300_000); }); // ═══════════════════════════════════════════════════════════════════════════ // CONCURRENT -- RAPID SEQUENTIAL WITH NO DELAY // ═══════════════════════════════════════════════════════════════════════════ describe("Concurrent -- rapid sequential requests with zero delay", () => { it("fires 15 resize requests sequentially with no delay between them", async () => { const results = []; for (let i = 0; i < 15; i++) { const res = await app.inject( buildToolRequest("resize", PNG_200x150, `rapid-fire-${i}.png`, { width: 30 + i * 5, }), ); results.push(res); } for (const res of results) { expect(res.statusCode).toBe(200); } // All should produce unique job IDs const jobIds = results.map((r) => JSON.parse(r.body).jobId); expect(new Set(jobIds).size).toBe(15); }, 60_000); }); // ═══════════════════════════════════════════════════════════════════════════ // CONCURRENT -- PARALLEL BATCH + PIPELINE + SINGLE // ═══════════════════════════════════════════════════════════════════════════ describe("Concurrent -- parallel batch, pipeline, and single requests", () => { it("runs batch, pipeline, and single resize simultaneously without corruption", async () => { // Single request const singleReq = app.inject( buildToolRequest("resize", PNG_200x150, "single.png", { width: 80 }), ); // Batch request 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: "settings", content: JSON.stringify({ width: 40 }) }, ]); const batchReq = app.inject({ method: "POST", url: "/api/v1/tools/image/resize/batch", headers: { "content-type": batchPayload.contentType, authorization: `Bearer ${adminToken}`, }, body: batchPayload.body, }); // Pipeline request const pipelinePayload = createMultipartPayload([ { name: "file", filename: "pipe.png", content: PNG_200x150, contentType: "image/png" }, { name: "pipeline", content: JSON.stringify({ steps: [ { toolId: "resize", settings: { width: 60 } }, { toolId: "compress", settings: { quality: 70 } }, ], }), }, ]); 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, ]); // Single request expect(singleRes.statusCode).toBe(200); const singleBody = JSON.parse(singleRes.body); expect(singleBody.jobId).toBeDefined(); // Batch request expect(batchRes.statusCode).toBe(200); expect(batchRes.headers["content-type"]).toBe("application/zip"); // Pipeline request expect(pipelineRes.statusCode).toBe(200); const pipelineBody = JSON.parse(pipelineRes.body); expect(pipelineBody.stepsCompleted).toBe(2); expect(pipelineBody.jobId).toBeDefined(); // Job IDs across all three must be unique expect(singleBody.jobId).not.toBe(pipelineBody.jobId); }, 120_000); }); // ═══════════════════════════════════════════════════════════════════════════ // EMPTY FILE WITH VALID EXTENSION THROUGH MULTIPLE TOOLS // ═══════════════════════════════════════════════════════════════════════════ describe("Empty file (0 bytes) with valid extension through various tools", () => { it("rejects empty .jpg file through text-overlay with 400", async () => { const res = await postTool("text-overlay", [ { name: "file", filename: "empty.jpg", content: Buffer.alloc(0), contentType: "image/jpeg", }, { 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 empty .webp file through watermark-text with 400", async () => { const res = await postTool("watermark-text", [ { name: "file", filename: "empty.webp", content: Buffer.alloc(0), contentType: "image/webp", }, { name: "settings", content: JSON.stringify({ text: "WM", fontSize: 12, opacity: 50 }), }, ]); expect(res.statusCode).toBe(400); const json = JSON.parse(res.body); expect(json.error).toBeDefined(); }); }); // ═══════════════════════════════════════════════════════════════════════════ // SERVER STABILITY AFTER ADVERSARIAL BARRAGE // ═══════════════════════════════════════════════════════════════════════════ describe("Server stability -- health check after adversarial barrage", () => { it("server remains responsive after processing all preceding tests", async () => { // This test runs last (due to describe ordering) and verifies the server // did not leak memory or enter a degraded state from all the abuse above. 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"); // Also verify a normal tool request still works const normalRes = await postTool("resize", [ { name: "file", filename: "sanity-check.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(); }); });