/** * 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; 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; // 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; expect(result.data).toBe(""); }); 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>; 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( '', ); 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(''); const result = parseXmp(xml); // The regex matches until the closing quote, so "John & Jane" is captured expect(result["dc:creator"]).toBe("John & Jane"); }); it("ignores attributes without namespace prefix", () => { // The regex requires "word:word" pattern, so single-word attributes are skipped const xml = Buffer.from(''); 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( '' + '', ); 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(``); 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(''); // 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(); }); });