← 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");
  }
});