Files
SnapOtter/tests/integration/generated/format-matrix-expanded.test.ts
T
SnapOtterandGitHub bc32f86a07 test: correct the SYNC_WAIT_MS comment and guard six 202 branches (#656)
The comment added in #652 was wrong. It said SYNC_WAIT_MS=0 drives every tool through its 202 path; it does the opposite. BullMQ arms its timer under `if (ttl)`, so 0 is falsy, waitUntilFinished waits forever and every route answers 200, matching the repo-wide convention that 0 means unlimited. A small positive value such as 1 is what forces the async path.

That error mattered: the local validation claimed for #652 exercised the synchronous path throughout and never reached settleAsyncFallback. Redone with SYNC_WAIT_MS=1, it surfaced six specs whose status gate accepts 202 but whose else branch then demands an error body a 202 never carries, a latent flake on a slow runner.

Each now settles the job first. Under a forced 1ms window the 'expected undefined to be defined' failures drop from 55 to 0, and all 1183 tests still pass on the normal 30s window. Per-shard totals unchanged at 9903 tests, 9435 passed, 468 skipped.

Specs asserting a bare 200 without listing 202 are deliberately untouched: they own the synchronous contract.
2026-07-27 14:20:29 +08:00

1998 lines
60 KiB
TypeScript

/**
* Expanded cross-format matrix integration tests.
*
* Covers tools NOT tested in format-matrix.test.ts or format-matrix-ai.test.ts:
*
* Single-file tools:
* - color-blindness (createToolRoute factory, standard response)
* - beautify (custom route, download response)
* - edit-metadata (custom route + /inspect endpoint)
* - vectorize (custom route, raster -> SVG)
* - split (custom route, streams ZIP)
* - favicon (custom route, streams ZIP)
* - meme-generator (custom route, download response)
* - barcode-read (custom route, JSON result)
*
* Multi-file tools (tested with pairs of format fixtures):
* - compose (base + overlay)
* - compare (image A + image B)
* - collage (2+ images, template-based)
* - stitch (2+ images, horizontal/vertical join)
*
* SVG-only tool:
* - svg-to-raster (only accepts SVG input)
*
* AI tool with 501 guard:
* - transparency-fixer (returns 501 when AI sidecar is not installed)
*
* For exotic formats (PSD, EXR, HDR, TGA, DNG, ICO, JXL, etc.) the test
* accepts both success (200) and graceful errors (400, 422) since CLI
* decoders may not be installed. For HEIC/HEIF the test accepts 422 when
* libheif is unavailable. No tool should ever return 500 (server crash).
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { fixtureDir, fixtures } from "../../fixtures/index.js";
import { settleAsyncFallback } from "../settle-job.js";
import {
buildTestApp,
createMultipartPayload,
loginAsAdmin,
type TestApp,
} from "../test-server.js";
// ---------------------------------------------------------------------------
// Format sample definitions (subset relevant to the expanded tools)
// ---------------------------------------------------------------------------
interface FormatSample {
name: string;
file: string;
mime: string;
needsCliDecoder: boolean;
needsHeifDecoder: boolean;
mayFailValidation: boolean;
}
const FORMAT_SAMPLES: FormatSample[] = [
{
name: "JPEG",
file: "sample.jpg",
mime: "image/jpeg",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "PNG",
file: "sample.png",
mime: "image/png",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "WebP",
file: "sample.webp",
mime: "image/webp",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "GIF",
file: "sample.gif",
mime: "image/gif",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "AVIF",
file: "sample.avif",
mime: "image/avif",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "TIFF",
file: "sample.tiff",
mime: "image/tiff",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "BMP",
file: "sample.bmp",
mime: "image/bmp",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: true,
},
{
name: "HEIC",
file: "sample.heic",
mime: "image/heic",
needsCliDecoder: false,
needsHeifDecoder: true,
mayFailValidation: false,
},
{
name: "HEIF",
file: "sample.heif",
mime: "image/heif",
needsCliDecoder: false,
needsHeifDecoder: true,
mayFailValidation: false,
},
{
name: "SVG",
file: "sample.svg",
mime: "image/svg+xml",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "ICO",
file: "sample.ico",
mime: "image/x-icon",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "PSD",
file: "sample.psd",
mime: "image/vnd.adobe.photoshop",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "EXR",
file: "sample.exr",
mime: "image/x-exr",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "HDR",
file: "sample.hdr",
mime: "image/vnd.radiance",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "TGA",
file: "sample.tga",
mime: "image/x-tga",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "DNG",
file: "sample.dng",
mime: "image/x-adobe-dng",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "JXL",
file: "sample.jxl",
mime: "image/jxl",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: true,
},
];
/** Core formats: natively readable by Sharp without CLI decoders */
const CORE_FORMATS = FORMAT_SAMPLES.filter(
(f) => !f.needsCliDecoder && !f.needsHeifDecoder && !f.mayFailValidation,
);
/** Exotic formats: need CLI decoders or HEIF support */
const EXOTIC_FORMATS = FORMAT_SAMPLES.filter(
(f) => f.needsCliDecoder || f.needsHeifDecoder || f.mayFailValidation,
);
/** Status codes we accept for formats that may lack decoder support */
const ACCEPTABLE_FALLBACK_CODES = [200, 202, 400, 422];
function needsFallback(fmt: FormatSample): boolean {
return fmt.needsCliDecoder || fmt.needsHeifDecoder || fmt.mayFailValidation;
}
// ---------------------------------------------------------------------------
// 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);
// ---------------------------------------------------------------------------
// 1. Color-blindness simulation x all formats
//
// Uses createToolRoute factory, returns standard download response.
// ---------------------------------------------------------------------------
describe("Color-blindness simulation cross-format", () => {
for (const fmt of FORMAT_SAMPLES) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`processes ${fmt.name} input`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({
simulationType: "deuteranomaly",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/color-blindness",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
if (needsFallback(fmt)) {
expect(ACCEPTABLE_FALLBACK_CODES).toContain(res.statusCode);
} else {
expect(res.statusCode).toBe(200);
}
if (res.statusCode === 200) {
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(typeof body.downloadUrl).toBe("string");
expect(body.processedSize).toBeGreaterThan(0);
expect(body.originalSize).toBeGreaterThan(0);
} else if (res.statusCode >= 400) {
// 202 (accepted, async) has no sync body to verify; only error
// codes carry a JSON error.
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 2. Beautify x all formats
//
// Custom route, accepts main image + optional background image.
// Returns standard {downloadUrl, processedSize} response.
// ---------------------------------------------------------------------------
describe("Beautify cross-format", () => {
for (const fmt of FORMAT_SAMPLES) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`processes ${fmt.name} input`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({
padding: 20,
borderRadius: 8,
backgroundType: "solid",
backgroundColor: "#f0f0f0",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/beautify",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
if (needsFallback(fmt)) {
expect(ACCEPTABLE_FALLBACK_CODES).toContain(res.statusCode);
} else {
expect(res.statusCode).toBe(200);
}
if (res.statusCode === 200) {
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(typeof body.downloadUrl).toBe("string");
expect(body.processedSize).toBeGreaterThan(0);
expect(body.originalSize).toBeGreaterThan(0);
} else if (res.statusCode >= 400) {
// 202 (accepted, async) has no sync body to verify; only error
// codes carry a JSON error.
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 3. Edit-metadata x all formats
//
// Custom route. Also tests the /inspect endpoint.
// Edit-metadata writes EXIF tags in-place using ExifTool.
// ---------------------------------------------------------------------------
describe("Edit-metadata cross-format", () => {
describe("edit endpoint", () => {
for (const fmt of FORMAT_SAMPLES) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`edits metadata in ${fmt.name} input`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({
title: "Test Title",
author: "Test Author",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/edit-metadata",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
if (needsFallback(fmt)) {
expect(ACCEPTABLE_FALLBACK_CODES).toContain(res.statusCode);
} else {
// edit-metadata may also fail with 422 if ExifTool is not installed
expect([200, 202, 400, 422]).toContain(res.statusCode);
}
if (await settleAsyncFallback(res)) return;
if (res.statusCode === 200) {
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(typeof body.downloadUrl).toBe("string");
expect(body.processedSize).toBeGreaterThan(0);
} else {
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
},
perTestTimeout,
);
}
});
describe("inspect endpoint", () => {
for (const fmt of FORMAT_SAMPLES) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`inspects ${fmt.name} metadata`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/edit-metadata/inspect",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
// Inspect uses ExifTool, which may not be installed; accept clean errors
expect(res.statusCode).not.toBe(500);
expect([200, 202, 400, 422]).toContain(res.statusCode);
const body = JSON.parse(res.body);
if (await settleAsyncFallback(res)) return;
if (res.statusCode === 200) {
// inspect returns metadata fields; at minimum it is an object
expect(typeof body).toBe("object");
} else {
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
},
perTestTimeout,
);
}
});
});
// ---------------------------------------------------------------------------
// 4. Vectorize x all formats
//
// Custom route: converts raster image to SVG via potrace/vtracer.
// Returns {downloadUrl, processedSize} with an SVG output.
// ---------------------------------------------------------------------------
describe("Vectorize cross-format", () => {
for (const fmt of FORMAT_SAMPLES) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`vectorizes ${fmt.name} input`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({ colorMode: "bw", threshold: 128 }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/vectorize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
if (needsFallback(fmt)) {
expect(ACCEPTABLE_FALLBACK_CODES).toContain(res.statusCode);
} else {
expect(res.statusCode).toBe(200);
}
if (res.statusCode === 200) {
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(typeof body.downloadUrl).toBe("string");
expect(body.downloadUrl).toContain(".svg");
expect(body.processedSize).toBeGreaterThan(0);
} else {
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 5. Split x core formats
//
// Custom route that streams a ZIP response (reply.hijack).
// We verify the HTTP status is 200 and content-type is application/zip.
// Only tested against core formats since the ZIP streaming response
// prevents JSON error parsing on failure.
// ---------------------------------------------------------------------------
describe("Split cross-format", () => {
for (const fmt of CORE_FORMATS) {
it(`splits ${fmt.name} into grid tiles`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({ columns: 2, rows: 2 }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/split",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
// Split streams a ZIP, so 200 means the archive was successfully created
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toContain("application/zip");
});
}
// Exotic formats: should not crash
for (const fmt of EXOTIC_FORMATS) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`${fmt.name}: no server crash`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({ columns: 2, rows: 2 }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/split",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
// Must not crash. Accept 200 (ZIP streamed) or 422 (clean error)
expect(res.statusCode).not.toBe(500);
expect([200, 202, 400, 422]).toContain(res.statusCode);
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 6. Favicon x core formats
//
// Custom route that streams a ZIP response with favicon variants.
// Only tested against core formats since it hijacks the response.
// ---------------------------------------------------------------------------
describe("Favicon cross-format", () => {
for (const fmt of CORE_FORMATS) {
it(`generates favicons from ${fmt.name} input`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/favicon",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
// Favicon streams a ZIP
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toContain("application/zip");
});
}
// NOTE: Exotic format error resilience is not tested for favicon because
// the route uses reply.hijack() to stream ZIP output. When Sharp fails
// mid-stream on an exotic format, the archive never finalizes and
// app.inject() hangs indefinitely. The error path is still safe (the
// route's try/catch returns 422 before hijack if decoding fails early),
// but mid-stream failures are inherently untestable via inject().
});
// ---------------------------------------------------------------------------
// 7. Meme-generator x core formats
//
// Custom route, returns {downloadUrl, processedSize}.
// Uses text overlays rendered via SVG compositing.
// ---------------------------------------------------------------------------
describe("Meme-generator cross-format", () => {
for (const fmt of FORMAT_SAMPLES) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`generates meme from ${fmt.name} input`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({
textLayout: "top-bottom",
textBoxes: [
{ id: "top", text: "TOP TEXT" },
{ id: "bottom", text: "BOTTOM TEXT" },
],
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/meme-generator",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
if (needsFallback(fmt)) {
expect(ACCEPTABLE_FALLBACK_CODES).toContain(res.statusCode);
} else {
expect(res.statusCode).toBe(200);
}
if (res.statusCode === 200) {
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(typeof body.downloadUrl).toBe("string");
expect(body.processedSize).toBeGreaterThan(0);
} else {
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 8. Barcode-read x core formats
//
// Custom route. Returns JSON with barcode scan results and optional overlay.
// Images without barcodes should still return 200 with empty results.
// ---------------------------------------------------------------------------
describe("Barcode-read cross-format", () => {
for (const fmt of FORMAT_SAMPLES) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`scans ${fmt.name} input for barcodes`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({ tryHarder: true }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/barcode-read",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
if (needsFallback(fmt)) {
expect(ACCEPTABLE_FALLBACK_CODES).toContain(res.statusCode);
} else {
expect(res.statusCode).toBe(200);
}
if (res.statusCode === 200) {
const body = JSON.parse(res.body);
// barcode-read returns { barcodes: [...], overlayUrl?, width, height }
expect(typeof body).toBe("object");
// The fixture images likely have no barcodes, so barcodes array
// should be empty -- but the response must be well-formed
if (body.barcodes !== undefined) {
expect(Array.isArray(body.barcodes)).toBe(true);
}
} else {
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 9. SVG-to-raster (SVG-only tool)
//
// Only accepts SVG input. Non-SVG formats should be rejected with 400.
// ---------------------------------------------------------------------------
describe("SVG-to-raster", () => {
it("converts SVG to PNG", async () => {
const fixturePath = fixtures.image.formats("svg");
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.svg",
contentType: "image/svg+xml",
content: buffer,
},
{
name: "settings",
content: JSON.stringify({
outputFormat: "png",
width: 200,
dpi: 150,
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/svg-to-raster",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(typeof body.downloadUrl).toBe("string");
expect(body.processedSize).toBeGreaterThan(0);
});
// Test multiple output formats from SVG
const SVG_OUTPUT_FORMATS = ["png", "jpg", "webp", "avif"] as const;
for (const outFmt of SVG_OUTPUT_FORMATS) {
const testTimeout = outFmt === "avif" ? 120_000 : 120_000;
it(`converts SVG to ${outFmt}`, { timeout: testTimeout }, async () => {
const fixturePath = fixtures.image.formats("svg");
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.svg",
contentType: "image/svg+xml",
content: buffer,
},
{
name: "settings",
content: JSON.stringify({ outputFormat: outFmt }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/svg-to-raster",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(body.processedSize).toBeGreaterThan(0);
});
}
// Non-SVG formats should be rejected
const NON_SVG_FORMATS = FORMAT_SAMPLES.filter(
(f) => f.name !== "SVG" && !f.needsCliDecoder && !f.needsHeifDecoder,
);
for (const fmt of NON_SVG_FORMATS) {
it(`rejects ${fmt.name} input with 400`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({ outputFormat: "png" }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/svg-to-raster",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(400);
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
});
}
});
// ---------------------------------------------------------------------------
// 10. Compose (multi-file) x core formats
//
// Requires two files: base image + overlay image. Tests each core format
// as the base image with a fixed PNG overlay, and vice versa.
// ---------------------------------------------------------------------------
describe("Compose cross-format", () => {
const PNG_PATH = fixtures.image.formats("png");
describe("format as base image (overlay is PNG)", () => {
for (const fmt of CORE_FORMATS) {
it(`${fmt.name} base + PNG overlay`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const baseBuffer = readFileSync(fixturePath);
const overlayBuffer = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: baseBuffer,
},
{
name: "overlay",
filename: "sample.png",
contentType: "image/png",
content: overlayBuffer,
},
{
name: "settings",
content: JSON.stringify({
x: 0,
y: 0,
opacity: 50,
blendMode: "over",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/compose",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(body.processedSize).toBeGreaterThan(0);
});
}
});
describe("format as overlay image (base is PNG)", () => {
for (const fmt of CORE_FORMATS) {
// Skip PNG-on-PNG (redundant with the reverse test)
if (fmt.name === "PNG") continue;
it(`PNG base + ${fmt.name} overlay`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const baseBuffer = readFileSync(PNG_PATH);
const overlayBuffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: baseBuffer,
},
{
name: "overlay",
filename: fmt.file,
contentType: fmt.mime,
content: overlayBuffer,
},
{
name: "settings",
content: JSON.stringify({
x: 0,
y: 0,
opacity: 75,
blendMode: "multiply",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/compose",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
// Overlay compositing with opacity < 100 can fail for some formats
// (e.g., WebP, AVIF) due to alpha channel handling in Sharp.
// Accept 200 (success) or 422 (processing error) for core formats.
expect([200, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(body.processedSize).toBeGreaterThan(0);
} else {
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
});
}
});
// Exotic format error resilience
describe("exotic format as base (no crash)", () => {
for (const fmt of EXOTIC_FORMATS) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`${fmt.name} base + PNG overlay: no crash`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const baseBuffer = readFileSync(fixturePath);
const overlayBuffer = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: baseBuffer,
},
{
name: "overlay",
filename: "sample.png",
contentType: "image/png",
content: overlayBuffer,
},
{
name: "settings",
content: JSON.stringify({ x: 0, y: 0, opacity: 50 }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/compose",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).not.toBe(500);
expect([200, 202, 400, 422]).toContain(res.statusCode);
},
perTestTimeout,
);
}
});
});
// ---------------------------------------------------------------------------
// 11. Compare (multi-file) x core formats
//
// Requires two files: image A + image B.
// Tests comparing each core format against PNG.
// ---------------------------------------------------------------------------
describe("Compare cross-format", () => {
const PNG_PATH = fixtures.image.formats("png");
for (const fmt of CORE_FORMATS) {
it(`compares ${fmt.name} with PNG`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const bufferA = readFileSync(fixturePath);
const bufferB = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: bufferA,
},
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: bufferB,
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/compare",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
// Compare returns similarity metrics and diff image
expect(typeof body).toBe("object");
if (body.similarity !== undefined) {
expect(typeof body.similarity).toBe("number");
}
});
}
// Exotic format error resilience
for (const fmt of EXOTIC_FORMATS) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`${fmt.name} vs PNG: no crash`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const bufferA = readFileSync(fixturePath);
const bufferB = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: bufferA,
},
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: bufferB,
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/compare",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).not.toBe(500);
expect([200, 202, 400, 422]).toContain(res.statusCode);
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 12. Collage (multi-file) x core formats
//
// Requires 2+ images and a template. Tests pairing each core format
// with a PNG fixture in a 2-image collage.
// ---------------------------------------------------------------------------
describe("Collage cross-format", () => {
const PNG_PATH = fixtures.image.formats("png");
for (const fmt of CORE_FORMATS) {
// Skip PNG + PNG (trivial)
if (fmt.name === "PNG") continue;
it(`${fmt.name} + PNG in 2-image collage`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const fmtBuffer = readFileSync(fixturePath);
const pngBuffer = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: fmtBuffer,
},
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: pngBuffer,
},
{
name: "settings",
content: JSON.stringify({
templateId: "2-h-equal",
gap: 4,
outputFormat: "png",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/collage",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(body.processedSize).toBeGreaterThan(0);
});
}
// Exotic format resilience
for (const fmt of EXOTIC_FORMATS) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`${fmt.name} + PNG collage: no crash`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const fmtBuffer = readFileSync(fixturePath);
const pngBuffer = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: fmtBuffer,
},
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: pngBuffer,
},
{
name: "settings",
content: JSON.stringify({
templateId: "2-h-equal",
gap: 4,
outputFormat: "png",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/collage",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).not.toBe(500);
expect([200, 202, 400, 422]).toContain(res.statusCode);
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 13. Stitch (multi-file) x core formats
//
// Requires 2+ images. Tests stitching each core format with a PNG fixture.
// ---------------------------------------------------------------------------
describe("Stitch cross-format", () => {
const PNG_PATH = fixtures.image.formats("png");
for (const fmt of CORE_FORMATS) {
// Skip PNG + PNG (trivial)
if (fmt.name === "PNG") continue;
it(`stitches ${fmt.name} + PNG horizontally`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const fmtBuffer = readFileSync(fixturePath);
const pngBuffer = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: fmtBuffer,
},
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: pngBuffer,
},
{
name: "settings",
content: JSON.stringify({
direction: "horizontal",
resizeMode: "fit",
format: "png",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/stitch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(body.processedSize).toBeGreaterThan(0);
});
}
// Exotic format resilience
for (const fmt of EXOTIC_FORMATS) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`${fmt.name} + PNG stitch: no crash`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const fmtBuffer = readFileSync(fixturePath);
const pngBuffer = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: fmtBuffer,
},
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: pngBuffer,
},
{
name: "settings",
content: JSON.stringify({
direction: "horizontal",
resizeMode: "fit",
format: "png",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/stitch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).not.toBe(500);
expect([200, 202, 400, 422]).toContain(res.statusCode);
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 14. Transparency-fixer (AI tool with 501 guard) x formats
//
// Returns 501 when AI sidecar is not installed.
// Tests that the validation layer works correctly for all formats.
// ---------------------------------------------------------------------------
describe("Transparency-fixer cross-format", () => {
for (const fmt of FORMAT_SAMPLES) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`${fmt.name}: returns 501 or accepts input`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: buffer,
},
{
name: "settings",
content: JSON.stringify({ defringe: 30, outputFormat: "png" }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/transparency-fixer",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
// Must never crash
expect(res.statusCode).not.toBe(500);
// Acceptable: 200 (sync success), 202 (async accepted), 400 (bad input),
// 422 (processing error), 501 (AI not installed)
expect([200, 202, 400, 422, 501]).toContain(res.statusCode);
const body = JSON.parse(res.body);
if (res.statusCode === 501) {
expect(body.code).toBe("FEATURE_NOT_INSTALLED");
expect(body.error).toBeDefined();
} else if (res.statusCode === 202) {
expect(body.jobId).toBeDefined();
expect(body.async).toBe(true);
} else if (res.statusCode >= 400) {
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
}
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 15. Color-blindness simulation types x JPEG
//
// Verifies each simulation type works correctly with a single core format.
// ---------------------------------------------------------------------------
describe("Color-blindness simulation types", () => {
const SIMULATION_TYPES = [
"protanopia",
"deuteranopia",
"tritanopia",
"protanomaly",
"deuteranomaly",
"tritanomaly",
"achromatopsia",
"blueConeMonochromacy",
] as const;
for (const simType of SIMULATION_TYPES) {
it(`simulation: ${simType}`, async () => {
const fixturePath = fixtures.image.formats("jpg");
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.jpg",
contentType: "image/jpeg",
content: buffer,
},
{
name: "settings",
content: JSON.stringify({ simulationType: simType }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/color-blindness",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(body.processedSize).toBeGreaterThan(0);
});
}
});
// ---------------------------------------------------------------------------
// 16. Vectorize color mode variants x JPEG
//
// Verifies both BW and color vectorization modes.
// ---------------------------------------------------------------------------
describe("Vectorize color modes", () => {
it("BW mode", async () => {
const fixturePath = fixtures.image.formats("jpg");
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.jpg",
contentType: "image/jpeg",
content: buffer,
},
{
name: "settings",
content: JSON.stringify({
colorMode: "bw",
threshold: 128,
pathMode: "spline",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/vectorize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toContain(".svg");
expect(body.processedSize).toBeGreaterThan(0);
});
it("color mode", async () => {
const fixturePath = fixtures.image.formats("jpg");
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.jpg",
contentType: "image/jpeg",
content: buffer,
},
{
name: "settings",
content: JSON.stringify({
colorMode: "color",
colorPrecision: 4,
layerDifference: 8,
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/vectorize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toContain(".svg");
expect(body.processedSize).toBeGreaterThan(0);
});
});
// ---------------------------------------------------------------------------
// 17. Compose blend modes x JPEG
//
// Verifies each blend mode works correctly.
// ---------------------------------------------------------------------------
describe("Compose blend modes", () => {
const BLEND_MODES = ["over", "multiply", "screen", "overlay", "darken", "lighten"] as const;
const PNG_PATH = fixtures.image.formats("png");
for (const blendMode of BLEND_MODES) {
it(`blend mode: ${blendMode}`, async () => {
const jpgPath = fixtures.image.formats("jpg");
if (!existsSync(jpgPath) || !existsSync(PNG_PATH)) return;
const baseBuffer = readFileSync(jpgPath);
const overlayBuffer = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.jpg",
contentType: "image/jpeg",
content: baseBuffer,
},
{
name: "overlay",
filename: "sample.png",
contentType: "image/png",
content: overlayBuffer,
},
{
name: "settings",
content: JSON.stringify({
x: 0,
y: 0,
opacity: 50,
blendMode,
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/compose",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(body.processedSize).toBeGreaterThan(0);
});
}
});
// ---------------------------------------------------------------------------
// 18. Stitch direction modes x JPEG
//
// Verifies horizontal, vertical, and grid stitching.
// ---------------------------------------------------------------------------
describe("Stitch direction modes", () => {
const DIRECTIONS = [
{ direction: "horizontal", gridColumns: 2 },
{ direction: "vertical", gridColumns: 2 },
{ direction: "grid", gridColumns: 2 },
] as const;
const JPG_PATH = fixtures.image.formats("jpg");
const PNG_PATH = fixtures.image.formats("png");
for (const { direction, gridColumns } of DIRECTIONS) {
it(`direction: ${direction}`, async () => {
if (!existsSync(JPG_PATH) || !existsSync(PNG_PATH)) return;
const bufA = readFileSync(JPG_PATH);
const bufB = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.jpg",
contentType: "image/jpeg",
content: bufA,
},
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: bufB,
},
{
name: "settings",
content: JSON.stringify({
direction,
gridColumns,
resizeMode: "fit",
format: "png",
}),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/stitch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
expect(body.processedSize).toBeGreaterThan(0);
});
}
});
// ---------------------------------------------------------------------------
// 15b. Find-duplicates (multi-file) x core formats
//
// Requires 2+ images. Tests each core format paired with a PNG fixture.
// find-duplicates computes perceptual hashes and returns duplicate groups.
// ---------------------------------------------------------------------------
describe("Find-duplicates cross-format", () => {
const PNG_PATH = fixtures.image.formats("png");
for (const fmt of CORE_FORMATS) {
it(`detects ${fmt.name} + PNG pair`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const fmtBuffer = readFileSync(fixturePath);
const pngBuffer = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: fmtBuffer,
},
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: pngBuffer,
},
{
name: "settings",
content: JSON.stringify({ threshold: 8 }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/find-duplicates",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
// find-duplicates returns { totalImages, duplicateGroups, uniqueCount }
expect(typeof body).toBe("object");
expect(body.totalImages).toBe(2);
expect(Array.isArray(body.duplicateGroups)).toBe(true);
});
}
// Exotic format resilience
for (const fmt of EXOTIC_FORMATS) {
const perTestTimeout = fmt.needsHeifDecoder || fmt.needsCliDecoder ? 180_000 : undefined;
it(
`${fmt.name} + PNG find-duplicates: no crash`,
async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath) || !existsSync(PNG_PATH)) return;
const fmtBuffer = readFileSync(fixturePath);
const pngBuffer = readFileSync(PNG_PATH);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: fmt.file,
contentType: fmt.mime,
content: fmtBuffer,
},
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: pngBuffer,
},
{
name: "settings",
content: JSON.stringify({ threshold: 8 }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/find-duplicates",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).not.toBe(500);
expect([200, 202, 400, 422]).toContain(res.statusCode);
},
perTestTimeout,
);
}
});
// ---------------------------------------------------------------------------
// 19. Error resilience: missing file for expanded tools
//
// Verifies that each expanded tool returns a clean 400 when no file is sent.
// ---------------------------------------------------------------------------
describe("Missing file returns 400 for expanded tools", () => {
const TOOL_ENDPOINTS = [
{ url: "/api/v1/tools/image/color-blindness", settings: { simulationType: "deuteranomaly" } },
{ url: "/api/v1/tools/image/beautify", settings: { padding: 20 } },
{ url: "/api/v1/tools/image/edit-metadata", settings: { title: "test" } },
{ url: "/api/v1/tools/image/vectorize", settings: { colorMode: "bw" } },
{ url: "/api/v1/tools/image/split", settings: { columns: 2, rows: 2 } },
{ url: "/api/v1/tools/image/favicon", settings: {} },
{
url: "/api/v1/tools/image/meme-generator",
settings: {
textLayout: "top-bottom",
textBoxes: [{ id: "top", text: "TEST" }],
},
},
{ url: "/api/v1/tools/image/barcode-read", settings: { tryHarder: true } },
{ url: "/api/v1/tools/image/svg-to-raster", settings: { outputFormat: "png" } },
{ url: "/api/v1/tools/image/compose", settings: { x: 0, y: 0 } },
{ url: "/api/v1/tools/image/compare", settings: {} },
{ url: "/api/v1/tools/image/collage", settings: { templateId: "2-h-equal" } },
{ url: "/api/v1/tools/image/stitch", settings: { direction: "horizontal" } },
{ url: "/api/v1/tools/image/find-duplicates", settings: { threshold: 8 } },
];
for (const { url, settings } of TOOL_ENDPOINTS) {
const toolName = url.split("/").pop();
it(`${toolName}: no file -> 400`, async () => {
const { body: payload, contentType } = createMultipartPayload([
{
name: "settings",
content: JSON.stringify(settings),
},
]);
const res = await app.inject({
method: "POST",
url,
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(400);
const body = JSON.parse(res.body);
expect(body.error).toBeDefined();
expect(typeof body.error).toBe("string");
});
}
});
// ---------------------------------------------------------------------------
// 20. Unauthenticated requests -> 401 for expanded tools
// ---------------------------------------------------------------------------
describe("Unauthenticated requests return 401 for expanded tools", () => {
const TOOL_ENDPOINTS = [
"/api/v1/tools/image/color-blindness",
"/api/v1/tools/image/beautify",
"/api/v1/tools/image/edit-metadata",
"/api/v1/tools/image/vectorize",
"/api/v1/tools/image/split",
"/api/v1/tools/image/favicon",
"/api/v1/tools/image/meme-generator",
"/api/v1/tools/image/barcode-read",
"/api/v1/tools/image/svg-to-raster",
"/api/v1/tools/image/compose",
"/api/v1/tools/image/compare",
"/api/v1/tools/image/collage",
"/api/v1/tools/image/stitch",
"/api/v1/tools/image/find-duplicates",
"/api/v1/tools/image/transparency-fixer",
];
for (const url of TOOL_ENDPOINTS) {
const toolName = url.split("/").pop();
it(`${toolName}: no auth -> 401`, async () => {
const fixturePath = fixtures.image.formats("png");
if (!existsSync(fixturePath)) return;
const buffer = readFileSync(fixturePath);
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: buffer,
},
]);
const res = await app.inject({
method: "POST",
url,
headers: {
// No authorization header
"content-type": contentType,
},
body: payload,
});
expect(res.statusCode).toBe(401);
});
}
});