mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Comprehensively adds { timeout: 120_000 } to every HEIF/HEIC
integration test across 40 test files. HEIF decoding on CI runners
is significantly slower than local, causing sporadic timeouts at
the default 5s or previous 60s limits.
406 lines
12 KiB
TypeScript
406 lines
12 KiB
TypeScript
/**
|
|
* Integration tests for the remove-background tool (/api/v1/tools/remove-background).
|
|
*
|
|
* This tool requires the Python sidecar (rembg). Tests accept both 200
|
|
* (sidecar running) and 501 (not installed) for the processing path while
|
|
* fully testing validation paths that don't depend on the sidecar.
|
|
*
|
|
* Also covers the /effects sub-route for Phase 2 compositing.
|
|
*/
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp } from "./test-server.js";
|
|
|
|
const FIXTURES = join(__dirname, "..", "fixtures");
|
|
const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
|
|
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
|
|
const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic"));
|
|
const TINY = readFileSync(join(FIXTURES, "test-1x1.png"));
|
|
|
|
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);
|
|
|
|
describe("Remove Background", () => {
|
|
// ── Phase 1: Processing (AI-dependent) ───────────────────────────
|
|
|
|
it("route exists and responds to POST", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
|
{ name: "settings", content: JSON.stringify({}) },
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([202, 501]).toContain(res.statusCode);
|
|
}, 60_000);
|
|
|
|
it("accepts default settings", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
|
{ name: "settings", content: JSON.stringify({}) },
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([202, 501]).toContain(res.statusCode);
|
|
|
|
if (res.statusCode === 202) {
|
|
const result = JSON.parse(res.body);
|
|
expect(result.jobId).toBeDefined();
|
|
expect(result.async).toBe(true);
|
|
}
|
|
|
|
if (res.statusCode === 501) {
|
|
const result = JSON.parse(res.body);
|
|
expect(result.code).toBe("FEATURE_NOT_INSTALLED");
|
|
}
|
|
}, 60_000);
|
|
|
|
it("accepts transparent backgroundType", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
|
{
|
|
name: "settings",
|
|
content: JSON.stringify({ backgroundType: "transparent" }),
|
|
},
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([202, 501]).toContain(res.statusCode);
|
|
}, 60_000);
|
|
|
|
it("accepts color background with blur and shadow settings", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
|
{
|
|
name: "settings",
|
|
content: JSON.stringify({
|
|
backgroundType: "color",
|
|
backgroundColor: "#FF0000",
|
|
blurEnabled: true,
|
|
blurIntensity: 50,
|
|
shadowEnabled: true,
|
|
shadowOpacity: 60,
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([202, 501]).toContain(res.statusCode);
|
|
}, 60_000);
|
|
|
|
it("accepts gradient background settings", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
|
{
|
|
name: "settings",
|
|
content: JSON.stringify({
|
|
backgroundType: "gradient",
|
|
gradientColor1: "#FF0000",
|
|
gradientColor2: "#0000FF",
|
|
gradientAngle: 45,
|
|
}),
|
|
},
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([202, 501]).toContain(res.statusCode);
|
|
}, 60_000);
|
|
|
|
it("processes JPEG input", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "photo.jpg", contentType: "image/jpeg", content: JPG },
|
|
{ name: "settings", content: JSON.stringify({}) },
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([202, 501]).toContain(res.statusCode);
|
|
}, 60_000);
|
|
|
|
it(
|
|
"handles HEIC input",
|
|
{ timeout: 120_000 },
|
|
async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "photo.heic", contentType: "image/heic", content: HEIC },
|
|
{ name: "settings", content: JSON.stringify({}) },
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([202, 501]).toContain(res.statusCode);
|
|
},
|
|
60_000,
|
|
);
|
|
|
|
it("handles 1x1 pixel input", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "tiny.png", contentType: "image/png", content: TINY },
|
|
{ name: "settings", content: JSON.stringify({}) },
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([202, 422, 501]).toContain(res.statusCode);
|
|
}, 60_000);
|
|
|
|
// ── Phase 2: Effects sub-route ───────────────────────────────────
|
|
|
|
it("effects route rejects missing settings", async () => {
|
|
const { body, contentType } = createMultipartPayload([]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background/effects",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
const result = JSON.parse(res.body);
|
|
expect(result.error).toMatch(/no settings/i);
|
|
});
|
|
|
|
it("effects route rejects invalid settings JSON", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "settings", content: "not json{{" },
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background/effects",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
const result = JSON.parse(res.body);
|
|
expect(result.error).toMatch(/json/i);
|
|
});
|
|
|
|
it("effects route rejects settings without jobId", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{
|
|
name: "settings",
|
|
content: JSON.stringify({ filename: "test.png" }),
|
|
},
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background/effects",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(400);
|
|
const result = JSON.parse(res.body);
|
|
expect(result.error).toMatch(/invalid settings/i);
|
|
});
|
|
|
|
// ── Validation (always testable) ─────────────────────────────────
|
|
|
|
it("rejects requests without a file", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "settings", content: JSON.stringify({}) },
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([400, 501]).toContain(res.statusCode);
|
|
if (res.statusCode === 400) {
|
|
const result = JSON.parse(res.body);
|
|
expect(result.error).toMatch(/no image/i);
|
|
}
|
|
});
|
|
|
|
it("rejects invalid settings JSON", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
|
{ name: "settings", content: "not valid json{{{" },
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([400, 501]).toContain(res.statusCode);
|
|
if (res.statusCode === 400) {
|
|
const result = JSON.parse(res.body);
|
|
expect(result.error).toMatch(/json/i);
|
|
}
|
|
});
|
|
|
|
it("rejects invalid backgroundType", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
|
{
|
|
name: "settings",
|
|
content: JSON.stringify({ backgroundType: "sparkles" }),
|
|
},
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([400, 501]).toContain(res.statusCode);
|
|
if (res.statusCode === 400) {
|
|
const result = JSON.parse(res.body);
|
|
expect(result.error).toMatch(/invalid settings/i);
|
|
}
|
|
});
|
|
|
|
it("rejects blurIntensity out of range", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
|
{
|
|
name: "settings",
|
|
content: JSON.stringify({ blurIntensity: 200 }),
|
|
},
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: {
|
|
authorization: `Bearer ${adminToken}`,
|
|
"content-type": contentType,
|
|
},
|
|
body,
|
|
});
|
|
|
|
expect([400, 501]).toContain(res.statusCode);
|
|
if (res.statusCode === 400) {
|
|
const result = JSON.parse(res.body);
|
|
expect(result.error).toMatch(/invalid settings/i);
|
|
}
|
|
});
|
|
|
|
it("rejects unauthenticated requests", async () => {
|
|
const { body, contentType } = createMultipartPayload([
|
|
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
|
|
{ name: "settings", content: JSON.stringify({}) },
|
|
]);
|
|
|
|
const res = await app.inject({
|
|
method: "POST",
|
|
url: "/api/v1/tools/remove-background",
|
|
headers: { "content-type": contentType },
|
|
body,
|
|
});
|
|
|
|
expect(res.statusCode).toBe(401);
|
|
});
|
|
});
|