← back to Charge And Explore
backend/test/telematics.test.ts
93 lines
import { test } from "node:test";
import assert from "node:assert/strict";
import { SmartcarProvider, normalizeCharge } from "../src/providers/telematics.ts";
test("normalizeCharge maps smartcar battery+charge to normalized schema", () => {
const n = normalizeCharge({ percentRemaining: 0.62, range: 187 }, { state: "CHARGING", isPluggedIn: true }, "imperial");
assert.equal(n.batteryPercent, 62);
assert.equal(n.rangeMiles, 187);
assert.equal(n.isCharging, true);
assert.equal(n.pluggedIn, true);
assert.equal(n.chargeState, "CHARGING");
assert.ok(n.updatedAt);
});
test("normalizeCharge: FULLY_CHARGED is not 'charging'", () => {
const n = normalizeCharge({ percentRemaining: 1 }, { state: "FULLY_CHARGED", isPluggedIn: true });
assert.equal(n.batteryPercent, 100);
assert.equal(n.isCharging, false);
assert.equal(n.chargeState, "FULLY_CHARGED");
});
test("normalizeCharge tolerates 0-100 percent + metric range + missing fields", () => {
const n = normalizeCharge({ percentRemaining: 80, range: 100 }, {}, "metric");
assert.equal(n.batteryPercent, 80);
assert.equal(n.rangeMiles, 62); // 100km -> ~62mi
assert.equal(n.isCharging, null);
assert.equal(n.pluggedIn, null);
assert.equal(n.chargeState, null);
});
test("normalizeCharge handles empty payloads", () => {
const n = normalizeCharge({}, {});
assert.equal(n.batteryPercent, null);
assert.equal(n.rangeMiles, null);
assert.equal(n.isCharging, null);
});
test("SmartcarProvider is inert without env creds", () => {
const p = new SmartcarProvider();
assert.equal(p.configured, false); // SMARTCAR_CLIENT_ID/SECRET unset in test env
assert.equal(p.id, "smartcar");
});
test("connectUrl builds a valid Smartcar authorize URL", () => {
const p = new SmartcarProvider();
const u = p.connectUrl("https://chargeandexplore.com/auth/smartcar/callback", "abc123");
assert.ok(u.startsWith("https://connect.smartcar.com/oauth/authorize"));
assert.ok(u.includes("state=abc123"));
assert.ok(u.includes("scope=read_vehicle_info"));
assert.ok(u.includes(encodeURIComponent("https://chargeandexplore.com/auth/smartcar/callback")));
});
// --- Smartcar Connect `mode` is an enum, not a free string (TK-10142) ---
// Smartcar Connect accepts ONLY "live" | "simulated". The provider used to
// default to "test", which is not a valid value, so every authorize URL
// carried an invalid mode. The pre-existing "connectUrl builds a valid
// Smartcar authorize URL" test passed the whole time it was broken — it never
// asserted `mode`. These assert the enum directly, including the injected
// fault, so the invalid-literal class cannot come back silently.
const MODE_OF = (u: string) => new URL(u).searchParams.get("mode");
const ALLOWED = new Set(["live", "simulated"]);
function connectUrlWithMode(mode: string | undefined): string {
const prev = process.env.SMARTCAR_MODE;
if (mode === undefined) delete process.env.SMARTCAR_MODE;
else process.env.SMARTCAR_MODE = mode;
try {
return new SmartcarProvider().connectUrl("https://chargeandexplore.com/auth/smartcar/callback", "s");
} finally {
if (prev === undefined) delete process.env.SMARTCAR_MODE;
else process.env.SMARTCAR_MODE = prev;
}
}
test("connectUrl mode defaults to 'live' when SMARTCAR_MODE is unset", () => {
assert.equal(MODE_OF(connectUrlWithMode(undefined)), "live");
});
test("connectUrl honours an explicit 'simulated'", () => {
assert.equal(MODE_OF(connectUrlWithMode("simulated")), "simulated");
});
test("NEGATIVE: an invalid SMARTCAR_MODE never reaches the authorize URL", () => {
// "test" is the exact literal the old default shipped. Injecting it (and
// other junk) must NOT produce mode=test — it must normalize to "live".
for (const bad of ["test", "", "LIVE", "prod", "sandbox"]) {
const m = MODE_OF(connectUrlWithMode(bad));
assert.ok(ALLOWED.has(m ?? ""), `mode=${m} from SMARTCAR_MODE=${JSON.stringify(bad)} is not an allowed value`);
assert.notEqual(m, "test");
}
});