/** * Integration tests for the content-aware resize (seam carving via caire) API endpoint. * * This tool uses the caire Go binary. In environments where caire is not * installed the route will return 422. Tests gracefully handle both scenarios * while still verifying route existence and input validation. */ 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_200x150 = readFileSync(join(FIXTURES, "test-200x150.png")); const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic")); const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg")); 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("Content-Aware Resize", () => { it("route exists and responds to POST", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ width: 150, height: 120, protectFaces: false }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); // 200 = caire available, 422 = caire not found expect([200, 422]).toContain(res.statusCode); }, 60_000); it("rejects requests without a file", async () => { const { body, contentType } = createMultipartPayload([ { name: "settings", content: JSON.stringify({ width: 150 }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect(res.statusCode).toBe(400); }); it("rejects requests without width, height, or square", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ protectFaces: false }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect(res.statusCode).toBe(400); const resBody = JSON.parse(res.body); expect(resBody.error).toContain("width, height, or square"); }); it("processes with only width specified", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ width: 150, protectFaces: false }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); // Accept 200 (caire available) or 422 (caire not available) expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const resBody = JSON.parse(res.body); expect(resBody.downloadUrl).toBeDefined(); expect(resBody.width).toBe(150); } }, 60_000); it("processes with only height specified", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ height: 120, protectFaces: false }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const resBody = JSON.parse(res.body); expect(resBody.downloadUrl).toBeDefined(); expect(resBody.height).toBe(120); } }, 60_000); it("supports enlargement via seam insertion", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ width: 300, protectFaces: false }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); // 200 = caire enlarged successfully, 422 = caire not available expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const resBody = JSON.parse(res.body); expect(resBody.downloadUrl).toBeDefined(); expect(resBody.width).toBe(300); } }, 60_000); it("accepts blurRadius and sobelThreshold options", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ width: 150, protectFaces: false, blurRadius: 6, sobelThreshold: 4, }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const resBody = JSON.parse(res.body); expect(resBody.downloadUrl).toBeDefined(); expect(resBody.width).toBe(150); } }, 60_000); it("supports square mode", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ square: true, protectFaces: false }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const resBody = JSON.parse(res.body); expect(resBody.downloadUrl).toBeDefined(); // Square mode produces equal width and height expect(resBody.width).toBe(resBody.height); } }, 60_000); it("rejects invalid blurRadius", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ width: 150, blurRadius: 50 }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect(res.statusCode).toBe(400); }); it("rejects invalid sobelThreshold", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ width: 150, sobelThreshold: 0 }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect(res.statusCode).toBe(400); }); it("rejects invalid settings JSON", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: "not-json{{{" }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", 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("rejects invalid image data", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "bad.png", contentType: "image/png", content: Buffer.from("not an image"), }, { name: "settings", content: JSON.stringify({ width: 150 }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect(res.statusCode).toBe(400); const result = JSON.parse(res.body); expect(result.error).toMatch(/invalid image/i); }); it("handles HEIC input (decodes before processing)", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.heic", contentType: "image/heic", content: HEIC }, { name: "settings", content: JSON.stringify({ width: 150, protectFaces: false }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); // HEIC decode or caire may not be available expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const resBody = JSON.parse(res.body); expect(resBody.downloadUrl).toBeDefined(); } }, 60_000); it("processes both width and height simultaneously", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ width: 120, height: 100, protectFaces: false }), }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const resBody = JSON.parse(res.body); expect(resBody.downloadUrl).toBeDefined(); expect(resBody.width).toBe(120); expect(resBody.height).toBe(100); } }, 60_000); it("returns expected response fields on success", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG_200x150 }, { name: "settings", content: JSON.stringify({ width: 150, protectFaces: false }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const resBody = JSON.parse(res.body); expect(resBody).toHaveProperty("jobId"); expect(resBody).toHaveProperty("downloadUrl"); expect(resBody).toHaveProperty("originalSize"); expect(resBody).toHaveProperty("processedSize"); expect(resBody).toHaveProperty("width"); expect(resBody).toHaveProperty("height"); expect(resBody.originalSize).toBeGreaterThan(0); expect(resBody.processedSize).toBeGreaterThan(0); } }, 60_000); it("returns 400 when file is empty", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "empty.png", contentType: "image/png", content: Buffer.alloc(0) }, { name: "settings", content: JSON.stringify({ width: 150 }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect(res.statusCode).toBe(400); const result = JSON.parse(res.body); expect(result.error).toMatch(/no image/i); }); it("processes JPEG input", async () => { const { body, contentType } = createMultipartPayload([ { name: "file", filename: "test.jpg", contentType: "image/jpeg", content: JPG }, { name: "settings", content: JSON.stringify({ width: 80, protectFaces: false }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/content-aware-resize", headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType, }, body, }); expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const resBody = JSON.parse(res.body); expect(resBody.downloadUrl).toBeDefined(); } }, 60_000); });