Files
SnapOtter/tests/unit/image-engine/metadata-expanded.test.ts
T
SnapOtter 06d1822491 test: expand test coverage across all layers (+1,157 tests)
Fix 2 failing unit tests (landing hero text mismatch) and broken
coverage tooling (brace-expansion v5 override breaking minimatch).
Add ~1,097 new test cases via 14-agent parallel expansion:

- Unit: +290 tests (AI bridge, image-engine, stores, API helpers)
- Integration: +504 tests (all tools, cross-format matrix, adversarial)
- E2E: +363 tests (navigation, tool UI, batch/pipeline, settings,
  visual regression, accessibility, performance, cross-browser)

Total: 4,223 unit + 6,057 integration + 1,563 E2E = 11,843 tests
2026-06-06 19:37:29 +08:00

244 lines
7.4 KiB
TypeScript

/**
* Expanded metadata tests covering:
* - sanitizeValue edge cases: deeply nested, mixed types, empty arrays/objects
* - parseGps edge cases: NaN altitude, non-numeric altitude, missing ref
* - parseXmp edge cases: multiple overlapping keys, multiline XML
* - parseExif: corrupt EXIF buffer (parse failure catch branch)
*/
import { parseGps, parseXmp, sanitizeValue } from "@snapotter/image-engine";
import { describe, expect, it } from "vitest";
describe("sanitizeValue edge cases", () => {
it("handles empty array", () => {
expect(sanitizeValue([])).toEqual([]);
});
it("handles empty object", () => {
expect(sanitizeValue({})).toEqual({});
});
it("handles deeply nested structure", () => {
const input = {
a: {
b: {
c: {
d: new Date("2025-12-25T00:00:00Z"),
},
},
},
};
const result = sanitizeValue(input) as Record<string, unknown>;
expect((result as any).a.b.c.d).toBe("2025-12-25T00:00:00.000Z");
});
it("handles array with mixed types including Buffers and Dates", () => {
const input = [
42,
"hello",
new Date("2024-01-01T00:00:00Z"),
Buffer.from([10, 20]),
null,
undefined,
true,
];
const result = sanitizeValue(input);
expect(result).toEqual([
42,
"hello",
"2024-01-01T00:00:00.000Z",
[10, 20],
null,
undefined,
true,
]);
});
it("handles object with Buffer value at exactly 256 bytes", () => {
const buf = Buffer.alloc(256, 0xaa);
const result = sanitizeValue({ data: buf }) as Record<string, unknown>;
// 256 is not > 256, so it should be an array
expect(Array.isArray(result.data)).toBe(true);
expect((result.data as number[]).length).toBe(256);
});
it("handles object with Buffer value at 257 bytes", () => {
const buf = Buffer.alloc(257, 0xbb);
const result = sanitizeValue({ data: buf }) as Record<string, unknown>;
expect(result.data).toBe("<binary 257 bytes>");
});
it("handles string values without transformation", () => {
expect(sanitizeValue("")).toBe("");
expect(sanitizeValue("test string")).toBe("test string");
});
it("handles NaN and Infinity numbers", () => {
expect(sanitizeValue(NaN)).toBeNaN();
expect(sanitizeValue(Infinity)).toBe(Infinity);
expect(sanitizeValue(-Infinity)).toBe(-Infinity);
});
it("handles zero-length buffer", () => {
const buf = Buffer.alloc(0);
expect(sanitizeValue(buf)).toEqual([]);
});
it("handles nested array of objects with dates", () => {
const input = [
{ ts: new Date("2025-01-01T00:00:00Z") },
{ ts: new Date("2025-06-01T00:00:00Z") },
];
const result = sanitizeValue(input) as Array<Record<string, unknown>>;
expect(result[0].ts).toBe("2025-01-01T00:00:00.000Z");
expect(result[1].ts).toBe("2025-06-01T00:00:00.000Z");
});
});
describe("parseGps edge cases", () => {
it("handles NaN altitude", () => {
const result = parseGps({
GPSAltitude: NaN,
GPSAltitudeRef: 0,
});
expect(result.altitude).toBeNull();
});
it("handles non-numeric altitude (string)", () => {
const result = parseGps({
GPSAltitude: "100" as unknown as number,
});
// typeof "100" is not "number", so altitude should be null
expect(result.altitude).toBeNull();
});
it("handles undefined altitude ref (positive altitude)", () => {
const result = parseGps({
GPSAltitude: 50,
});
expect(result.altitude).toBe(50);
});
it("handles altitude ref = 0 (above sea level)", () => {
const result = parseGps({
GPSAltitude: 100,
GPSAltitudeRef: 0,
});
expect(result.altitude).toBe(100);
});
it("handles altitude ref = 1 (below sea level)", () => {
const result = parseGps({
GPSAltitude: 100,
GPSAltitudeRef: 1,
});
expect(result.altitude).toBe(-100);
});
it("handles lat without ref (defaults to positive)", () => {
const result = parseGps({
GPSLatitude: [10, 0, 0],
});
expect(result.latitude).toBeCloseTo(10, 3);
});
it("handles lon without ref (defaults to positive)", () => {
const result = parseGps({
GPSLongitude: [20, 30, 0],
});
expect(result.longitude).toBeCloseTo(20.5, 3);
});
it("ignores latitude with wrong length (4 elements)", () => {
const result = parseGps({
GPSLatitude: [10, 20, 30, 40],
GPSLatitudeRef: "N",
});
expect(result.latitude).toBeNull();
});
it("ignores latitude with wrong length (1 element)", () => {
const result = parseGps({
GPSLatitude: [10],
GPSLatitudeRef: "N",
});
expect(result.latitude).toBeNull();
});
it("handles zero coordinates (prime meridian / equator)", () => {
const result = parseGps({
GPSLatitude: [0, 0, 0],
GPSLatitudeRef: "N",
GPSLongitude: [0, 0, 0],
GPSLongitudeRef: "E",
});
expect(result.latitude).toBe(0);
expect(result.longitude).toBe(0);
});
it("handles maximum coordinate values", () => {
const result = parseGps({
GPSLatitude: [90, 0, 0],
GPSLatitudeRef: "N",
GPSLongitude: [180, 0, 0],
GPSLongitudeRef: "E",
});
expect(result.latitude).toBe(90);
expect(result.longitude).toBe(180);
});
});
describe("parseXmp edge cases", () => {
it("handles multiple attributes on same element", () => {
const xml = Buffer.from(
'<rdf:Description xmp:Rating="5" xmp:Label="Select" dc:title="Photo 1" />',
);
const result = parseXmp(xml);
expect(result["xmp:Rating"]).toBe("5");
expect(result["xmp:Label"]).toBe("Select");
expect(result["dc:title"]).toBe("Photo 1");
});
it("handles attributes with special characters in values", () => {
const xml = Buffer.from('<rdf:Description dc:creator="John &amp; Jane" />');
const result = parseXmp(xml);
// The regex matches until the closing quote, so "John &amp; Jane" is captured
expect(result["dc:creator"]).toBe("John &amp; Jane");
});
it("ignores attributes without namespace prefix", () => {
// The regex requires "word:word" pattern, so single-word attributes are skipped
const xml = Buffer.from('<element standalone="true" ns:valid="yes" />');
const result = parseXmp(xml);
expect(result.standalone).toBeUndefined();
expect(result["ns:valid"]).toBe("yes");
});
it("handles XML with multiple rdf:Description elements", () => {
const xml = Buffer.from(
'<rdf:Description dc:title="First" />' +
'<rdf:Description exif:DateTimeOriginal="2025-01-01" />',
);
const result = parseXmp(xml);
expect(result["dc:title"]).toBe("First");
expect(result["exif:DateTimeOriginal"]).toBe("2025-01-01");
});
it("handles large XMP buffer without error", () => {
// Generate a large but parseable XMP
const attrs = Array.from({ length: 100 }, (_, i) => `ns:key${i}="val${i}"`).join(" ");
const xml = Buffer.from(`<rdf:Description ${attrs} />`);
const result = parseXmp(xml);
expect(Object.keys(result).length).toBe(100);
expect(result["ns:key0"]).toBe("val0");
expect(result["ns:key99"]).toBe("val99");
});
it("handles empty attributes as empty strings", () => {
const xml = Buffer.from('<rdf:Description dc:title="" />');
// The regex /(\w+:\w+)="([^"]+)"/ requires at least one char between quotes
// So empty values are NOT matched -- this is correct behavior
const result = parseXmp(xml);
expect(result["dc:title"]).toBeUndefined();
});
});