Files
SnapOtter/tests/integration/compare.test.ts
T
SnapOtter cd24bb92b6 fix: update corrupted image test expectations from 422 to 400
validateImageBuffer catches corrupt image data before processing
reaches the tool handler, so the correct status code is 400 (bad
request) rather than 422 (processing failure). Also fix SVGZ
watermark validation by returning early for compressed SVG (Sharp
cannot read gzip-compressed SVGZ directly) and passing the actual
watermark filename to validateImageBuffer for correct format
detection.
2026-05-14 00:09:31 +08:00

966 lines
33 KiB
TypeScript

/**
* Integration tests for the compare tool (/api/v1/tools/compare).
*
* Covers identical-image comparison, different-image comparison,
* similarity score, diff image generation, and input validation.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import sharp from "sharp";
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 WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
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("Compare", () => {
it("reports 100% similarity when comparing an image with itself", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ 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).toBe(100);
expect(result.jobId).toBeDefined();
expect(result.downloadUrl).toBeDefined();
expect(result.dimensions).toBeDefined();
expect(result.dimensions.width).toBe(200);
expect(result.dimensions.height).toBe(150);
});
it("reports less than 100% similarity for different images", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
]);
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).toBeLessThan(100);
expect(result.similarity).toBeGreaterThanOrEqual(0);
expect(result.downloadUrl).toBeDefined();
});
it("generates a downloadable diff image", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
]);
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);
// Download the diff image
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
});
expect(dlRes.statusCode).toBe(200);
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.format).toBe("png");
expect(meta.width).toBe(result.dimensions.width);
expect(meta.height).toBe(result.dimensions.height);
});
it("uses the larger dimensions when comparing different-sized images", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "b.webp", contentType: "image/webp", content: WEBP },
]);
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);
// Max of (200, 50) = 200 wide; Max of (150, 50) = 150 tall
expect(result.dimensions.width).toBe(200);
expect(result.dimensions.height).toBe(150);
});
it("returns all expected fields in the response", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ 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).toHaveProperty("jobId");
expect(result).toHaveProperty("similarity");
expect(result).toHaveProperty("dimensions");
expect(result).toHaveProperty("downloadUrl");
expect(result).toHaveProperty("originalSize");
expect(result).toHaveProperty("processedSize");
expect(result.processedSize).toBeGreaterThan(0);
});
// ── Validation ──────────────────────────────────────────────────────
it("rejects requests with only one image", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.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(400);
const result = JSON.parse(res.body);
expect(result.error).toMatch(/two image/i);
});
it("rejects requests with no images", async () => {
const { body, contentType } = createMultipartPayload([{ name: "dummy", content: "nothing" }]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/compare",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
expect(res.statusCode).toBe(400);
const result = JSON.parse(res.body);
expect(result.error).toMatch(/two image/i);
});
it("rejects unauthenticated requests", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "b.png", contentType: "image/png", content: PNG },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/compare",
headers: { "content-type": contentType },
body,
});
expect(res.statusCode).toBe(401);
});
// ── Extended coverage: cross-format, HEIC, diff details ────────────
it("compares JPEG vs PNG (cross-format)", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", content: JPG },
{ 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).toBeLessThan(100);
expect(result.similarity).toBeGreaterThanOrEqual(0);
expect(result.dimensions.width).toBe(200);
expect(result.dimensions.height).toBe(150);
});
it("compares WebP vs JPEG (cross-format)", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.webp", contentType: "image/webp", content: WEBP },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
]);
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);
});
it("compares HEIC vs PNG", { timeout: 120_000 }, async () => {
const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.heic", contentType: "image/heic", content: HEIC },
{ 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.dimensions).toBeDefined();
});
it("compares two HEIC images (same file)", { timeout: 120_000 }, async () => {
const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.heic", contentType: "image/heic", content: HEIC },
{ name: "file", filename: "b.heic", contentType: "image/heic", content: HEIC },
]);
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).toBe(100);
});
it("diff image is always PNG format", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", content: JPG },
{ name: "file", filename: "b.webp", contentType: "image/webp", content: WEBP },
]);
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);
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
});
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.format).toBe("png");
});
it("similarity is rounded to 2 decimal places", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
]);
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);
const simStr = String(result.similarity);
const decimals = simStr.includes(".") ? simStr.split(".")[1].length : 0;
expect(decimals).toBeLessThanOrEqual(2);
});
it("compares two very small images (50x50)", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.webp", contentType: "image/webp", content: WEBP },
{ name: "file", filename: "b.webp", contentType: "image/webp", content: WEBP },
]);
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).toBe(100);
expect(result.dimensions.width).toBe(50);
expect(result.dimensions.height).toBe(50);
});
it("compares a portrait JPEG with a landscape PNG", async () => {
const PORTRAIT_JPG = readFileSync(join(FIXTURES, "test-portrait.jpg"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "portrait.jpg", contentType: "image/jpeg", content: PORTRAIT_JPG },
{ name: "file", filename: "landscape.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).toBeLessThan(100);
// Dimensions should be max of both
expect(result.dimensions.width).toBeGreaterThan(0);
expect(result.dimensions.height).toBeGreaterThan(0);
});
it("processedSize reflects the diff image file size", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
]);
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);
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
});
// processedSize should match the actual diff image buffer size
expect(result.processedSize).toBe(dlRes.rawPayload.length);
});
it("rejects requests with three or more images (only two allowed)", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "b.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "c.jpg", contentType: "image/jpeg", content: JPG },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/compare",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
// The route accepts the first two files, ignoring the third
// so this should succeed
expect(res.statusCode).toBe(200);
});
// ── Branch coverage: multipart parse error (lines 35-39) ────────────
it("returns 400 when corrupt image data fails validation", async () => {
// Create a buffer that looks like an image but corrupts Sharp
const corruptBuffer = Buffer.from("not a real image content at all");
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "corrupt.png", contentType: "image/png", content: corruptBuffer },
{ 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,
});
// validateImageBuffer catches corrupt data before processing
expect(res.statusCode).toBe(400);
const result = JSON.parse(res.body);
expect(result.error).toBeDefined();
});
// ── Branch coverage: 1x1 tiny images (line 117-121 area) ───────────
it("compares two 1x1 pixel images", async () => {
const TINY = readFileSync(join(FIXTURES, "test-1x1.png"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: TINY },
{ name: "file", filename: "b.png", contentType: "image/png", content: TINY },
]);
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).toBe(100);
expect(result.dimensions.width).toBe(1);
expect(result.dimensions.height).toBe(1);
});
it("compares a 1x1 image with a large image", async () => {
const TINY = readFileSync(join(FIXTURES, "test-1x1.png"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "tiny.png", contentType: "image/png", content: TINY },
{ name: "file", filename: "large.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);
// Max dimensions: 200x150
expect(result.dimensions.width).toBe(200);
expect(result.dimensions.height).toBe(150);
expect(result.similarity).toBeGreaterThanOrEqual(0);
expect(result.similarity).toBeLessThanOrEqual(100);
});
// ── Branch coverage: large file handling ────────────────────────────
it("compares a large stress image with a small image", async () => {
const LARGE = readFileSync(join(FIXTURES, "content", "stress-large.jpg"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "large.jpg", contentType: "image/jpeg", content: LARGE },
{ name: "file", filename: "small.jpg", contentType: "image/jpeg", content: JPG },
]);
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.dimensions.width).toBeGreaterThan(0);
expect(result.dimensions.height).toBeGreaterThan(0);
});
// ── Branch coverage: HEIC vs HEIC (portrait) ───────────────────────
it("compares HEIC portrait with standard HEIC", { timeout: 120_000 }, async () => {
const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic"));
const HEIC_PORTRAIT = readFileSync(join(FIXTURES, "test-portrait.heic"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "standard.heic", contentType: "image/heic", content: HEIC },
{
name: "file",
filename: "portrait.heic",
contentType: "image/heic",
content: HEIC_PORTRAIT,
},
]);
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();
});
// ── Branch coverage: originalSize reflects both inputs ──────────────
it("originalSize is sum of both input buffers", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
]);
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);
// originalSize is sum of both decoded buffers (after HEIC conversion)
// For non-HEIC, it should be close to input sizes
expect(result.originalSize).toBeGreaterThan(0);
});
// ── Branch coverage: blank image comparison ─────────────────────────
it("compares blank image with colored image", async () => {
const BLANK = readFileSync(join(FIXTURES, "test-blank.png"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "blank.png", contentType: "image/png", content: BLANK },
{ name: "file", filename: "colored.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).toBeLessThan(100);
});
// ── Branch coverage: both corrupt images fail processing ───────────
it("returns 400 when both images are corrupt", async () => {
const corrupt1 = Buffer.from("this is not an image");
const corrupt2 = Buffer.from("neither is this one");
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: corrupt1 },
{ name: "file", filename: "b.png", contentType: "image/png", content: corrupt2 },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/compare",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
// validateImageBuffer catches corrupt data before processing
expect(res.statusCode).toBe(400);
const result = JSON.parse(res.body);
expect(result.error).toBeDefined();
});
// ── Branch coverage: HEIF content format input ─────────────────────
it("compares portrait HEIC image with PNG", { timeout: 120_000 }, async () => {
const HEIC_PORTRAIT = readFileSync(join(FIXTURES, "test-portrait.heic"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.heic", contentType: "image/heic", content: HEIC_PORTRAIT },
{ 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();
});
// ── Branch coverage: same JPEG compared to itself ──────────────────
it("reports 100% similarity for same JPEG compared to itself", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", content: JPG },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
]);
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).toBe(100);
expect(result.dimensions.width).toBe(100);
expect(result.dimensions.height).toBe(100);
});
// ── Branch coverage: exif-oriented image comparison ────────────────
it("compares an EXIF-oriented image with a standard image", async () => {
const EXIF = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "exif.jpg", contentType: "image/jpeg", content: EXIF },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
]);
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.dimensions.width).toBeGreaterThan(0);
expect(result.dimensions.height).toBeGreaterThan(0);
});
// ── Branch coverage: large stress image compared to small ──────────
it("compares two large stress images", async () => {
const LARGE = readFileSync(join(FIXTURES, "content", "stress-large.jpg"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", content: LARGE },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: LARGE },
]);
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).toBe(100);
});
// ── HEIF format input ─────────────────────────────────────────────
it("compares HEIF image with PNG", { timeout: 120_000 }, 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();
});
// ── SVG vs SVG comparison ─────────────────────────────────────────
it("compares two identical SVG images (100% similarity)", 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/compare",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.similarity).toBe(100);
expect(result.downloadUrl).toBeDefined();
expect(result.dimensions.width).toBeGreaterThan(0);
expect(result.dimensions.height).toBeGreaterThan(0);
});
// ── Two different SVGs ────────────────────────────────────────────
it("compares two different SVG images", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const LOGO_SVG = readFileSync(join(FIXTURES, "content", "svg-logo.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: LOGO_SVG },
]);
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);
});
// ── Portrait vs portrait comparison ───────────────────────────────
it("compares two portrait images", async () => {
const PORTRAIT = readFileSync(join(FIXTURES, "test-portrait.jpg"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", content: PORTRAIT },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: PORTRAIT },
]);
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).toBe(100);
});
// ── Diff image dimensions verified via download ───────────────────
it("diff image matches the reported dimensions", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ 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);
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
});
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.width).toBe(result.dimensions.width);
expect(meta.height).toBe(result.dimensions.height);
expect(meta.format).toBe("png");
});
});