Files
ln-bi/src/server/routes/mileage/summary-api.test.ts
T
shishengliang e2f932232f
ci/woodpecker/push/woodpecker Pipeline was successful
feat(mileage): 接入区间汇总并复用查询结果
2026-10-29 09:11:21 +08:00

75 lines
4.5 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { fetchMileageSummary, parseMileageSummary, MileageSummaryError } from './summary-api.js';
const originalConfig = { url: process.env.ONEOS_MILEAGE_API_BASE_URL, key: process.env.ONEOS_MILEAGE_API_KEY };
test.before(() => { process.env.ONEOS_MILEAGE_API_BASE_URL = 'https://oneos.test'; process.env.ONEOS_MILEAGE_API_KEY = 'test-only'; });
test.after(() => {
if (originalConfig.url === undefined) delete process.env.ONEOS_MILEAGE_API_BASE_URL; else process.env.ONEOS_MILEAGE_API_BASE_URL = originalConfig.url;
if (originalConfig.key === undefined) delete process.env.ONEOS_MILEAGE_API_KEY; else process.env.ONEOS_MILEAGE_API_KEY = originalConfig.key;
});
const start = '2026-10-01', end = '2026-10-02';
const vehicle = (plate: string) => ({ plateNumber: plate, vin: 'VIN', periodMileageKm: 0, endDateTotalMileageKm: null,
endDateDataTime: null, endDateSourceProtocol: null, validDays: 1, missingDays: 1, anomalyDays: 0,
status: 'PARTIAL_DATA', qualityReasons: ['CARRIED_FORWARD'], sourceProtocols: ['MQTT'], updatedAt: null });
const payload = () => ({ code: 'SUCCESS', startDate: start, endDate: end, timezone: 'Asia/Shanghai', vehicleCount: 1,
data: { vehicles: [vehicle('TEST0')], dailyTrend: [
{ date: start, mileageKm: 0, validVehicleCount: 1, missingVehicleCount: 0, anomalyVehicleCount: 0 },
{ date: end, mileageKm: null, validVehicleCount: 0, missingVehicleCount: 1, anomalyVehicleCount: 0 },
] }, snapshotId: 's1', generatedAt: '2026-10-27T00:00:00Z', expiresAt: '2099-01-01T00:00:00Z', nextCursor: null as string | null });
test('summary boundary preserves real zero and missing trend; rejects malformed counts/status/values', () => {
const p = payload();
assert.equal(parseMileageSummary(p,start,end).vehicles[0].periodMileageKm,0);
assert.equal(parseMileageSummary(p,start,end).dailyTrend[1].mileageKm,null);
for (const mutate of [
(p: ReturnType<typeof payload>) => { p.data.vehicles[0].validDays = 2; },
(p: ReturnType<typeof payload>) => { p.data.vehicles[0].status = 'NORMAL'; },
(p: ReturnType<typeof payload>) => { p.data.vehicles[0].periodMileageKm = NaN; },
(p: ReturnType<typeof payload>) => { p.data.dailyTrend[1].mileageKm = 0; },
]) { const bad = payload(); mutate(bad); assert.throws(() => parseMileageSummary(bad,start,end)); }
});
test('summary pagination keeps the snapshot and reuses one fleet trend; empty scope never means all', async t => {
t.mock.method(globalThis, 'fetch', async (_url: string, options: RequestInit) => {
const body = JSON.parse(options.body as string);
assert.deepEqual(body.plateNumbers, ['TEST0','TEST1']);
assert.equal(body.includeDailyTrend,true);
assert.equal(body.pageSize,5000);
const p = payload(); p.vehicleCount = 2;
p.data.vehicles = [vehicle(body.cursor ? 'TEST1':'TEST0')];
p.data.dailyTrend[0].validVehicleCount = 2;
p.data.dailyTrend[1].missingVehicleCount = 2;
p.nextCursor = body.cursor ? null:'next';
return Response.json(p);
});
const result = await fetchMileageSummary(start,end,['MQTT'],['TEST0','TEST1']);
assert.equal(result.vehicles.length,2);
assert.equal(result.dailyTrend.length,2);
assert.equal(result.dailyTrend[1].mileageKm,null);
await assert.rejects(fetchMileageSummary(start,end,['MQTT'],[]), /空车牌/);
});
test('summary authorization, throttling and timeout errors are explicit and never return empty success', async t => {
for (const status of [401,403,429,504]) {
const mock = t.mock.method(globalThis, 'fetch', async () => Response.json({code:'TEST_ERROR',traceId:'test-trace'}, {status,headers:{'Retry-After':'30'}}));
await assert.rejects(fetchMileageSummary(start,end,['MQTT']), e => e instanceof MileageSummaryError && e.status === status && e.traceId === 'test-trace' && e.retryAfter === '30');
mock.mock.restore();
}
});
test('snapshot changes and duplicate vehicles reject partial pagination', async t => {
for (const changedSnapshot of [true,false]) {
const mock=t.mock.method(globalThis,'fetch',async (_url:string,options:RequestInit)=>{
const body=JSON.parse(options.body as string), p=payload(); p.vehicleCount=2;
p.data.dailyTrend[0].validVehicleCount=2; p.data.dailyTrend[1].missingVehicleCount=2;
p.nextCursor=body.cursor?null:'next';
if(body.cursor && changedSnapshot)p.snapshotId='s2';
return Response.json(p);
});
await assert.rejects(fetchMileageSummary(start,end,['MQTT']),changedSnapshot?/快照/:/重复车辆/);
mock.mock.restore();
}
});