diff --git a/tests/integration/barcode-read.test.ts b/tests/integration/barcode-read.test.ts index 54a7c2f0..4d401456 100644 --- a/tests/integration/barcode-read.test.ts +++ b/tests/integration/barcode-read.test.ts @@ -826,4 +826,76 @@ describe("Barcode Read", () => { expect(Array.isArray(result.barcodes)).toBe(true); // May or may not detect at very small size, but should not error }); + + // ── HEIF format input ───────────────────────────────────────────── + + it("reads barcodes from a HEIF image", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/barcode-read", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.filename).toBe("photo.heif"); + expect(Array.isArray(result.barcodes)).toBe(true); + }); + + // ── Animated GIF input ──────────────────────────────────────────── + + it("reads barcodes from an animated GIF", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/barcode-read", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.filename).toBe("anim.gif"); + expect(Array.isArray(result.barcodes)).toBe(true); + }); + + // ── SVG input ───────────────────────────────────────────────────── + + it("reads barcodes from an SVG image", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/barcode-read", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.filename).toBe("icon.svg"); + expect(Array.isArray(result.barcodes)).toBe(true); + }); }); diff --git a/tests/integration/collage.test.ts b/tests/integration/collage.test.ts index 0e17e6c3..a9935014 100644 --- a/tests/integration/collage.test.ts +++ b/tests/integration/collage.test.ts @@ -1454,4 +1454,91 @@ describe("Collage", () => { const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); }); + + // ── HEIF format input ───────────────────────────────────────────── + + it("handles HEIF input images", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "f1", filename: "a.heif", contentType: "image/heif", content: HEIF }, + { name: "f2", filename: "b.jpg", contentType: "image/jpeg", content: JPG }, + { + name: "settings", + content: JSON.stringify({ templateId: "2-h-equal" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/collage", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); + + // ── Animated GIF input ──────────────────────────────────────────── + + it("handles animated GIF input images", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "f1", filename: "a.gif", contentType: "image/gif", content: GIF }, + { name: "f2", filename: "b.jpg", contentType: "image/jpeg", content: JPG }, + { + name: "settings", + content: JSON.stringify({ templateId: "2-h-equal" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/collage", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); + + // ── SVG input ───────────────────────────────────────────────────── + + it("handles SVG input images in collage", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "f1", filename: "icon.svg", contentType: "image/svg+xml", content: SVG }, + { name: "f2", filename: "b.jpg", contentType: "image/jpeg", content: JPG }, + { + name: "settings", + content: JSON.stringify({ templateId: "2-h-equal" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/collage", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); }); diff --git a/tests/integration/compare.test.ts b/tests/integration/compare.test.ts index 552acbaa..2eadc990 100644 --- a/tests/integration/compare.test.ts +++ b/tests/integration/compare.test.ts @@ -773,4 +773,84 @@ describe("Compare", () => { const result = JSON.parse(res.body); expect(result.similarity).toBe(100); }); + + // ── HEIF format input ───────────────────────────────────────────── + + it("compares HEIF image with PNG", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "a.heif", contentType: "image/heif", content: HEIF }, + { name: "file", filename: "b.png", contentType: "image/png", content: PNG }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/compare", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.similarity).toBeGreaterThanOrEqual(0); + expect(result.similarity).toBeLessThanOrEqual(100); + expect(result.downloadUrl).toBeDefined(); + expect(result.dimensions.width).toBeGreaterThan(0); + expect(result.dimensions.height).toBeGreaterThan(0); + }); + + // ── Animated GIF input ──────────────────────────────────────────── + + it("compares animated GIF with PNG", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "a.gif", contentType: "image/gif", content: GIF }, + { name: "file", filename: "b.png", contentType: "image/png", content: PNG }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/compare", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.similarity).toBeGreaterThanOrEqual(0); + expect(result.similarity).toBeLessThanOrEqual(100); + expect(result.downloadUrl).toBeDefined(); + }); + + // ── SVG input ───────────────────────────────────────────────────── + + it("compares SVG image with PNG", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "a.svg", contentType: "image/svg+xml", content: SVG }, + { name: "file", filename: "b.png", contentType: "image/png", content: PNG }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/compare", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.similarity).toBeGreaterThanOrEqual(0); + expect(result.similarity).toBeLessThanOrEqual(100); + expect(result.downloadUrl).toBeDefined(); + }); }); diff --git a/tests/integration/compose.test.ts b/tests/integration/compose.test.ts index 18c987df..1d406b39 100644 --- a/tests/integration/compose.test.ts +++ b/tests/integration/compose.test.ts @@ -1009,4 +1009,83 @@ describe("Compose", () => { const result = JSON.parse(res.body); expect(result.error).toMatch(/processing failed/i); }); + + // ── HEIF format input ───────────────────────────────────────────── + + it("handles HEIF base image", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "base.heif", contentType: "image/heif", content: HEIF }, + { name: "overlay", filename: "overlay.jpg", contentType: "image/jpeg", content: JPG }, + { name: "settings", content: JSON.stringify({ x: 10, y: 10 }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/compose", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); + + // ── Animated GIF input ──────────────────────────────────────────── + + it("handles animated GIF as base image", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const TINY = readFileSync(join(FIXTURES, "test-1x1.png")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "base.gif", contentType: "image/gif", content: GIF }, + { name: "overlay", filename: "overlay.png", contentType: "image/png", content: TINY }, + { name: "settings", content: JSON.stringify({ x: 0, y: 0 }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/compose", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); + + // ── SVG input ───────────────────────────────────────────────────── + + it("handles SVG as overlay image", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "base.png", contentType: "image/png", content: PNG }, + { name: "overlay", filename: "overlay.svg", contentType: "image/svg+xml", content: SVG }, + { name: "settings", content: JSON.stringify({ x: 10, y: 10 }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/compose", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); }); diff --git a/tests/integration/find-duplicates.test.ts b/tests/integration/find-duplicates.test.ts index 96ede266..f946444d 100644 --- a/tests/integration/find-duplicates.test.ts +++ b/tests/integration/find-duplicates.test.ts @@ -962,4 +962,80 @@ describe("Find Duplicates", () => { // Should be roughly 2x the file size of a single PNG expect(result.spaceSaveable).toBeGreaterThan(PNG.length); }); + + // ── HEIF format input ───────────────────────────────────────────── + + it("handles HEIF input images in duplicate detection", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "a.heif", contentType: "image/heif", content: HEIF }, + { name: "file", filename: "b.heif", contentType: "image/heif", content: HEIF }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/find-duplicates", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.totalImages).toBe(2); + expect(result.duplicateGroups).toHaveLength(1); + expect(result.duplicateGroups[0].files).toHaveLength(2); + }, 60_000); + + // ── Animated GIF input ──────────────────────────────────────────── + + it("handles animated GIF input in duplicate detection", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "a.gif", contentType: "image/gif", content: GIF }, + { name: "file", filename: "b.gif", contentType: "image/gif", content: GIF }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/find-duplicates", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.totalImages).toBe(2); + expect(result.duplicateGroups).toHaveLength(1); + }); + + // ── SVG input ───────────────────────────────────────────────────── + + it("handles SVG input in duplicate detection", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "a.svg", contentType: "image/svg+xml", content: SVG }, + { name: "file", filename: "b.svg", contentType: "image/svg+xml", content: SVG }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/find-duplicates", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.totalImages).toBe(2); + expect(result.duplicateGroups).toHaveLength(1); + }); }); diff --git a/tests/integration/split.test.ts b/tests/integration/split.test.ts index 418aa2db..26c1e214 100644 --- a/tests/integration/split.test.ts +++ b/tests/integration/split.test.ts @@ -1386,4 +1386,101 @@ describe("Split", () => { expect(res.statusCode).toBe(200); }); + + // ── HEIF format input ───────────────────────────────────────────── + + it("splits a HEIF input image", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF }, + { + name: "settings", + content: JSON.stringify({ columns: 2, rows: 2, outputFormat: "png" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/split", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const zip = new AdmZip(res.rawPayload); + const entries = zip.getEntries(); + expect(entries.length).toBe(4); + for (const entry of entries) { + expect(entry.entryName).toMatch(/\.png$/); + const meta = await sharp(entry.getData()).metadata(); + expect(meta.format).toBe("png"); + expect(meta.width).toBeGreaterThan(0); + expect(meta.height).toBeGreaterThan(0); + } + }); + + // ── Animated GIF input ──────────────────────────────────────────── + + it("splits an animated GIF input image", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF }, + { + name: "settings", + content: JSON.stringify({ columns: 2, rows: 2, outputFormat: "png" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/split", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const zip = new AdmZip(res.rawPayload); + const entries = zip.getEntries(); + expect(entries.length).toBe(4); + }); + + // ── SVG input ───────────────────────────────────────────────────── + + it("splits an SVG input image", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG }, + { + name: "settings", + content: JSON.stringify({ columns: 2, rows: 2, outputFormat: "png" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/split", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const zip = new AdmZip(res.rawPayload); + const entries = zip.getEntries(); + expect(entries.length).toBe(4); + for (const entry of entries) { + const meta = await sharp(entry.getData()).metadata(); + expect(meta.format).toBe("png"); + expect(meta.width).toBeGreaterThan(0); + expect(meta.height).toBeGreaterThan(0); + } + }); }); diff --git a/tests/integration/stitch.test.ts b/tests/integration/stitch.test.ts index a293c30c..4682d240 100644 --- a/tests/integration/stitch.test.ts +++ b/tests/integration/stitch.test.ts @@ -1485,6 +1485,93 @@ describe("Stitch", () => { expect(result.downloadUrl).toBeDefined(); }); + // ── HEIF format input ───────────────────────────────────────────── + + it("handles HEIF input in stitching", async () => { + const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "f1", filename: "a.heif", contentType: "image/heif", content: HEIF }, + { name: "f2", filename: "b.jpg", contentType: "image/jpeg", content: JPG }, + { + name: "settings", + content: JSON.stringify({ direction: "horizontal" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/stitch", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); + + // ── Animated GIF input ──────────────────────────────────────────── + + it("handles animated GIF input in stitching", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "f1", filename: "a.gif", contentType: "image/gif", content: GIF }, + { name: "f2", filename: "b.jpg", contentType: "image/jpeg", content: JPG }, + { + name: "settings", + content: JSON.stringify({ direction: "horizontal" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/stitch", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); + + // ── SVG input ───────────────────────────────────────────────────── + + it("handles SVG input in stitching", async () => { + const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "f1", filename: "icon.svg", contentType: "image/svg+xml", content: SVG }, + { name: "f2", filename: "b.jpg", contentType: "image/jpeg", content: JPG }, + { + name: "settings", + content: JSON.stringify({ direction: "horizontal" }), + }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/stitch", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); + // ── Branch coverage: vertical crop with very different sizes ─────── it("stitches vertically with crop mode using very different image sizes", async () => {