/** * Integration tests for the strip-metadata tool. * * Tests EXIF stripping, GPS stripping, ICC profile stripping, XMP stripping, * and the stripAll flag. Uses test-with-exif.jpg fixture and verifies that * metadata is actually removed from the output. */ import sharp from "sharp"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { fixtures, readFixture } from "../../../fixtures/index.js"; import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp, } from "../../test-server.js"; const EXIF_JPG = readFixture(fixtures.image.exifGps); const PNG = readFixture(fixtures.image.base.png200); 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); function makePayload( settings: Record, buffer: Buffer = EXIF_JPG, filename = "test-with-exif.jpg", contentType = "image/jpeg", ) { return createMultipartPayload([ { name: "file", filename, contentType, content: buffer }, { name: "settings", content: JSON.stringify(settings) }, ]); } async function postTool( settings: Record, buffer?: Buffer, filename?: string, ct?: string, ) { const { body: payload, contentType } = makePayload(settings, buffer, filename, ct); return app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); } // ── Verify fixture has metadata ─────────────────────────────────── describe("Fixture verification", () => { it("test-with-exif.jpg has EXIF data", async () => { const meta = await sharp(EXIF_JPG).metadata(); expect(meta.exif).toBeDefined(); expect(meta.exif?.length).toBeGreaterThan(0); }); }); // ── Strip all metadata ──────────────────────────────────────────── describe("Strip all metadata", () => { it("strips all metadata with stripAll=true (default)", async () => { const res = await postTool({ stripAll: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); expect(result.processedSize).toBeGreaterThan(0); // Download and verify EXIF is gone const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const meta = await sharp(dlRes.rawPayload).metadata(); // After stripping all, EXIF/ICC/XMP should be absent or empty expect(!meta.exif || meta.exif.length === 0).toBe(true); expect(!meta.icc || meta.icc.length === 0).toBe(true); expect(!meta.xmp || meta.xmp.length === 0).toBe(true); }); it("strips with default settings (stripAll defaults to true)", async () => { const res = await postTool({}); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); }); }); // ── Strip individual categories ─────────────────────────────────── describe("Selective stripping", () => { it("strips only EXIF data", async () => { const res = await postTool({ stripAll: false, stripExif: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); expect(result.processedSize).toBeGreaterThan(0); }); it("strips only GPS data", async () => { const res = await postTool({ stripAll: false, stripGps: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); }); it("strips only ICC profile", async () => { const res = await postTool({ stripAll: false, stripIcc: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); }); it("strips only XMP data", async () => { const res = await postTool({ stripAll: false, stripXmp: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); }); it("strips EXIF + GPS but keeps ICC", async () => { const res = await postTool({ stripAll: false, stripExif: true, stripGps: true, stripIcc: false, }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); }); }); // ── Inspect endpoint ────────────────────────────────────────────── describe("Inspect endpoint", () => { it("returns parsed metadata for image with EXIF", async () => { const { body: payload, contentType } = createMultipartPayload([ { name: "file", filename: "test-with-exif.jpg", contentType: "image/jpeg", content: EXIF_JPG, }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata/inspect", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.filename).toBeDefined(); expect(result.fileSize).toBeGreaterThan(0); // Should have parsed exif or at least have the filename expect(result.filename).toBe("test-with-exif.jpg"); }); it("returns 400 when no file is provided to inspect", async () => { const { body: payload, contentType } = createMultipartPayload([ { name: "other", content: "nothing" }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata/inspect", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(400); }); }); // ── Format preservation ─────────────────────────────────────────── describe("Format handling", () => { it("preserves JPEG format after stripping", async () => { const res = await postTool({ stripAll: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.format).toBe("jpeg"); }); it("processes PNG input", async () => { const res = await postTool({ stripAll: true }, PNG, "test.png", "image/png"); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.format).toBe("png"); }); it("preserves image dimensions after stripping", async () => { const originalMeta = await sharp(EXIF_JPG).metadata(); const res = await postTool({ stripAll: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.width).toBe(originalMeta.width); expect(meta.height).toBe(originalMeta.height); }); }); // ── Selective stripping with download verification ──────────────── describe("Selective stripping with download verification", () => { it("selective stripExif returns valid downloadable image", async () => { const res = await postTool({ stripAll: false, stripExif: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); // Verify the output is a valid image const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.format).toBe("jpeg"); expect(meta.width).toBeGreaterThan(0); }); it("selective stripXmp returns valid downloadable image", async () => { const res = await postTool({ stripAll: false, stripXmp: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.format).toBe("jpeg"); }); it("selective stripIcc returns valid downloadable image", async () => { const res = await postTool({ stripAll: false, stripIcc: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.format).toBe("jpeg"); }); it("strips multiple categories but not all returns valid image", async () => { const res = await postTool({ stripAll: false, stripExif: true, stripGps: true, stripXmp: true, stripIcc: false, }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.format).toBe("jpeg"); expect(meta.width).toBeGreaterThan(0); }); it("with all strip flags false, preserves metadata", async () => { const res = await postTool({ stripAll: false, stripExif: false, stripGps: false, stripIcc: false, stripXmp: false, }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); // With withMetadata(), the output should retain EXIF const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.exif).toBeDefined(); }); }); // ── WebP input ────────────────────────────────────────────────── describe("WebP format handling", () => { it("processes WebP input and strips metadata", async () => { const webp = readFixture(fixtures.image.base.webp50); const res = await postTool({ stripAll: true }, webp, "test.webp", "image/webp"); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.format).toBe("webp"); }); }); // ── File size comparison ───────────────────────────────────────── describe("File size impact", () => { it("stripping metadata typically reduces or maintains file size", async () => { const res = await postTool({ stripAll: true }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); // After stripping, size should be positive expect(result.processedSize).toBeGreaterThan(0); }); }); // ── Inspect endpoint edge cases ───────────────────────────────── describe("Inspect endpoint edge cases", () => { it("inspects a PNG file with no EXIF", async () => { const { body: payload, contentType } = createMultipartPayload([ { name: "file", filename: "test.png", contentType: "image/png", content: PNG, }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata/inspect", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.filename).toBe("test.png"); expect(result.fileSize).toBeGreaterThan(0); // PNG without EXIF should not have exif property expect(result.exif).toBeUndefined(); }); }); // ── Error handling ──────────────────────────────────────────────── describe("Error handling", () => { it("returns 400 when no file is provided", async () => { const { body: payload, contentType } = createMultipartPayload([ { name: "settings", content: JSON.stringify({ stripAll: true }) }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(400); }); it("returns 400 for invalid settings JSON", async () => { const { body: payload, contentType } = createMultipartPayload([ { name: "file", filename: "test.jpg", contentType: "image/jpeg", content: EXIF_JPG }, { name: "settings", content: "not-json" }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(400); }); }); // ── Format-specific re-encoding paths ────���──────────────────── describe("Format-specific re-encoding", () => { it("re-encodes AVIF format after stripping", async () => { // Create a small AVIF buffer from PNG using Sharp const avifBuffer = await sharp(PNG).avif({ quality: 50 }).toBuffer(); const res = await postTool({ stripAll: true }, avifBuffer, "test.avif", "image/avif"); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); expect(result.processedSize).toBeGreaterThan(0); }); it("handles default format case (non-standard format)", async () => { // Using a GIF triggers the default case in the switch const gifBuffer = await sharp(PNG).gif().toBuffer(); const res = await postTool({ stripAll: true }, gifBuffer, "test.gif", "image/gif"); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); }); it("re-encodes TIFF format after stripping", async () => { const tiffBuffer = await sharp(PNG).tiff().toBuffer(); const res = await postTool({ stripAll: true }, tiffBuffer, "test.tiff", "image/tiff"); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); expect(result.processedSize).toBeGreaterThan(0); }); }); // ── Inspect endpoint: ICC profile parsing ────────────────────── describe("Inspect endpoint: ICC and XMP parsing", () => { it("inspect returns ICC info when present", async () => { const { body: payload, contentType } = createMultipartPayload([ { name: "file", filename: "test-with-exif.jpg", contentType: "image/jpeg", content: EXIF_JPG, }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata/inspect", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); // ICC may or may not be present in the fixture, but the response should be valid if (result.icc) { expect(typeof result.icc).toBe("object"); } }); it("inspect handles EXIF with GPS data", async () => { const { body: payload, contentType } = createMultipartPayload([ { name: "file", filename: "test-with-exif.jpg", contentType: "image/jpeg", content: EXIF_JPG, }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata/inspect", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.filename).toBe("test-with-exif.jpg"); // GPS may or may not be in fixture, but exif object should exist if (result.exif) { expect(typeof result.exif).toBe("object"); } }); it("inspect returns empty metadata for image without any metadata", async () => { const blankPng = readFixture(fixtures.image.edge.blank); const { body: payload, contentType } = createMultipartPayload([ { name: "file", filename: "blank.png", contentType: "image/png", content: blankPng, }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata/inspect", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.filename).toBe("blank.png"); expect(result.fileSize).toBeGreaterThan(0); // No EXIF, ICC, or XMP expected expect(result.exif).toBeUndefined(); expect(result.icc).toBeUndefined(); expect(result.xmp).toBeUndefined(); }); it("inspect handles empty file buffer", 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/tools/image/strip-metadata/inspect", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(400); const result = JSON.parse(res.body); expect(result.error).toMatch(/no image/i); }); it("inspect returns 422 for corrupt image that passes size check", async () => { // Build a buffer with enough bytes to pass the length check // but that Sharp cannot parse as valid image data const corruptBuffer = Buffer.alloc(512, 0xab); const { body: payload, contentType } = createMultipartPayload([ { name: "file", filename: "corrupt.jpg", contentType: "image/jpeg", content: corruptBuffer, }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata/inspect", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(422); const result = JSON.parse(res.body); expect(result.error).toMatch(/failed to read/i); }); }); // ── HEIC format handling ──────────────────────────────────────── describe("HEIC format handling", () => { it("strips metadata from HEIC input", { timeout: 120_000 }, async () => { const heic = readFixture(fixtures.image.base.heic200); const res = await postTool({ stripAll: true }, heic, "test.heic", "image/heic"); // HEIC 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.processedSize).toBeGreaterThan(0); } }); it("strips selective metadata from HEIC input", { timeout: 120_000 }, async () => { const heic = readFixture(fixtures.image.base.heic200); const res = await postTool( { stripAll: false, stripExif: true, stripGps: true }, heic, "photo.heic", "image/heic", ); expect([200, 422]).toContain(res.statusCode); if (res.statusCode === 200) { const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); } }); }); // ── Large file handling ──────────────────────────────────────── describe("Large file handling", () => { it("strips metadata from a large stress image", async () => { const large = readFixture(fixtures.image.stressLarge); const res = await postTool({ stripAll: true }, large, "stress-large.jpg", "image/jpeg"); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); expect(result.processedSize).toBeGreaterThan(0); }); }); // ── Tiny file handling ───────────────────────────────────────── describe("Tiny file handling", () => { it("strips metadata from a 1x1 pixel image", async () => { const tiny = readFixture(fixtures.image.edge.px1); const res = await postTool({ stripAll: true }, tiny, "tiny.png", "image/png"); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); expect(result.processedSize).toBeGreaterThan(0); }); it("inspects a 1x1 pixel image", async () => { const tiny = readFixture(fixtures.image.edge.px1); const { body: payload, contentType } = createMultipartPayload([ { name: "file", filename: "tiny.png", contentType: "image/png", content: tiny, }, ]); const res = await app.inject({ method: "POST", url: "/api/v1/tools/image/strip-metadata/inspect", payload, headers: { "content-type": contentType, authorization: `Bearer ${adminToken}`, }, }); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.filename).toBe("tiny.png"); expect(result.fileSize).toBeGreaterThan(0); }); }); // ── AVIF fixture re-encoding ───────────────────────────────────── describe("AVIF fixture re-encoding", () => { it("re-encodes real AVIF fixture after stripping", async () => { const avifFixture = readFixture(fixtures.image.formats("avif")); const res = await postTool({ stripAll: true }, avifFixture, "sample.avif", "image/avif"); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); expect(result.processedSize).toBeGreaterThan(0); const dlRes = await app.inject({ method: "GET", url: result.downloadUrl, headers: { authorization: `Bearer ${adminToken}` }, }); expect(dlRes.statusCode).toBe(200); const meta = await sharp(dlRes.rawPayload).metadata(); expect(meta.format).toBe("heif"); }); it("selectively strips EXIF from AVIF fixture", async () => { const avifFixture = readFixture(fixtures.image.formats("avif")); const res = await postTool( { stripAll: false, stripExif: true }, avifFixture, "sample.avif", "image/avif", ); expect(res.statusCode).toBe(200); const result = JSON.parse(res.body); expect(result.downloadUrl).toBeDefined(); }); });