Files
SnapOtter/tests/integration/adversarial-final-gaps.test.ts
T
SnapOtter 06d1822491 test: expand test coverage across all layers (+1,157 tests)
Fix 2 failing unit tests (landing hero text mismatch) and broken
coverage tooling (brace-expansion v5 override breaking minimatch).
Add ~1,097 new test cases via 14-agent parallel expansion:

- Unit: +290 tests (AI bridge, image-engine, stores, API helpers)
- Integration: +504 tests (all tools, cross-format matrix, adversarial)
- E2E: +363 tests (navigation, tool UI, batch/pipeline, settings,
  visual regression, accessibility, performance, cross-browser)

Total: 4,223 unit + 6,057 integration + 1,563 E2E = 11,843 tests
2026-06-06 19:37:29 +08:00

1192 lines
40 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 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 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<string, unknown> }>;
},
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<string, unknown>,
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 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/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/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();
});
});