Files
SnapOtter/tests/integration/api.test.ts
T
Siddharth Kumar Sah c48bfba879 feat: make AI tools pipeline-compatible and add search to tool picker
Register remove-background, upscale, and blur-faces in the pipeline
tool registry via registerToolProcessFn(). These tools keep their
custom HTTP routes (with progress callbacks) for direct use, but now
also provide a simple process function for pipeline/batch execution.

Add a search bar to the pipeline tool picker so users can quickly
find tools by name or description. Uses the existing SearchBar
component and the same filtering pattern as the main tool panel.

Update tests to reflect that these 3 AI tools are now pipeline-
compatible (moved from excluded to included assertions).
2026-03-28 15:09:23 +08:00

2993 lines
103 KiB
TypeScript

/**
* Integration tests for the Stirling-Image API.
*
* These tests build a real Fastify server backed by an in-memory SQLite DB
* and exercise every endpoint with real HTTP-like requests via `app.inject()`.
* The goal is adversarial: we try to BREAK the system with invalid inputs,
* path traversal, missing auth, type confusion, and boundary conditions.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { afterAll, beforeAll, beforeEach, 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 JPG_100x100 = readFileSync(join(FIXTURES, "test-100x100.jpg"));
const WEBP_50x50 = readFileSync(join(FIXTURES, "test-50x50.webp"));
const PNG_1x1 = readFileSync(join(FIXTURES, "test-1x1.png"));
// ---------------------------------------------------------------------------
// 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);
// adminToken is created once in beforeAll — reuse it throughout.
// Individual tests that need a fresh token should create their own.
// ═══════════════════════════════════════════════════════════════════════════
// AUTH ENDPOINTS
// ═══════════════════════════════════════════════════════════════════════════
describe("Auth endpoints", () => {
// ── POST /api/auth/login ───────────────────────────────────────
describe("POST /api/auth/login", () => {
it("returns a token for valid credentials", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "admin", password: "Adminpass1" },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.token).toBeDefined();
expect(typeof body.token).toBe("string");
expect(body.user.username).toBe("admin");
expect(body.user.role).toBe("admin");
expect(body.expiresAt).toBeDefined();
});
it("returns 401 for wrong password", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "admin", password: "wrongpassword" },
});
expect(res.statusCode).toBe(401);
expect(JSON.parse(res.body).error).toMatch(/invalid/i);
});
it("returns 401 for non-existent user", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "ghost", password: "whatever123" },
});
expect(res.statusCode).toBe(401);
});
it("returns 400 when username is missing", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { password: "Adminpass1" },
});
expect(res.statusCode).toBe(400);
});
it("returns 400 when password is missing", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "admin" },
});
expect(res.statusCode).toBe(400);
});
it("returns 400 when body is empty", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: {},
});
expect(res.statusCode).toBe(400);
});
it("returns 400 when body is null", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
headers: { "content-type": "application/json" },
payload: "null",
});
// The route checks body?.username — null body should trigger 400
expect(res.statusCode).toBe(400);
});
it("handles SQL injection attempt in username gracefully", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "' OR 1=1 --", password: "anything" },
});
// Should NOT succeed — parameterized queries prevent injection
expect(res.statusCode).toBe(401);
});
it("handles extremely long username", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "a".repeat(10_000), password: "Adminpass1" },
});
expect(res.statusCode).toBe(401);
});
it("handles unicode in credentials", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "\u0000admin", password: "Adminpass1" },
});
expect(res.statusCode).toBe(401);
});
});
// ── GET /api/auth/session ──────────────────────────────────────
describe("GET /api/auth/session", () => {
it("returns user info with a valid token", async () => {
const res = await app.inject({
method: "GET",
url: "/api/auth/session",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.user.username).toBe("admin");
expect(body.user.role).toBe("admin");
expect(body.expiresAt).toBeDefined();
});
it("returns 401 with no token", async () => {
const res = await app.inject({
method: "GET",
url: "/api/auth/session",
});
expect(res.statusCode).toBe(401);
});
it("returns 401 with an invalid token", async () => {
const res = await app.inject({
method: "GET",
url: "/api/auth/session",
headers: { authorization: "Bearer totally-fake-token-12345" },
});
expect(res.statusCode).toBe(401);
});
it("returns 401 with a malformed Authorization header", async () => {
const res = await app.inject({
method: "GET",
url: "/api/auth/session",
headers: { authorization: "NotBearer something" },
});
expect(res.statusCode).toBe(401);
});
it("returns 401 with empty Bearer token", async () => {
const res = await app.inject({
method: "GET",
url: "/api/auth/session",
headers: { authorization: "Bearer " },
});
expect(res.statusCode).toBe(401);
});
});
// ── POST /api/auth/logout ──────────────────────────────────────
describe("POST /api/auth/logout", () => {
it("invalidates the session token", async () => {
// Get a fresh token to logout
const loginRes = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "admin", password: "Adminpass1" },
});
const freshToken = JSON.parse(loginRes.body).token;
// Logout
const logoutRes = await app.inject({
method: "POST",
url: "/api/auth/logout",
headers: { authorization: `Bearer ${freshToken}` },
});
expect(logoutRes.statusCode).toBe(200);
expect(JSON.parse(logoutRes.body).ok).toBe(true);
// Verify the token no longer works
const sessionRes = await app.inject({
method: "GET",
url: "/api/auth/session",
headers: { authorization: `Bearer ${freshToken}` },
});
expect(sessionRes.statusCode).toBe(401);
});
it("succeeds even without a token (idempotent)", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/logout",
});
// Logout is on a public path prefix (/api/auth/) — should succeed
expect(res.statusCode).toBe(200);
});
});
// ── POST /api/auth/change-password ─────────────────────────────
describe("POST /api/auth/change-password", () => {
it("changes password with valid current password", async () => {
// Create a disposable user first
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "changepw_user", password: "Original1234", role: "user" },
});
expect(regRes.statusCode).toBe(201);
// Log in as the new user
const loginRes = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "changepw_user", password: "Original1234" },
});
const userToken = JSON.parse(loginRes.body).token;
// Change password
const changeRes = await app.inject({
method: "POST",
url: "/api/auth/change-password",
headers: { authorization: `Bearer ${userToken}` },
payload: { currentPassword: "Original1234", newPassword: "Newpassword99" },
});
expect(changeRes.statusCode).toBe(200);
expect(JSON.parse(changeRes.body).ok).toBe(true);
// Verify old password no longer works
const oldLoginRes = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "changepw_user", password: "Original1234" },
});
expect(oldLoginRes.statusCode).toBe(401);
// Verify new password works
const newLoginRes = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "changepw_user", password: "Newpassword99" },
});
expect(newLoginRes.statusCode).toBe(200);
});
it("rejects wrong current password", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/change-password",
headers: { authorization: `Bearer ${adminToken}` },
payload: { currentPassword: "wrong_password_here", newPassword: "Newpass1234" },
});
expect(res.statusCode).toBe(401);
expect(JSON.parse(res.body).code).toBe("INVALID_PASSWORD");
});
it("rejects new password shorter than 8 characters", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/change-password",
headers: { authorization: `Bearer ${adminToken}` },
payload: { currentPassword: "Adminpass1", newPassword: "short" },
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).code).toBe("VALIDATION_ERROR");
});
it("returns 400 when fields are missing", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/change-password",
headers: { authorization: `Bearer ${adminToken}` },
payload: { currentPassword: "Adminpass1" },
});
expect(res.statusCode).toBe(400);
});
it("returns 401 without authentication", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/change-password",
payload: { currentPassword: "Adminpass1", newPassword: "Newpass1234" },
});
expect(res.statusCode).toBe(401);
});
});
// ── POST /api/auth/register ────────────────────────────────────
describe("POST /api/auth/register", () => {
it("admin can create a new user", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "newuser1", password: "Password1234", role: "user" },
});
expect(res.statusCode).toBe(201);
const body = JSON.parse(res.body);
expect(body.username).toBe("newuser1");
expect(body.role).toBe("user");
expect(body.id).toBeDefined();
});
it("returns 409 for duplicate username", async () => {
// First creation
await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "duplicate_user", password: "Password1234" },
});
// Second attempt
const res = await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "duplicate_user", password: "Differentpass1" },
});
expect(res.statusCode).toBe(409);
expect(JSON.parse(res.body).code).toBe("CONFLICT");
});
it("rejects password shorter than 8 characters", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "shortpw_user", password: "short" },
});
expect(res.statusCode).toBe(400);
});
it("returns 400 when username is missing", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { password: "Password1234" },
});
expect(res.statusCode).toBe(400);
});
it("non-admin cannot register users", async () => {
// Create a regular user first
await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "regular_user", password: "Password1234", role: "user" },
});
// Log in as regular user
const loginRes = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "regular_user", password: "Password1234" },
});
const userToken = JSON.parse(loginRes.body).token;
// Try to register — should fail with 403
const res = await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${userToken}` },
payload: { username: "sneaky_user", password: "Password1234" },
});
expect(res.statusCode).toBe(403);
});
it("defaults role to 'user' when invalid role provided", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "badrole_user", password: "Password1234", role: "superadmin" },
});
expect(res.statusCode).toBe(201);
expect(JSON.parse(res.body).role).toBe("user");
});
});
// ── DELETE /api/auth/users/:id ─────────────────────────────────
describe("DELETE /api/auth/users/:id", () => {
it("admin can't delete themselves", async () => {
// Get the admin's session to find their user ID
const sessionRes = await app.inject({
method: "GET",
url: "/api/auth/session",
headers: { authorization: `Bearer ${adminToken}` },
});
const adminId = JSON.parse(sessionRes.body).user.id;
const res = await app.inject({
method: "DELETE",
url: `/api/auth/users/${adminId}`,
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).code).toBe("SELF_DELETE");
});
it("admin can delete another user", async () => {
// Create a user to delete
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "doomed_user", password: "Password1234" },
});
const userId = JSON.parse(regRes.body).id;
const deleteRes = await app.inject({
method: "DELETE",
url: `/api/auth/users/${userId}`,
headers: { authorization: `Bearer ${adminToken}` },
});
expect(deleteRes.statusCode).toBe(200);
expect(JSON.parse(deleteRes.body).ok).toBe(true);
// Verify the user can no longer log in
const loginRes = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "doomed_user", password: "Password1234" },
});
expect(loginRes.statusCode).toBe(401);
});
it("returns 404 for non-existent user ID", async () => {
const res = await app.inject({
method: "DELETE",
url: "/api/auth/users/00000000-0000-0000-0000-000000000000",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(404);
});
it("non-admin cannot delete users", async () => {
// Create + login as regular user
await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "nonadmin_deleter", password: "Password1234", role: "user" },
});
const loginRes = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "nonadmin_deleter", password: "Password1234" },
});
const userToken = JSON.parse(loginRes.body).token;
const res = await app.inject({
method: "DELETE",
url: "/api/auth/users/some-random-id",
headers: { authorization: `Bearer ${userToken}` },
});
expect(res.statusCode).toBe(403);
});
});
});
// ═══════════════════════════════════════════════════════════════════════════
// FILE UPLOAD / DOWNLOAD
// ═══════════════════════════════════════════════════════════════════════════
describe("File upload/download", () => {
// ── POST /api/v1/upload ────────────────────────────────────────
describe("POST /api/v1/upload", () => {
it("uploads a valid PNG and returns a jobId", async () => {
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "test.png",
contentType: "image/png",
content: PNG_200x150,
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.jobId).toBeDefined();
expect(body.files).toHaveLength(1);
expect(body.files[0].name).toBe("test.png");
expect(body.files[0].format).toBe("png");
expect(body.files[0].size).toBeGreaterThan(0);
});
it("uploads multiple files at once", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG_1x1 },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG_100x100 },
{ name: "file", filename: "c.webp", contentType: "image/webp", content: WEBP_50x50 },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body).files).toHaveLength(3);
});
it("returns 400 for empty multipart (no files)", async () => {
const { body: payload, contentType } = createMultipartPayload([
// Only a text field, no file
{ name: "metadata", content: "some text" },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toMatch(/no valid files/i);
});
it("returns 400 for a non-image file (text file disguised as upload)", async () => {
const textContent = Buffer.from("This is not an image. Just plain text content.");
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "malicious.txt",
contentType: "text/plain",
content: textContent,
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("rejects a file with image extension but non-image content", async () => {
const fakeImage = Buffer.from("#!/bin/bash\necho 'gotcha'");
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "evil.png",
contentType: "image/png",
content: fakeImage,
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("sanitizes path traversal in filename", async () => {
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "../../../etc/passwd.png",
contentType: "image/png",
content: PNG_1x1,
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
// Should succeed but the filename should be sanitized
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.files[0].name).not.toContain("..");
expect(body.files[0].name).not.toContain("/");
});
});
// ── GET /api/v1/download/:jobId/:filename ──────────────────────
describe("GET /api/v1/download/:jobId/:filename", () => {
let uploadJobId: string;
let uploadedFilename: string;
beforeAll(async () => {
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "download-test.png",
contentType: "image/png",
content: PNG_200x150,
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
const body = JSON.parse(res.body);
uploadJobId = body.jobId;
uploadedFilename = body.files[0].name;
});
it("downloads an uploaded file successfully", async () => {
const res = await app.inject({
method: "GET",
url: `/api/v1/download/${uploadJobId}/${uploadedFilename}`,
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toContain("image/png");
expect(res.headers["content-disposition"]).toContain("attachment");
// Verify the downloaded bytes match what we uploaded
expect(Buffer.from(res.rawPayload).length).toBe(PNG_200x150.length);
});
it("download is public (no auth required)", async () => {
const res = await app.inject({
method: "GET",
url: `/api/v1/download/${uploadJobId}/${uploadedFilename}`,
// No authorization header
});
expect(res.statusCode).toBe(200);
});
it("returns 404 for non-existent file", async () => {
const res = await app.inject({
method: "GET",
url: `/api/v1/download/${uploadJobId}/nonexistent.png`,
});
expect(res.statusCode).toBe(404);
});
it("returns 404 for non-existent jobId", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/download/00000000-0000-0000-0000-000000000000/file.png",
});
expect(res.statusCode).toBe(404);
});
it("returns 400 for path traversal in jobId", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/download/..%2F..%2F..%2Fetc/passwd",
});
expect(res.statusCode).toBe(400);
});
it("returns 400 for path traversal in filename", async () => {
const res = await app.inject({
method: "GET",
url: `/api/v1/download/${uploadJobId}/..%2F..%2F..%2Fetc%2Fpasswd`,
});
expect(res.statusCode).toBe(400);
});
it("returns 400 or 401 for backslash traversal in filename", async () => {
const res = await app.inject({
method: "GET",
url: `/api/v1/download/${uploadJobId}/..\\..\\..\\etc\\passwd`,
});
// Backslash may corrupt the URL path so the public-path check fails (401)
// or the path-traversal guard catches it (400). Either way, blocked.
expect([400, 401]).toContain(res.statusCode);
});
it("returns 400 for null bytes in filename", async () => {
const res = await app.inject({
method: "GET",
url: `/api/v1/download/${uploadJobId}/file.png%00.txt`,
});
expect(res.statusCode).toBe(400);
});
});
});
// ═══════════════════════════════════════════════════════════════════════════
// TOOL PROCESSING
// ═══════════════════════════════════════════════════════════════════════════
describe("Tool processing", () => {
// ── Resize ─────────────────────────────────────────────────────
describe("POST /api/v1/tools/resize", () => {
it("resizes an image successfully", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "resize-me.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ width: 100, height: 75 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.jobId).toBeDefined();
expect(body.downloadUrl).toBeDefined();
expect(body.originalSize).toBeGreaterThan(0);
expect(body.processedSize).toBeGreaterThan(0);
});
it("resizes by percentage", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "pct.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ percentage: 50 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
});
it("returns 400 for invalid settings (negative width)", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "bad.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify({ width: -100 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("returns 400 when no file is provided", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "settings", content: JSON.stringify({ width: 100 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toMatch(/no image/i);
});
it("returns 400 for malformed settings JSON", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "img.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: "{not valid json!!!" },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toMatch(/json/i);
});
it("returns 422 with empty settings (no dimensions given)", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "defaults.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: "{}" },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
// All resize fields are optional at the Zod level, but Sharp needs at
// least width or height — so processing fails with 422
expect(res.statusCode).toBe(422);
});
it("download URL from resize result is accessible", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "chain.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ width: 50 }) },
]);
const resizeRes = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
const { downloadUrl } = JSON.parse(resizeRes.body);
const downloadRes = await app.inject({
method: "GET",
url: downloadUrl,
});
expect(downloadRes.statusCode).toBe(200);
});
});
// ── Crop ───────────────────────────────────────────────────────
describe("POST /api/v1/tools/crop", () => {
it("crops an image successfully", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "crop.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ left: 10, top: 10, width: 50, height: 50 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/crop",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toBeDefined();
});
it("returns 400 when required crop fields are missing", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "crop.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ left: 10 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/crop",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("returns 422 when crop exceeds image bounds", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "crop.png", contentType: "image/png", content: PNG_200x150 },
{
name: "settings",
content: JSON.stringify({ left: 0, top: 0, width: 9999, height: 9999 }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/crop",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
// Sharp should throw when crop extends beyond image — wrapped as 422
expect(res.statusCode).toBe(422);
});
});
// ── Convert ────────────────────────────────────────────────────
describe("POST /api/v1/tools/convert", () => {
it("converts PNG to JPEG", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "convert.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ format: "jpg" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.downloadUrl).toContain(".jpg");
});
it("converts PNG to WebP", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "convert.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ format: "webp", quality: 80 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
});
it("rejects unsupported format", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "convert.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify({ format: "bmp" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("rejects quality outside valid range", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "convert.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify({ format: "jpg", quality: 999 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
});
// ── Rotate ─────────────────────────────────────────────────────
describe("POST /api/v1/tools/rotate", () => {
it("rotates 90 degrees", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "rotate.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ angle: 90 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/rotate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
});
it("flips horizontally", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "flip.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ horizontal: true }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/rotate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
});
it("works with default settings (no rotation)", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "norotate.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: "{}" },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/rotate",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
});
});
// ── Nonexistent tool ───────────────────────────────────────────
describe("POST /api/v1/tools/nonexistent", () => {
it("returns 404 for a tool that does not exist", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "img.png", contentType: "image/png", content: PNG_1x1 },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/nonexistent",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(404);
});
});
// ── Different input formats ────────────────────────────────────
describe("Input format handling", () => {
it("processes JPEG input", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "test.jpg", contentType: "image/jpeg", content: JPG_100x100 },
{ name: "settings", content: JSON.stringify({ width: 50 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
});
it("processes WebP input", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "test.webp", contentType: "image/webp", content: WEBP_50x50 },
{ name: "settings", content: JSON.stringify({ width: 25 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
});
});
});
// ═══════════════════════════════════════════════════════════════════════════
// HEALTH & CONFIG
// ═══════════════════════════════════════════════════════════════════════════
describe("Health & Config", () => {
describe("GET /api/v1/health", () => {
it("returns healthy status without auth", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/health",
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.status).toBe("healthy");
expect(body.version).toBeDefined();
expect(body.uptime).toBeDefined();
});
it("also works with auth", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/health",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(200);
});
});
describe("GET /api/v1/config/auth", () => {
it("returns authEnabled flag without auth", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/config/auth",
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(typeof body.authEnabled).toBe("boolean");
expect(body.authEnabled).toBe(true); // We set AUTH_ENABLED=true in test-server
});
});
});
// ═══════════════════════════════════════════════════════════════════════════
// AUTH MIDDLEWARE
// ═══════════════════════════════════════════════════════════════════════════
describe("Auth middleware", () => {
it("protected route without token returns 401", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/settings",
});
expect(res.statusCode).toBe(401);
});
it("protected route with valid token succeeds", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/settings",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(200);
});
it("public route /api/v1/health works without token", async () => {
const res = await app.inject({ method: "GET", url: "/api/v1/health" });
expect(res.statusCode).toBe(200);
});
it("public route /api/v1/config/auth works without token", async () => {
const res = await app.inject({ method: "GET", url: "/api/v1/config/auth" });
expect(res.statusCode).toBe(200);
});
it("upload route requires auth", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "img.png", contentType: "image/png", content: PNG_1x1 },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: { "content-type": contentType },
payload,
});
expect(res.statusCode).toBe(401);
});
it("tool routes require auth", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "img.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: "{}" },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: { "content-type": contentType },
payload,
});
expect(res.statusCode).toBe(401);
});
it("settings routes require auth", async () => {
const res = await app.inject({
method: "PUT",
url: "/api/v1/settings",
payload: { theme: "dark" },
});
expect(res.statusCode).toBe(401);
});
it("expired token is rejected", async () => {
// We can't easily create an expired session through the API, but we can
// check that a completely fake token is rejected on protected routes.
const res = await app.inject({
method: "GET",
url: "/api/v1/settings",
headers: { authorization: "Bearer expired-fake-token-xyz" },
});
expect(res.statusCode).toBe(401);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// SETTINGS
// ═══════════════════════════════════════════════════════════════════════════
describe("Settings", () => {
describe("GET /api/v1/settings", () => {
it("returns settings object (requires auth)", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/settings",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.settings).toBeDefined();
expect(typeof body.settings).toBe("object");
});
it("returns 401 without auth", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/settings",
});
expect(res.statusCode).toBe(401);
});
});
describe("PUT /api/v1/settings", () => {
it("admin can save settings", async () => {
const res = await app.inject({
method: "PUT",
url: "/api/v1/settings",
headers: { authorization: `Bearer ${adminToken}` },
payload: { theme: "dark", locale: "en" },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.ok).toBe(true);
expect(body.updatedCount).toBe(2);
});
it("saved settings are retrievable", async () => {
// Save
await app.inject({
method: "PUT",
url: "/api/v1/settings",
headers: { authorization: `Bearer ${adminToken}` },
payload: { testKey: "testValue" },
});
// Retrieve
const res = await app.inject({
method: "GET",
url: "/api/v1/settings/testKey",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.key).toBe("testKey");
expect(body.value).toBe("testValue");
});
it("non-admin cannot save settings", async () => {
// Create a regular user
await app.inject({
method: "POST",
url: "/api/auth/register",
headers: { authorization: `Bearer ${adminToken}` },
payload: { username: "settings_user", password: "Password1234", role: "user" },
});
const loginRes = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "settings_user", password: "Password1234" },
});
const userToken = JSON.parse(loginRes.body).token;
const res = await app.inject({
method: "PUT",
url: "/api/v1/settings",
headers: { authorization: `Bearer ${userToken}` },
payload: { theme: "hacked" },
});
expect(res.statusCode).toBe(403);
});
it("returns 400 for non-object body", async () => {
const res = await app.inject({
method: "PUT",
url: "/api/v1/settings",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: JSON.stringify([1, 2, 3]),
});
expect(res.statusCode).toBe(400);
});
it("settings upsert works (update existing key)", async () => {
// Set initial value
await app.inject({
method: "PUT",
url: "/api/v1/settings",
headers: { authorization: `Bearer ${adminToken}` },
payload: { upsertKey: "original" },
});
// Update
await app.inject({
method: "PUT",
url: "/api/v1/settings",
headers: { authorization: `Bearer ${adminToken}` },
payload: { upsertKey: "updated" },
});
// Verify
const res = await app.inject({
method: "GET",
url: "/api/v1/settings/upsertKey",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(JSON.parse(res.body).value).toBe("updated");
});
});
describe("GET /api/v1/settings/:key", () => {
it("returns 404 for non-existent key", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/settings/nonexistent_key_xyz",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(404);
});
});
});
// ═══════════════════════════════════════════════════════════════════════════
// API KEY MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
describe("API Keys", () => {
describe("POST /api/v1/api-keys", () => {
it("generates a new API key", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/api-keys",
headers: { authorization: `Bearer ${adminToken}` },
payload: { name: "Test Key" },
});
expect(res.statusCode).toBe(201);
const body = JSON.parse(res.body);
expect(body.key).toBeDefined();
expect(body.key).toMatch(/^si_/);
expect(body.name).toBe("Test Key");
expect(body.id).toBeDefined();
});
it("uses default name when none provided", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/api-keys",
headers: { authorization: `Bearer ${adminToken}` },
payload: {},
});
expect(res.statusCode).toBe(201);
expect(JSON.parse(res.body).name).toBe("Default API Key");
});
it("rejects name longer than 100 chars", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/api-keys",
headers: { authorization: `Bearer ${adminToken}` },
payload: { name: "x".repeat(101) },
});
expect(res.statusCode).toBe(400);
});
it("requires auth", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/api-keys",
payload: { name: "Sneaky Key" },
});
expect(res.statusCode).toBe(401);
});
});
describe("GET /api/v1/api-keys", () => {
it("lists user's API keys (without exposing raw keys)", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/api-keys",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.apiKeys).toBeDefined();
expect(Array.isArray(body.apiKeys)).toBe(true);
// Verify that no raw key is returned in list
for (const key of body.apiKeys) {
expect(key.key).toBeUndefined();
expect(key.keyHash).toBeUndefined();
expect(key.id).toBeDefined();
expect(key.name).toBeDefined();
}
});
});
describe("DELETE /api/v1/api-keys/:id", () => {
it("deletes own API key", async () => {
// Create a key
const createRes = await app.inject({
method: "POST",
url: "/api/v1/api-keys",
headers: { authorization: `Bearer ${adminToken}` },
payload: { name: "To Delete" },
});
const keyId = JSON.parse(createRes.body).id;
// Delete it
const deleteRes = await app.inject({
method: "DELETE",
url: `/api/v1/api-keys/${keyId}`,
headers: { authorization: `Bearer ${adminToken}` },
});
expect(deleteRes.statusCode).toBe(200);
expect(JSON.parse(deleteRes.body).ok).toBe(true);
});
it("returns 404 for non-existent key", async () => {
const res = await app.inject({
method: "DELETE",
url: "/api/v1/api-keys/00000000-0000-0000-0000-000000000000",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(404);
});
});
});
// ═══════════════════════════════════════════════════════════════════════════
// PIPELINE
// ═══════════════════════════════════════════════════════════════════════════
describe("Pipeline", () => {
describe("POST /api/v1/pipeline/execute", () => {
it("executes a multi-step pipeline (resize then rotate)", async () => {
const pipeline = {
steps: [
{ toolId: "resize", settings: { width: 100 } },
{ toolId: "rotate", settings: { angle: 90 } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "pipeline.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(2);
expect(body.downloadUrl).toBeDefined();
});
it("returns 400 when pipeline references non-existent tool", async () => {
const pipeline = {
steps: [{ toolId: "fake-tool-xyz", settings: {} }],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "pipeline.png", contentType: "image/png", content: PNG_1x1 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("returns 400 when pipeline has no steps", async () => {
const pipeline = { steps: [] };
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "pipeline.png", contentType: "image/png", content: PNG_1x1 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("returns 400 without pipeline definition", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "pipeline.png", contentType: "image/png", content: PNG_1x1 },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("returns 400 when a step has invalid settings", async () => {
const pipeline = {
steps: [
{ toolId: "crop", settings: { left: -1, top: -1 } }, // invalid: min(0)
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "pipeline.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("executes Social Media Ready template pipeline", async () => {
const pipeline = {
steps: [
{ toolId: "resize", settings: { width: 1080, height: 1080, fit: "cover" } },
{ toolId: "compress", settings: { quality: 80 } },
{ toolId: "strip-metadata", settings: {} },
{ toolId: "convert", settings: { format: "webp" } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "social.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(4);
expect(body.steps).toHaveLength(4);
expect(body.steps[0].toolId).toBe("resize");
expect(body.steps[1].toolId).toBe("compress");
expect(body.steps[2].toolId).toBe("strip-metadata");
expect(body.steps[3].toolId).toBe("convert");
expect(body.downloadUrl).toContain(".webp");
// Note: upscaling from 200x150 to 1080x1080 increases size
expect(body.processedSize).toBeGreaterThan(0);
});
it("executes Privacy Clean template pipeline", async () => {
const pipeline = {
steps: [
{ toolId: "strip-metadata", settings: {} },
{ toolId: "convert", settings: { format: "jpg" } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "privacy.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(2);
expect(body.downloadUrl).toContain(".jpg");
});
it("executes Web Optimization template pipeline", async () => {
const pipeline = {
steps: [
{ toolId: "resize", settings: { width: 1920, fit: "inside" } },
{ toolId: "convert", settings: { format: "webp" } },
{ toolId: "compress", settings: { quality: 80 } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "web.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(3);
});
it("executes Profile Picture template pipeline", async () => {
const pipeline = {
steps: [
{ toolId: "resize", settings: { width: 400, height: 400, fit: "cover" } },
{ toolId: "compress", settings: { quality: 85 } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "profile.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(2);
});
it("executes Watermark Batch template pipeline", async () => {
const pipeline = {
steps: [
{ toolId: "watermark-text", settings: { text: "SAMPLE", opacity: 30 } },
{ toolId: "strip-metadata", settings: {} },
{ toolId: "compress", settings: { quality: 85 } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "watermark.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(3);
});
it("executes a single-step pipeline", async () => {
const pipeline = {
steps: [{ toolId: "convert", settings: { format: "webp" } }],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "single.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(1);
expect(body.downloadUrl).toContain(".webp");
});
it("returns full step details in response", async () => {
const pipeline = {
steps: [
{ toolId: "resize", settings: { width: 50 } },
{ toolId: "convert", settings: { format: "jpg" } },
{ toolId: "compress", settings: { quality: 60 } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "details.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.jobId).toBeDefined();
expect(typeof body.jobId).toBe("string");
expect(body.originalSize).toBeGreaterThan(0);
expect(body.processedSize).toBeGreaterThan(0);
expect(body.steps).toHaveLength(3);
for (const step of body.steps) {
expect(step.step).toBeGreaterThan(0);
expect(step.toolId).toBeDefined();
expect(step.size).toBeGreaterThan(0);
}
});
it("returns 400 for invalid JSON pipeline", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "bad.png", contentType: "image/png", content: PNG_1x1 },
{ name: "pipeline", content: "not valid json{{{" },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toContain("JSON");
});
it("returns 400 when no file is provided", async () => {
const pipeline = {
steps: [{ toolId: "resize", settings: { width: 100 } }],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("handles pipeline with different image formats (JPG input)", async () => {
const pipeline = {
steps: [
{ toolId: "resize", settings: { width: 50 } },
{ toolId: "convert", settings: { format: "png" } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "test.jpg", contentType: "image/jpeg", content: JPG_100x100 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(2);
expect(body.downloadUrl).toContain(".png");
});
it("handles pipeline with WebP input", async () => {
const pipeline = {
steps: [
{ toolId: "resize", settings: { width: 25 } },
{ toolId: "convert", settings: { format: "jpg" } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "test.webp", contentType: "image/webp", content: WEBP_50x50 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(2);
});
it("executes a pipeline with border + rotate combo", async () => {
const pipeline = {
steps: [
{ toolId: "border", settings: { borderWidth: 10, borderColor: "#ff0000" } },
{ toolId: "rotate", settings: { angle: 45 } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "bordered.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body).stepsCompleted).toBe(2);
});
it("executes pipeline with 5 steps chained", async () => {
const pipeline = {
steps: [
{ toolId: "resize", settings: { width: 150 } },
{ toolId: "rotate", settings: { angle: 90 } },
{ toolId: "strip-metadata", settings: {} },
{ toolId: "compress", settings: { quality: 70 } },
{ toolId: "convert", settings: { format: "webp" } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "chain.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.stepsCompleted).toBe(5);
expect(body.steps).toHaveLength(5);
});
it("rejects pipeline exceeding 20 steps", async () => {
const steps = Array.from({ length: 21 }, () => ({
toolId: "resize",
settings: { width: 100 },
}));
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "too-many.png", contentType: "image/png", content: PNG_1x1 },
{ name: "pipeline", content: JSON.stringify({ steps }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("processes result file is downloadable", async () => {
const pipeline = {
steps: [{ toolId: "resize", settings: { width: 50 } }],
};
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "downloadable.png",
contentType: "image/png",
content: PNG_200x150,
},
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
// Verify the download URL is valid
const dlRes = await app.inject({
method: "GET",
url: body.downloadUrl,
headers: { authorization: `Bearer ${adminToken}` },
});
expect(dlRes.statusCode).toBe(200);
expect(dlRes.rawPayload.length).toBeGreaterThan(0);
});
it("returns 400 with mixed valid and invalid tool IDs", async () => {
const pipeline = {
steps: [
{ toolId: "resize", settings: { width: 100 } },
{ toolId: "nonexistent-tool", settings: {} },
{ toolId: "convert", settings: { format: "jpg" } },
],
};
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "mixed.png", contentType: "image/png", content: PNG_1x1 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toContain("nonexistent-tool");
});
});
describe("Pipeline CRUD", () => {
let savedPipelineId: string;
it("saves a pipeline definition", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: {
name: "My Pipeline",
description: "Test pipeline",
steps: [
{ toolId: "resize", settings: { width: 100 } },
{ toolId: "convert", settings: { format: "jpg" } },
],
},
});
expect(res.statusCode).toBe(201);
const body = JSON.parse(res.body);
expect(body.id).toBeDefined();
expect(body.name).toBe("My Pipeline");
savedPipelineId = body.id;
});
it("lists saved pipelines", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/pipeline/list",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.pipelines).toBeDefined();
expect(body.pipelines.length).toBeGreaterThan(0);
});
it("deletes a saved pipeline", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/v1/pipeline/${savedPipelineId}`,
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(200);
expect(JSON.parse(res.body).ok).toBe(true);
});
it("returns 404 deleting non-existent pipeline", async () => {
const res = await app.inject({
method: "DELETE",
url: "/api/v1/pipeline/00000000-0000-0000-0000-000000000000",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(404);
});
it("rejects pipeline save with invalid tool ID", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: {
name: "Bad Pipeline",
steps: [{ toolId: "does-not-exist", settings: {} }],
},
});
expect(res.statusCode).toBe(400);
});
it("rejects pipeline save with empty name", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: {
name: "",
steps: [{ toolId: "resize", settings: {} }],
},
});
expect(res.statusCode).toBe(400);
});
it("saves pipeline without description", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: {
name: "No Description Pipeline",
steps: [{ toolId: "resize", settings: { width: 100 } }],
},
});
expect(res.statusCode).toBe(201);
const body = JSON.parse(res.body);
expect(body.name).toBe("No Description Pipeline");
expect(body.description).toBeNull();
// Cleanup
await app.inject({
method: "DELETE",
url: `/api/v1/pipeline/${body.id}`,
headers: { authorization: `Bearer ${adminToken}` },
});
});
it("saves and retrieves pipeline with correct step data", async () => {
const steps = [
{ toolId: "resize", settings: { width: 200, height: 200, fit: "cover" } },
{ toolId: "compress", settings: { quality: 75 } },
{ toolId: "convert", settings: { format: "webp" } },
];
const saveRes = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: { name: "Full Pipeline", description: "With all data", steps },
});
expect(saveRes.statusCode).toBe(201);
const saved = JSON.parse(saveRes.body);
// List and verify the pipeline steps are correctly stored
const listRes = await app.inject({
method: "GET",
url: "/api/v1/pipeline/list",
headers: { authorization: `Bearer ${adminToken}` },
});
const listed = JSON.parse(listRes.body);
const found = listed.pipelines.find((p: { id: string }) => p.id === saved.id);
expect(found).toBeDefined();
expect(found.steps).toHaveLength(3);
expect(found.steps[0].toolId).toBe("resize");
expect(found.steps[0].settings.width).toBe(200);
expect(found.steps[1].toolId).toBe("compress");
expect(found.steps[2].toolId).toBe("convert");
expect(found.createdAt).toBeDefined();
// Cleanup
await app.inject({
method: "DELETE",
url: `/api/v1/pipeline/${saved.id}`,
headers: { authorization: `Bearer ${adminToken}` },
});
});
it("rejects pipeline name exceeding 100 characters", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: {
name: "A".repeat(101),
steps: [{ toolId: "resize", settings: {} }],
},
});
expect(res.statusCode).toBe(400);
});
it("rejects pipeline description exceeding 500 characters", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: {
name: "Long Desc",
description: "D".repeat(501),
steps: [{ toolId: "resize", settings: {} }],
},
});
expect(res.statusCode).toBe(400);
});
it("rejects saving pipeline with more than 20 steps", async () => {
const steps = Array.from({ length: 21 }, () => ({
toolId: "resize",
settings: { width: 100 },
}));
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: { name: "Too Many Steps", steps },
});
expect(res.statusCode).toBe(400);
});
it("can save and delete multiple pipelines", async () => {
// Save two pipelines
const res1 = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: {
name: "Pipeline A",
steps: [{ toolId: "resize", settings: { width: 100 } }],
},
});
const res2 = await app.inject({
method: "POST",
url: "/api/v1/pipeline/save",
headers: { authorization: `Bearer ${adminToken}` },
payload: {
name: "Pipeline B",
steps: [{ toolId: "convert", settings: { format: "jpg" } }],
},
});
expect(res1.statusCode).toBe(201);
expect(res2.statusCode).toBe(201);
const id1 = JSON.parse(res1.body).id;
const id2 = JSON.parse(res2.body).id;
// List should have both
const listRes = await app.inject({
method: "GET",
url: "/api/v1/pipeline/list",
headers: { authorization: `Bearer ${adminToken}` },
});
const pipelines = JSON.parse(listRes.body).pipelines;
expect(pipelines.some((p: { id: string }) => p.id === id1)).toBe(true);
expect(pipelines.some((p: { id: string }) => p.id === id2)).toBe(true);
// Delete both
await app.inject({
method: "DELETE",
url: `/api/v1/pipeline/${id1}`,
headers: { authorization: `Bearer ${adminToken}` },
});
await app.inject({
method: "DELETE",
url: `/api/v1/pipeline/${id2}`,
headers: { authorization: `Bearer ${adminToken}` },
});
});
});
describe("GET /api/v1/pipeline/tools", () => {
it("returns the list of pipeline-compatible tool IDs", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/pipeline/tools",
headers: { authorization: `Bearer ${adminToken}` },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(Array.isArray(body.toolIds)).toBe(true);
expect(body.toolIds.length).toBeGreaterThan(0);
});
it("includes factory-registered tools (resize, crop, convert)", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/pipeline/tools",
headers: { authorization: `Bearer ${adminToken}` },
});
const { toolIds } = JSON.parse(res.body);
expect(toolIds).toContain("resize");
expect(toolIds).toContain("crop");
expect(toolIds).toContain("convert");
expect(toolIds).toContain("compress");
expect(toolIds).toContain("rotate");
});
it("includes AI tools registered for pipeline use", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/pipeline/tools",
headers: { authorization: `Bearer ${adminToken}` },
});
const { toolIds } = JSON.parse(res.body);
expect(toolIds).toContain("remove-background");
expect(toolIds).toContain("upscale");
expect(toolIds).toContain("blur-faces");
});
it("excludes tools that are not pipeline-compatible", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/pipeline/tools",
headers: { authorization: `Bearer ${adminToken}` },
});
const { toolIds } = JSON.parse(res.body);
expect(toolIds).not.toContain("ocr");
expect(toolIds).not.toContain("erase-object");
expect(toolIds).not.toContain("info");
expect(toolIds).not.toContain("collage");
expect(toolIds).not.toContain("compare");
});
});
describe("Pipeline rejects incompatible tools", () => {
const customRouteTools = ["ocr", "erase-object"];
for (const toolId of customRouteTools) {
it(`returns 400 when pipeline uses "${toolId}" (custom-route tool)`, async () => {
const pipeline = { steps: [{ toolId, settings: {} }] };
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 },
{ name: "pipeline", content: JSON.stringify(pipeline) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/pipeline/execute",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
expect(JSON.parse(res.body).error).toContain("not found");
});
}
});
});
// ═══════════════════════════════════════════════════════════════════════════
// BATCH PROCESSING
// ═══════════════════════════════════════════════════════════════════════════
describe("Batch processing", () => {
describe("POST /api/v1/tools/:toolId/batch", () => {
it("processes multiple images in a batch and returns ZIP", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG_1x1 },
{ name: "file", filename: "b.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ width: 50 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
// Batch streams a ZIP directly — raw status comes from writeHead
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toBe("application/zip");
expect(res.headers["x-job-id"]).toBeDefined();
});
it("returns 404 for non-existent batch tool", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: "{}" },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/nonexistent/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(404);
});
it("returns 400 for batch with no files", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "settings", content: JSON.stringify({ width: 50 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("batch converts multiple images to webp", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "img1.png", contentType: "image/png", content: PNG_200x150 },
{ name: "file", filename: "img2.jpg", contentType: "image/jpeg", content: JPG_100x100 },
{ name: "settings", content: JSON.stringify({ format: "webp" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/convert/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toBe("application/zip");
});
it("batch compresses images with quality setting", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG_200x150 },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG_100x100 },
{ name: "settings", content: JSON.stringify({ quality: 50 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/compress/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toBe("application/zip");
});
it("batch strips metadata from images", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "meta1.png", contentType: "image/png", content: PNG_200x150 },
{ name: "file", filename: "meta2.jpg", contentType: "image/jpeg", content: JPG_100x100 },
{ name: "settings", content: JSON.stringify({}) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/strip-metadata/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toBe("application/zip");
});
it("batch rotates images", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "rot1.png", contentType: "image/png", content: PNG_200x150 },
{ name: "file", filename: "rot2.webp", contentType: "image/webp", content: WEBP_50x50 },
{ name: "settings", content: JSON.stringify({ angle: 180 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/rotate/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toBe("application/zip");
});
it("returns 400 for batch with invalid settings", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify({ format: "invalid-format" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/convert/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("batch with single file works", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "solo.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ width: 100 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toBe("application/zip");
});
it("batch uses default settings when none provided", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "default.png", contentType: "image/png", content: PNG_200x150 },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/strip-metadata/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toBe("application/zip");
});
it("batch includes X-File-Order header", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "first.png", contentType: "image/png", content: PNG_1x1 },
{ name: "file", filename: "second.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ width: 50 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["x-file-order"]).toBeDefined();
});
it("returns ZIP with content-disposition attachment header", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify({ width: 50 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-disposition"]).toContain("attachment");
expect(res.headers["content-disposition"]).toContain("batch-resize");
});
it("batch with clientJobId uses provided ID", async () => {
const clientJobId = "my-custom-job-id-12345";
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify({ width: 50 }) },
{ name: "clientJobId", content: clientJobId },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["x-job-id"]).toBe(clientJobId);
});
it("batch handles mixed file formats", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG_200x150 },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG_100x100 },
{ name: "file", filename: "c.webp", contentType: "image/webp", content: WEBP_50x50 },
{ name: "settings", content: JSON.stringify({ width: 50 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize/batch",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(200);
expect(res.headers["content-type"]).toBe("application/zip");
});
});
describe("Batch rejects incompatible tools", () => {
const customRouteTools = ["ocr", "erase-object"];
for (const toolId of customRouteTools) {
it(`returns 404 for batch "${toolId}" (custom-route tool)`, async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({}) },
]);
const res = await app.inject({
method: "POST",
url: `/api/v1/tools/${toolId}/batch`,
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(404);
expect(JSON.parse(res.body).error).toContain("not found");
});
}
});
});
// ═══════════════════════════════════════════════════════════════════════════
// EDGE CASES & ADVERSARIAL INPUTS
// ═══════════════════════════════════════════════════════════════════════════
describe("Edge cases & adversarial inputs", () => {
it("rejects a zero-byte file upload", async () => {
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "empty.png",
contentType: "image/png",
content: Buffer.alloc(0),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
// Empty files are skipped, resulting in "No valid files uploaded"
expect(res.statusCode).toBe(400);
});
it("handles a file with only null bytes", async () => {
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: "nulls.png",
contentType: "image/png",
content: Buffer.alloc(1024, 0),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("handles concurrent requests without corruption", async () => {
const requests = Array.from({ length: 5 }, (_, i) => {
const { body: payload, contentType } = createMultipartPayload([
{
name: "file",
filename: `concurrent-${i}.png`,
contentType: "image/png",
content: PNG_1x1,
},
{ name: "settings", content: JSON.stringify({ width: 1 }) },
]);
return app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
});
const results = await Promise.all(requests);
for (const res of results) {
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.jobId).toBeDefined();
}
// Verify all jobIds are unique
const jobIds = results.map((r) => JSON.parse(r.body).jobId);
expect(new Set(jobIds).size).toBe(5);
});
it("rejects a JSON body on multipart-only endpoint", async () => {
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": "application/json",
},
payload: { width: 100 },
});
expect(res.statusCode).toBe(400);
});
it("handles extremely long tool settings values gracefully", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "img.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify({ width: 1, fit: "a".repeat(10_000) }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
// Zod should reject the invalid enum value
expect(res.statusCode).toBe(400);
});
it("handles type confusion in settings (string where number expected)", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "img.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify({ width: "not-a-number" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(res.statusCode).toBe(400);
});
it("handles array where object expected in login body", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
headers: { "content-type": "application/json" },
payload: JSON.stringify(["admin", "Adminpass1"]),
});
expect(res.statusCode).toBe(400);
});
it("handles deeply nested JSON in settings", async () => {
// Create a deeply nested object
let nested: Record<string, unknown> = { value: 1 };
for (let i = 0; i < 50; i++) {
nested = { nested };
}
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "img.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify(nested) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
// Zod schema only cares about the expected keys; extra nesting is ignored.
// Sharp may fail (422) if no valid resize dimensions are derived.
expect([200, 400, 422]).toContain(res.statusCode);
});
it("handles NaN/Infinity in numeric settings", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "img.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: '{"width": NaN}' },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
// NaN is not valid JSON, so it should fail to parse
expect(res.statusCode).toBe(400);
});
it("rejects double-encoded path traversal in download", async () => {
const res = await app.inject({
method: "GET",
url: "/api/v1/download/%252e%252e%252f%252e%252e%252fetc/passwd",
});
// Even after double decoding, path traversal should be blocked or 404
expect([400, 404]).toContain(res.statusCode);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// WORKSPACE INTEGRITY
// ═══════════════════════════════════════════════════════════════════════════
describe("Workspace integrity", () => {
it("upload creates workspace with input dir", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "ws-test.png", contentType: "image/png", content: PNG_1x1 },
]);
const uploadRes = await app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(uploadRes.statusCode).toBe(200);
const { jobId } = JSON.parse(uploadRes.body);
// The uploaded file should be downloadable from the workspace
const downloadRes = await app.inject({
method: "GET",
url: `/api/v1/download/${jobId}/ws-test.png`,
});
expect(downloadRes.statusCode).toBe(200);
});
it("tool processing creates both input and output in workspace", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "ws-tool.png", contentType: "image/png", content: PNG_200x150 },
{ name: "settings", content: JSON.stringify({ width: 50 }) },
]);
const toolRes = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
expect(toolRes.statusCode).toBe(200);
const { jobId, downloadUrl } = JSON.parse(toolRes.body);
// Output file is downloadable
const outputRes = await app.inject({
method: "GET",
url: downloadUrl,
});
expect(outputRes.statusCode).toBe(200);
// Input file is also saved in workspace (from tool-factory.ts)
const inputRes = await app.inject({
method: "GET",
url: `/api/v1/download/${jobId}/ws-tool.png`,
});
expect(inputRes.statusCode).toBe(200);
});
it("each job gets a unique workspace (no cross-contamination)", async () => {
const makeReq = () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "isolated.png", contentType: "image/png", content: PNG_1x1 },
]);
return app.inject({
method: "POST",
url: "/api/v1/upload",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
payload,
});
};
const [res1, res2] = await Promise.all([makeReq(), makeReq()]);
const jobId1 = JSON.parse(res1.body).jobId;
const jobId2 = JSON.parse(res2.body).jobId;
expect(jobId1).not.toBe(jobId2);
});
});