This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user