98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { ValidationError, validateChargingSession, validateConsumption } from "../src/lib/validate.ts";
|
|
|
|
const INSTALLATION = "6f1a2b3c-4d5e-4f60-8a9b-0c1d2e3f4a5b";
|
|
const SESSION = "11111111-2222-4333-8444-555555555555";
|
|
|
|
const day = { km: 42.5, kwh: 7.25, hours: 0.9, socDrop: 11.5 };
|
|
|
|
test("accepts a well-formed daily record", () => {
|
|
const record = validateConsumption({
|
|
installationId: INSTALLATION,
|
|
date: "2026-10-07",
|
|
recordedAt: 1_760_000_000_000,
|
|
day,
|
|
lifetime: { km: 12000, kwh: 2100, hours: 240, socDrop: 3400 },
|
|
sohPercent: 97.5,
|
|
});
|
|
assert.equal(record.date, "2026-10-07");
|
|
assert.equal(record.day.kwh, 7.25);
|
|
assert.ok(record.recordedAt instanceof Date);
|
|
});
|
|
|
|
test("a missing SOH is omitted rather than defaulted", () => {
|
|
// The car omits a field it could not read. Writing 0 here would be a battery at
|
|
// nothing, which is the same class of lie the telemetry side refuses to send.
|
|
const record = validateConsumption({
|
|
installationId: INSTALLATION,
|
|
date: "2026-10-07",
|
|
recordedAt: 1,
|
|
day,
|
|
lifetime: day,
|
|
});
|
|
assert.equal("sohPercent" in record, false);
|
|
});
|
|
|
|
test("rejects malformed identifiers, dates and out-of-range numbers", () => {
|
|
const base = { installationId: INSTALLATION, date: "2026-10-07", recordedAt: 1, day, lifetime: day };
|
|
assert.throws(() => validateConsumption({ ...base, installationId: "nope" }), ValidationError);
|
|
assert.throws(() => validateConsumption({ ...base, date: "07.10.2026" }), ValidationError);
|
|
assert.throws(() => validateConsumption({ ...base, sohPercent: 140 }), ValidationError);
|
|
assert.throws(() => validateConsumption({ ...base, day: { ...day, kwh: "7" } }), ValidationError);
|
|
assert.throws(() => validateConsumption({ ...base, recordedAt: -1 }), ValidationError);
|
|
assert.throws(() => validateConsumption(null), ValidationError);
|
|
});
|
|
|
|
test("a charging session must match the id in its path", () => {
|
|
const body = {
|
|
installationId: INSTALLATION,
|
|
sessionId: SESSION,
|
|
startedAt: 1_760_000_000_000,
|
|
endedAt: 1_760_002_000_000,
|
|
durationSeconds: 1800,
|
|
energyKwh: 31.4,
|
|
chargingLossPercent: 10,
|
|
gridEnergyKwh: 34.54,
|
|
pricePerKwh: 7.5,
|
|
totalCost: 235.5,
|
|
startSocPercent: 18,
|
|
endSocPercent: 72,
|
|
curve: [{ timestampMs: 1_760_000_060_000, powerKw: 84.2, socPercent: 20 }],
|
|
};
|
|
const record = validateChargingSession(body, SESSION);
|
|
assert.equal(record.curve.length, 1);
|
|
assert.equal(record.curve[0].powerKw, 84.2);
|
|
assert.equal(record.chargingLossPercent, 10);
|
|
assert.equal(record.gridEnergyKwh, 34.54);
|
|
assert.equal(record.totalCost, 259.05);
|
|
assert.throws(() => validateChargingSession(body, "11111111-2222-4333-8444-555555555556"), ValidationError);
|
|
});
|
|
|
|
test("refuses a curve longer than the car can produce", () => {
|
|
const curve = Array.from({ length: 721 }, (_unused, index) => ({
|
|
timestampMs: 1_760_000_000_000 + index * 60_000,
|
|
powerKw: 50,
|
|
}));
|
|
assert.throws(
|
|
() =>
|
|
validateChargingSession(
|
|
{
|
|
installationId: INSTALLATION,
|
|
sessionId: SESSION,
|
|
startedAt: 1,
|
|
endedAt: 2,
|
|
durationSeconds: 1,
|
|
energyKwh: 1,
|
|
chargingLossPercent: 10,
|
|
gridEnergyKwh: 1.1,
|
|
pricePerKwh: 1,
|
|
totalCost: 1,
|
|
curve,
|
|
},
|
|
SESSION,
|
|
),
|
|
ValidationError,
|
|
);
|
|
});
|