diff --git a/tests/integration/bulk-rename.test.ts b/tests/integration/bulk-rename.test.ts index 40fa06ee..0cc55ca5 100644 --- a/tests/integration/bulk-rename.test.ts +++ b/tests/integration/bulk-rename.test.ts @@ -15,6 +15,9 @@ const FIXTURES = join(__dirname, "..", "fixtures"); const PNG = readFileSync(join(FIXTURES, "test-200x150.png")); const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg")); const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp")); +const TINY_PNG = readFileSync(join(FIXTURES, "test-1x1.png")); +const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); +const GIF = readFileSync(join(FIXTURES, "animated.gif")); /** Extract sorted filenames from a ZIP buffer. */ function zipEntryNames(buf: Buffer): string[] { @@ -614,4 +617,161 @@ describe("Bulk Rename", () => { expect(filenames).toHaveLength(1); expect(filenames[0]).toBe("solo-1.png"); }); + + // ── HEIF file handling ───────────────────────────────────────── + + it("renames HEIF files preserving extension", async () => { + const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF }, + { name: "settings", content: JSON.stringify({ pattern: "renamed-{{index}}" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/bulk-rename", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const filenames = zipEntryNames(res.rawPayload); + expect(filenames).toContain("renamed-1.heif"); + }); + + // ── Large file handling ──────────────────────────────────────── + + it("renames a large stress image preserving content", async () => { + const LARGE = readFileSync(join(FIXTURES, "content", "stress-large.jpg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "big.jpg", contentType: "image/jpeg", content: LARGE }, + { name: "settings", content: JSON.stringify({ pattern: "large-{{index}}" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/bulk-rename", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const filenames = zipEntryNames(res.rawPayload); + expect(filenames).toContain("large-1.jpg"); + + const zip = new AdmZip(res.rawPayload); + const entry = zip.getEntry("large-1.jpg"); + expect(entry).not.toBeNull(); + expect(entry!.getData().length).toBe(LARGE.length); + }); + + // ── Tiny file handling ───────────────────────────────────────── + + it("renames a 1x1 pixel image", async () => { + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "tiny.png", contentType: "image/png", content: TINY_PNG }, + { name: "settings", content: JSON.stringify({ pattern: "small-{{index}}" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/bulk-rename", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const filenames = zipEntryNames(res.rawPayload); + expect(filenames).toContain("small-1.png"); + }); + + // ── SVG file handling ────────────────────────────────────────── + + it("renames SVG files preserving extension", async () => { + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG }, + { name: "settings", content: JSON.stringify({ pattern: "vector-{{index}}" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/bulk-rename", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const filenames = zipEntryNames(res.rawPayload); + expect(filenames).toContain("vector-1.svg"); + }); + + // ── Animated GIF handling ────────────────────────────────────── + + it("renames animated GIF files preserving extension", async () => { + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "animation.gif", contentType: "image/gif", content: GIF }, + { name: "settings", content: JSON.stringify({ pattern: "anim-{{index}}" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/bulk-rename", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const filenames = zipEntryNames(res.rawPayload); + expect(filenames).toContain("anim-1.gif"); + }); + + // ── Mixed format batch (5+ files) ───────────────────────────── + + it("renames a batch of 5+ mixed format files", async () => { + const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "a.png", contentType: "image/png", content: PNG }, + { name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG }, + { name: "file", filename: "c.webp", contentType: "image/webp", content: WEBP }, + { name: "file", filename: "d.gif", contentType: "image/gif", content: GIF }, + { name: "file", filename: "e.svg", contentType: "image/svg+xml", content: SVG }, + { name: "file", filename: "f.heic", contentType: "image/heic", content: HEIC }, + { name: "settings", content: JSON.stringify({ pattern: "batch-{{padded}}" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/bulk-rename", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + expect(res.statusCode).toBe(200); + const filenames = zipEntryNames(res.rawPayload); + expect(filenames).toHaveLength(6); + expect(filenames).toContain("batch-1.png"); + expect(filenames).toContain("batch-2.jpg"); + expect(filenames).toContain("batch-3.webp"); + expect(filenames).toContain("batch-4.gif"); + expect(filenames).toContain("batch-5.svg"); + expect(filenames).toContain("batch-6.heic"); + }); }); diff --git a/tests/integration/image-enhancement.test.ts b/tests/integration/image-enhancement.test.ts index 8aee5819..6c1cb973 100644 --- a/tests/integration/image-enhancement.test.ts +++ b/tests/integration/image-enhancement.test.ts @@ -17,6 +17,8 @@ const PNG = readFileSync(join(FIXTURES, "test-200x150.png")); const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg")); const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp")); const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic")); +const SVG = readFileSync(join(FIXTURES, "test-100x100.svg")); +const GIF = readFileSync(join(FIXTURES, "animated.gif")); let testApp: TestApp; let app: TestApp["app"]; @@ -583,6 +585,57 @@ describe("Document mode variations", () => { }); }); +// ── Authentication ────────────────────────────────────────────── +describe("Authentication", () => { + it("returns 401 for unauthenticated request", async () => { + const { body: payload, contentType } = makePayload({ mode: "auto" }); + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/image-enhancement", + payload, + headers: { "content-type": contentType }, + }); + expect(res.statusCode).toBe(401); + }); +}); + +// ── HEIF input ───────────────────────────────────────────────── +describe("HEIF input", () => { + it("enhances HEIF (sample.heif) input", async () => { + const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif")); + const res = await postTool({ mode: "auto" }, HEIF, "sample.heif", "image/heif"); + expect([200, 422]).toContain(res.statusCode); + if (res.statusCode === 200) { + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + } + }); +}); + +// ── SVG input ────────────────────────────────────────────────── +describe("SVG input", () => { + it("enhances SVG input after rasterization", async () => { + const res = await postTool({ mode: "auto" }, SVG, "test.svg", "image/svg+xml"); + expect([200, 400, 422]).toContain(res.statusCode); + if (res.statusCode === 200) { + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + } + }); +}); + +// ── Animated GIF input ───────────────────────────────────────── +describe("Animated GIF input", () => { + it("enhances animated GIF input", async () => { + const res = await postTool({ mode: "auto" }, GIF, "animated.gif", "image/gif"); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); +}); + // ── Selective correction combinations ─────────────────────────── describe("Selective correction edge cases", () => { it("enables only contrast and white balance", async () => { diff --git a/tests/integration/info.test.ts b/tests/integration/info.test.ts index 2ffd5b41..0d4b5df4 100644 --- a/tests/integration/info.test.ts +++ b/tests/integration/info.test.ts @@ -587,6 +587,33 @@ describe("Info", () => { expect(typeof result.hasXmp).toBe("boolean"); }); + // ── HEIF format info ────────────────────────────────────────── + + it("returns correct metadata for HEIF (sample.heif) image", async () => { + const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "sample.heif", contentType: "image/heif", content: HEIF }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/info", + headers: { + authorization: `Bearer ${adminToken}`, + "content-type": contentType, + }, + body, + }); + + // HEIF might need system decoder -- skip if 422 + if (res.statusCode === 422) return; + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.width).toBeGreaterThan(0); + expect(result.height).toBeGreaterThan(0); + expect(result.filename).toBe("sample.heif"); + }); + // ── Alpha channel detection ───────────────────────────────────── it("detects alpha channel in PNG with transparency", async () => { diff --git a/tests/integration/optimize-for-web.test.ts b/tests/integration/optimize-for-web.test.ts index 3950f88e..44747d33 100644 --- a/tests/integration/optimize-for-web.test.ts +++ b/tests/integration/optimize-for-web.test.ts @@ -723,6 +723,47 @@ describe("Preview endpoint format variations", () => { }); }); +// ── Unauthenticated request ──────────────────────────────────── +describe("Authentication", () => { + it("returns 401 for unauthenticated request", async () => { + const { body: payload, contentType } = makePayload({ format: "webp" }); + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/optimize-for-web", + payload, + headers: { "content-type": contentType }, + }); + expect(res.statusCode).toBe(401); + }); +}); + +// ── HEIF input handling ─────────────────────────────────────── +describe("HEIF input", () => { + it("optimizes HEIF (sample.heif) input to webp", async () => { + const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif")); + const res = await postTool({ format: "webp" }, HEIF, "sample.heif", "image/heif"); + // HEIF decode may not be available + expect([200, 422]).toContain(res.statusCode); + if (res.statusCode === 200) { + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.downloadUrl).toContain(".webp"); + } + }); +}); + +// ── Animated GIF input ──────────────────────────────────────── +describe("Animated GIF input", () => { + it("optimizes animated GIF to webp", async () => { + const GIF = readFileSync(join(FIXTURES, "animated.gif")); + const res = await postTool({ format: "webp" }, GIF, "animated.gif", "image/gif"); + expect(res.statusCode).toBe(200); + const result = JSON.parse(res.body); + expect(result.downloadUrl).toBeDefined(); + expect(result.processedSize).toBeGreaterThan(0); + }); +}); + // ── File with no extension ────────────────────────────────────── describe("File naming edge cases", () => { it("handles file without extension", async () => { diff --git a/tests/integration/vectorize.test.ts b/tests/integration/vectorize.test.ts index 149f0c46..4298b62c 100644 --- a/tests/integration/vectorize.test.ts +++ b/tests/integration/vectorize.test.ts @@ -699,4 +699,74 @@ describe("vectorize", () => { expect(res.statusCode).toBe(200); }); + + // ── HEIF input ─────────────────────────────────────────────── + + it("works with HEIF (sample.heif) input after decoding", async () => { + const HEIF = readFileSync(join(FIXTURES, "formats", "sample.heif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "sample.heif", contentType: "image/heif", content: HEIF }, + { name: "settings", content: JSON.stringify({}) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/vectorize", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + // HEIF may fail if system decoder missing + expect([200, 422]).toContain(res.statusCode); + if (res.statusCode === 200) { + const json = JSON.parse(res.body); + expect(json.downloadUrl).toMatch(/\.svg$/); + } + }); + + // ── SVG input ──────────────────────────────────────────────── + + it("vectorizes SVG input (re-traces after rasterization)", async () => { + const SVG_BUF = readFileSync(join(FIXTURES, "test-100x100.svg")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "test.svg", contentType: "image/svg+xml", content: SVG_BUF }, + { name: "settings", content: JSON.stringify({ colorMode: "bw" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/vectorize", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + // SVG may need rasterization first; accept success or processing error + expect([200, 422]).toContain(res.statusCode); + if (res.statusCode === 200) { + const json = JSON.parse(res.body); + expect(json.downloadUrl).toMatch(/\.svg$/); + } + }); + + // ── Animated GIF input ─────────────────────────────────────── + + it("vectorizes animated GIF input", async () => { + const GIF_BUF = readFileSync(join(FIXTURES, "animated.gif")); + const { body, contentType } = createMultipartPayload([ + { name: "file", filename: "animated.gif", contentType: "image/gif", content: GIF_BUF }, + { name: "settings", content: JSON.stringify({ colorMode: "bw" }) }, + ]); + + const res = await app.inject({ + method: "POST", + url: "/api/v1/tools/vectorize", + headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType }, + body, + }); + + expect(res.statusCode).toBe(200); + const json = JSON.parse(res.body); + expect(json.downloadUrl).toMatch(/\.svg$/); + expect(json.processedSize).toBeGreaterThan(0); + }); });