import { describe, it, expect } from "vitest"; import { fleetAiValue, fleetAiNumber, fleetAiBoolean, parseFleetAiTimestamp, readFleetAiError, fixAgeSeconds, SAST_OFFSET_MINUTES, } from "./tracker-parsing"; describe("fleetAiValue", () => { it("maps the double-dash sentinel to null", () => { // §1.1: "Fields returning -- in responses mean the value is // unavailable or not configured. This is not null — handle it // explicitly." Without this, "--" leaks into the UI as a literal. expect(fleetAiValue("--")).toBeNull(); }); it("maps blank and whitespace-only values to null", () => { expect(fleetAiValue("")).toBeNull(); expect(fleetAiValue(" ")).toBeNull(); }); it("maps missing values to null", () => { expect(fleetAiValue(undefined)).toBeNull(); expect(fleetAiValue(null)).toBeNull(); }); it("trims and returns real values", () => { expect(fleetAiValue(" Carletonville, GP ")).toBe("Carletonville, GP"); }); it("does not swallow a legitimate value that merely contains a dash", () => { expect(fleetAiValue("Sibanye - Driefontein")).toBe("Sibanye - Driefontein"); expect(fleetAiValue("-")).toBe("-"); }); }); describe("fleetAiNumber", () => { it("parses numeric strings", () => { expect(fleetAiNumber("-26.348597")).toBe(-26.348597); expect(fleetAiNumber(27.375136)).toBe(27.375136); }); it("returns null for the double-dash sentinel", () => { expect(fleetAiNumber("--")).toBeNull(); }); it("strips a unit suffix — gps.speed comes back as '62km/h'", () => { expect(fleetAiNumber("62km/h")).toBe(62); }); it("returns null rather than NaN for unparseable input", () => { expect(fleetAiNumber("unknown")).toBeNull(); expect(fleetAiNumber(undefined)).toBeNull(); }); it("keeps a real zero — a stationary vehicle has speed 0", () => { expect(fleetAiNumber("0")).toBe(0); }); }); describe("fleetAiBoolean", () => { it("reads the ON/OFF flags the platform uses for IGN", () => { expect(fleetAiBoolean("ON")).toBe(true); expect(fleetAiBoolean("OFF")).toBe(false); expect(fleetAiBoolean("on")).toBe(true); }); it("returns null for an unwired sensor rather than false", () => { // "--" means the device has no ignition wire. Reading that as // "ignition off" would make the stop detector treat every moving // vehicle as parked. expect(fleetAiBoolean("--")).toBeNull(); expect(fleetAiBoolean(undefined)).toBeNull(); expect(fleetAiBoolean("MAYBE")).toBeNull(); }); it("accepts the boolean-ish variants", () => { expect(fleetAiBoolean("1")).toBe(true); expect(fleetAiBoolean("0")).toBe(false); expect(fleetAiBoolean("true")).toBe(true); }); }); describe("parseFleetAiTimestamp", () => { it("reads dd-MM-yyyy HH:mm:ss as platform-local and returns UTC", () => { // §11: responses carry the platform's configured timezone, which is // UTC+2 for South Africa. Parsing naively would make every fresh fix // look two hours stale. expect(parseFleetAiTimestamp("04-09-2026 14:30:00", SAST_OFFSET_MINUTES)).toBe( "2026-09-04T12:30:00.000Z" ); }); it("reads yyyy-MM-dd HH:mm:ss too", () => { expect(parseFleetAiTimestamp("2026-09-04 14:30:00", SAST_OFFSET_MINUTES)).toBe( "2026-09-04T12:30:00.000Z" ); }); it("disambiguates dd-MM-yyyy from MM-dd-yyyy per the documented format", () => { // 04-09-2026 is 4 September, not 9 April. expect(parseFleetAiTimestamp("04-09-2026 00:00:00", 0)).toBe( "2026-09-04T00:00:00.000Z" ); }); it("accepts a missing seconds component", () => { expect(parseFleetAiTimestamp("04-09-2026 14:30", SAST_OFFSET_MINUTES)).toBe( "2026-09-04T12:30:00.000Z" ); }); it("passes through an ISO-8601 string with an explicit zone", () => { expect(parseFleetAiTimestamp("2026-09-04T12:30:00Z", SAST_OFFSET_MINUTES)).toBe( "2026-09-04T12:30:00.000Z" ); }); it("returns null for the double-dash sentinel and for junk", () => { expect(parseFleetAiTimestamp("--", SAST_OFFSET_MINUTES)).toBeNull(); expect(parseFleetAiTimestamp("not a date", SAST_OFFSET_MINUTES)).toBeNull(); expect(parseFleetAiTimestamp(undefined, SAST_OFFSET_MINUTES)).toBeNull(); }); it("rejects an out-of-range date rather than rolling it over", () => { // JS Date would silently turn month 13 into January of next year. expect(parseFleetAiTimestamp("04-13-2026 00:00:00", 0)).toBeNull(); expect(parseFleetAiTimestamp("32-09-2026 00:00:00", 0)).toBeNull(); }); }); describe("readFleetAiError", () => { it("detects a failure envelope even though the HTTP status was 200", () => { // §12: "The API returns HTTP 200 for most responses including // errors. Always inspect the response body." expect( readFleetAiError({ RESULT: "FAIL", MSG: "Invalid request or validation failed" }) ).toBe("Invalid request or validation failed"); }); it("detects the lowercase result/message variant", () => { expect(readFleetAiError({ result: "FAIL", message: "Invalid parameter" })).toBe( "Invalid parameter" ); }); it("detects a root.error field", () => { expect(readFleetAiError({ root: { error: "No Vehicle Found For Given User" } })).toBe( "No Vehicle Found For Given User" ); }); it("detects a bare error message with no result field", () => { expect(readFleetAiError({ message: "Refresh Token Expired" })).toBe( "Refresh Token Expired" ); expect(readFleetAiError({ message: "The call exceeded the limit of one minute one call" })).toBe( "The call exceeded the limit of one minute one call" ); }); it("returns null for a successful response", () => { expect(readFleetAiError({ result: "sample", message: "sample", data: [] })).toBeNull(); expect(readFleetAiError({ root: { VehicleData: [] } })).toBeNull(); expect(readFleetAiError({ data: [{ plate_no: "ABC123GP" }] })).toBeNull(); }); it("does not mistake a success message for an error", () => { expect(readFleetAiError({ result: 1, message: "User is Active" })).toBeNull(); }); it("returns null for a non-object body", () => { expect(readFleetAiError(null)).toBeNull(); expect(readFleetAiError("plain text")).toBeNull(); }); }); describe("fixAgeSeconds", () => { const now = new Date("2026-09-04T12:30:00.000Z"); it("measures how stale a fix is", () => { expect(fixAgeSeconds("2026-09-04T12:25:00.000Z", now)).toBe(300); }); it("returns 0 for a fix in the future rather than a negative age", () => { // Clock skew between the platform and this server is normal; a // negative age would render as "-3 minutes ago" in the UI. expect(fixAgeSeconds("2026-09-04T12:33:00.000Z", now)).toBe(0); }); it("returns null when the fix time is unknown", () => { expect(fixAgeSeconds(null, now)).toBeNull(); }); });