mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: fill image-engine unit test branch coverage gaps
This commit is contained in:
@@ -198,6 +198,17 @@ describe("detectFormat", () => {
|
||||
expect(format).not.toBe("avif");
|
||||
});
|
||||
|
||||
it("rejects ftyp box when buffer too short to read brand (< 12 bytes)", async () => {
|
||||
// Buffer has ftyp at offset 4 but is only 8 bytes long -- too short for brand
|
||||
const buf = Buffer.alloc(8);
|
||||
buf[4] = 0x66;
|
||||
buf[5] = 0x74;
|
||||
buf[6] = 0x79;
|
||||
buf[7] = 0x70; // "ftyp"
|
||||
const format = await detectFormat(buf);
|
||||
expect(format).not.toBe("avif");
|
||||
});
|
||||
|
||||
it("detects JXL ISOBMFF container magic bytes", async () => {
|
||||
const buf = Buffer.from([0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0, 0, 0, 0]);
|
||||
const format = await detectFormat(buf);
|
||||
|
||||
@@ -153,6 +153,18 @@ describe("editMetadata", () => {
|
||||
expect(buf.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("removes a Photo (IFD2) field via fieldsToRemove", async () => {
|
||||
const img = sharp(jpgWithExif);
|
||||
// DateTimeOriginal is in the Photo (IFD2) section
|
||||
const result = await editMetadata(img, {
|
||||
fieldsToRemove: ["DateTimeOriginal"],
|
||||
});
|
||||
const exif = await getExif(result);
|
||||
expect(exif?.Photo?.DateTimeOriginal).toBeUndefined();
|
||||
// Other IFD0 fields should be preserved
|
||||
expect(exif?.Image?.Artist).toBe("Test Artist");
|
||||
});
|
||||
|
||||
// -- Combined edit + remove ------------------------------------------------
|
||||
|
||||
it("handles both edits and removals together", async () => {
|
||||
@@ -200,4 +212,37 @@ describe("editMetadata", () => {
|
||||
const buf = await result.toBuffer();
|
||||
expect(buf.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("survives corrupt EXIF during removal (catch branch)", async () => {
|
||||
// Corrupt the byte order marker in the EXIF so exif-reader throws,
|
||||
// but sharp still sees the EXIF segment as present.
|
||||
const rawBuf = Buffer.from(jpgWithExif);
|
||||
// Find the 'Exif' header in the raw JPEG
|
||||
let exifStart = -1;
|
||||
for (let i = 0; i < rawBuf.length - 4; i++) {
|
||||
if (
|
||||
rawBuf[i] === 0x45 &&
|
||||
rawBuf[i + 1] === 0x78 &&
|
||||
rawBuf[i + 2] === 0x69 &&
|
||||
rawBuf[i + 3] === 0x66
|
||||
) {
|
||||
exifStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(exifStart).toBeGreaterThanOrEqual(0);
|
||||
// Corrupt the byte order marker (offset +6 after 'Exif\0\0')
|
||||
const corruptBuf = Buffer.from(rawBuf);
|
||||
corruptBuf[exifStart + 6] = 0xde;
|
||||
corruptBuf[exifStart + 7] = 0xad;
|
||||
|
||||
const img = sharp(corruptBuf);
|
||||
// fieldsToRemove triggers the removal path which tries to parse EXIF
|
||||
const result = await editMetadata(img, {
|
||||
fieldsToRemove: ["Software"],
|
||||
artist: "Survivor",
|
||||
});
|
||||
const buf = await result.toBuffer();
|
||||
expect(buf.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -219,6 +219,26 @@ describe("parseExif", () => {
|
||||
expect(result.image).toEqual({});
|
||||
expect(result.photo).toEqual({});
|
||||
});
|
||||
|
||||
it("parses GPSInfo section when GPS data is present", async () => {
|
||||
// Create a JPEG with GPS EXIF data via withExif IFD3
|
||||
const buf = await sharp({
|
||||
create: { width: 10, height: 10, channels: 3, background: "#808080" },
|
||||
})
|
||||
.withExif({
|
||||
IFD0: { Artist: "GPS Test" },
|
||||
IFD3: { GPSLatitudeRef: "N" },
|
||||
})
|
||||
.jpeg()
|
||||
.toBuffer();
|
||||
const metadata = await sharp(buf).metadata();
|
||||
expect(metadata.exif).toBeTruthy();
|
||||
const result = parseExif(metadata.exif!);
|
||||
// The GPSInfo section should be populated with sanitized values
|
||||
expect(typeof result.gps).toBe("object");
|
||||
expect(Object.keys(result.gps).length).toBeGreaterThan(0);
|
||||
expect(result.gps.GPSLatitudeRef).toBe("N");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -198,6 +198,28 @@ describe("resize", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("withoutEnlargement clamps height only (no width given)", async () => {
|
||||
const img = sharp(png1x1);
|
||||
const result = await resize(img, {
|
||||
height: 500,
|
||||
withoutEnlargement: true,
|
||||
});
|
||||
const meta = await getMeta(result);
|
||||
expect(meta.height).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
it("withoutEnlargement does not clamp when target is smaller", async () => {
|
||||
const img = sharp(png200x150);
|
||||
const result = await resize(img, {
|
||||
width: 50,
|
||||
height: 50,
|
||||
withoutEnlargement: true,
|
||||
});
|
||||
const meta = await getMeta(result);
|
||||
expect(meta.width).toBe(50);
|
||||
expect(meta.height).toBe(50);
|
||||
});
|
||||
|
||||
it("percentage on a 1x1 image rounds to at least 1px (percentage >= 50)", async () => {
|
||||
// 1 * 50 / 100 = 0.5 -> rounds to 1 (Math.round), so width=1
|
||||
const img = sharp(png1x1);
|
||||
@@ -253,6 +275,35 @@ describe("crop", () => {
|
||||
expect(meta.height).toBe(1);
|
||||
});
|
||||
|
||||
it("crops using percent unit", async () => {
|
||||
const img = sharp(png200x150);
|
||||
// 50% of 200 = 100, 50% of 150 = 75, starting at 10% left, 10% top
|
||||
const result = await crop(img, {
|
||||
left: 10,
|
||||
top: 10,
|
||||
width: 50,
|
||||
height: 50,
|
||||
unit: "percent",
|
||||
});
|
||||
const meta = await getMeta(result);
|
||||
expect(meta.width).toBe(100); // 50% of 200
|
||||
expect(meta.height).toBe(75); // 50% of 150 (Math.round)
|
||||
});
|
||||
|
||||
it("crops 100% of image using percent unit", async () => {
|
||||
const img = sharp(png200x150);
|
||||
const result = await crop(img, {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
unit: "percent",
|
||||
});
|
||||
const meta = await getMeta(result);
|
||||
expect(meta.width).toBe(200);
|
||||
expect(meta.height).toBe(150);
|
||||
});
|
||||
|
||||
// -- Error cases --
|
||||
|
||||
it("throws on width = 0", async () => {
|
||||
@@ -688,6 +739,49 @@ describe("compress", () => {
|
||||
"Target size must be greater than 0",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to PNG for SVG input (NO_ENCODER format)", async () => {
|
||||
const svgBuf = readFileSync(path.join(FIXTURES_DIR, "formats/sample.svg"));
|
||||
const img = sharp(svgBuf);
|
||||
const result = await compress(img, { quality: 80 });
|
||||
const meta = await getMeta(result);
|
||||
expect(meta.format).toBe("png");
|
||||
});
|
||||
|
||||
it("target size early break when within tolerance", async () => {
|
||||
// Use a large solid-color image so binary search finishes quickly
|
||||
const bigBuf = await sharp({
|
||||
create: { width: 800, height: 600, channels: 3, background: "#cc6633" },
|
||||
})
|
||||
.jpeg({ quality: 95 })
|
||||
.toBuffer();
|
||||
// Set target to the actual size so it matches within tolerance immediately
|
||||
const targetBytes = bigBuf.length;
|
||||
const result = await compress(sharp(bigBuf), {
|
||||
targetSizeBytes: targetBytes,
|
||||
format: "jpg",
|
||||
});
|
||||
const buf = await result.toBuffer();
|
||||
// Should be within 5% tolerance of target
|
||||
expect(Math.abs(buf.length - targetBytes) / targetBytes).toBeLessThan(0.5);
|
||||
});
|
||||
|
||||
it("target size falls back to bestQuality=1 when bestBuffer stays null", async () => {
|
||||
// Use a very tiny target that forces all iterations to overshoot,
|
||||
// so bestBuffer never gets assigned and the fallback path runs
|
||||
const bigBuf = await sharp({
|
||||
create: { width: 200, height: 200, channels: 3, background: "#ff8800" },
|
||||
})
|
||||
.jpeg({ quality: 100 })
|
||||
.toBuffer();
|
||||
// Target of 1 byte -- impossible to hit, so every iteration overshoots
|
||||
const result = await compress(sharp(bigBuf), {
|
||||
targetSizeBytes: 1,
|
||||
format: "jpg",
|
||||
});
|
||||
const buf = await result.toBuffer();
|
||||
expect(buf.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -729,6 +823,27 @@ describe("stripMetadata", () => {
|
||||
expect(meta.width).toBe(200);
|
||||
expect(meta.height).toBe(150);
|
||||
});
|
||||
|
||||
it("stripIcc=true enters selective stripping path", async () => {
|
||||
const img = sharp(jpg100x100);
|
||||
const result = await stripMetadata(img, { stripIcc: true });
|
||||
const buf = await result.toBuffer();
|
||||
expect(buf.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("stripGps=true enters selective stripping path", async () => {
|
||||
const img = sharp(jpg100x100);
|
||||
const result = await stripMetadata(img, { stripGps: true });
|
||||
const buf = await result.toBuffer();
|
||||
expect(buf.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("stripXmp=true enters selective stripping path", async () => {
|
||||
const img = sharp(jpg100x100);
|
||||
const result = await stripMetadata(img, { stripXmp: true });
|
||||
const buf = await result.toBuffer();
|
||||
expect(buf.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user