88 lines
5.2 KiB
JavaScript
88 lines
5.2 KiB
JavaScript
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { createRideBusClient, parseRequest } from '../server/ridebus.mjs';
|
||
import { routeDepartures } from '../server/timetable.mjs';
|
||
import { formatWait, isTravelDate, localDate } from '../src/lib/time.ts';
|
||
|
||
const route = { id: 1, number: '112С', name: 'Вокзал → Центр' };
|
||
const stops = [
|
||
{ stop_id: 1, sequence: 1, time_offset_minutes: 0 },
|
||
{ stop_id: 2, sequence: 2, time_offset_minutes: 10 },
|
||
{ stop_id: 3, sequence: 3, time_offset_minutes: 25 },
|
||
];
|
||
|
||
test('waiting time uses hours and minutes and dates respect Belarus midnight', () => {
|
||
for (const [minutes, label] of [[0, 'сейчас'], [-1, 'сейчас'], [1, 'через 1 мин'], [59, 'через 59 мин'], [60, 'через 1 ч'], [61, 'через 1 ч 1 мин'], [121, 'через 2 ч 1 мин'], [180, 'через 3 ч']]) {
|
||
assert.equal(formatWait(minutes), label);
|
||
}
|
||
assert.equal(localDate(Date.parse('2026-10-06T21:01:00Z')), '2026-10-07');
|
||
assert.equal(isTravelDate('2026-02-30'), false);
|
||
});
|
||
|
||
test('timetable requests require actual calendar dates and safe IDs', () => {
|
||
assert.deepEqual(parseRequest(new URLSearchParams('resource=timetable&cityId=1&stopId=2&date=2026-10-07')), { resource: 'timetable', cityId: 1, stopId: 2, date: '2026-10-07' });
|
||
for (const date of ['', '2026-02-30', '2026-13-01', '2026-10-07&url=evil', '0000-01-01']) {
|
||
assert.throws(() => parseRequest(new URLSearchParams({ resource: 'timetable', cityId: '1', stopId: '2', date })), error => error.status === 400);
|
||
}
|
||
});
|
||
|
||
test('full timetable respects weekday exceptions and shortened trips, and offsets from the trip start', () => {
|
||
const result = routeDepartures(route, stops, { schedule: { trips: [
|
||
{ departureTime: '08:00', startStopId: 2, endStopId: 3, daysOfWeek: [3], note: 'Укороченный рейс' },
|
||
{ departureTime: '09:00', endStopId: 1 },
|
||
{ departureTime: '10:00', exceptDaysOfWeek: [3] },
|
||
{ departureTime: '11:00', daysOfWeek: [4] },
|
||
{ departureTime: '12:00', startStopId: 999 },
|
||
] } }, 3, '2026-10-07');
|
||
assert.equal(result.departures.length, 1);
|
||
assert.equal(result.departures[0].scheduledTime, '08:15');
|
||
assert.equal(result.departures[0].scheduledAt, '2026-10-07T05:15:00.000Z');
|
||
assert.equal(result.departures[0].note, 'Укороченный рейс');
|
||
assert.equal(result.partial, true);
|
||
});
|
||
|
||
test('reverse trips and legacy schedules produce correct times across midnight', () => {
|
||
const reverse = routeDepartures(route, stops, { schedule: { trips: [{ departureTime: '08:00', startStopId: 3, endStopId: 1 }] } }, 2, '2026-10-07');
|
||
assert.equal(reverse.departures[0].scheduledTime, '08:15');
|
||
const legacy = routeDepartures(route, stops, { schedule: { departure_times: ['23:50:00'] } }, 3, '2026-10-06');
|
||
assert.equal(legacy.departures[0].scheduledTime, '00:15');
|
||
assert.equal(legacy.departures[0].scheduledAt, '2026-10-06T21:15:00.000Z');
|
||
});
|
||
|
||
test('adapter includes previous-day overnight trips, caches by date/city, and reports failed routes', async () => {
|
||
const calls = [];
|
||
const client = createRideBusClient({ apiKey: 'secret', fetchData: async (url, options) => {
|
||
calls.push(`${options.headers['X-City-Id']}:${url.pathname}${url.search}`);
|
||
if (url.pathname.includes('/routes/2/')) return { ok: false, status: 500 };
|
||
const data = url.pathname.includes('/schedule/today')
|
||
? { schedule: { departure_times: ['08:00', '23:50'] }, isHoliday: true, holidayName: 'Праздник' }
|
||
: url.pathname.endsWith('/stops') ? stops
|
||
: { routes: [{ id: 1, route_number: '112С', name: 'Центр' }, { id: 2, route_number: '2', name: 'Больница' }] };
|
||
return { ok: true, status: 200, json: async () => data };
|
||
} });
|
||
const input = { resource: 'timetable', cityId: 1, stopId: 3, date: '2026-10-07' };
|
||
const data = await client.get(input);
|
||
assert.deepEqual(data.departures.map(item => item.scheduledTime), ['00:15', '08:25']);
|
||
assert.equal(data.partial, true);
|
||
assert.equal(data.isHoliday, true);
|
||
assert.equal(JSON.stringify(data).includes('secret'), false);
|
||
const count = calls.length;
|
||
await client.get(input);
|
||
assert.equal(calls.length, count);
|
||
await client.get({ ...input, date: '2026-10-08' });
|
||
assert.ok(calls.some(call => call.includes('date=2026-10-08')));
|
||
await client.get({ ...input, cityId: 2 });
|
||
assert.ok(calls.some(call => call.startsWith('2:')));
|
||
});
|
||
|
||
test('missing route schedule is empty while total upstream failure remains an error', async () => {
|
||
const client = createRideBusClient({ apiKey: 'key', fetchData: async url => {
|
||
if (url.pathname.includes('/schedule/today')) return { ok: false, status: 404 };
|
||
return { ok: true, json: async () => url.pathname.endsWith('/stops') ? stops : { routes: [{ id: 1, route_number: '1', name: 'Центр' }] } };
|
||
} });
|
||
const data = await client.get({ resource: 'timetable', cityId: 1, stopId: 3, date: '2026-10-07' });
|
||
assert.deepEqual(data.departures, []);
|
||
assert.equal(data.partial, false);
|
||
await assert.rejects(createRideBusClient({ apiKey: 'key', fetchData: async () => ({ ok: false, status: 500 }) }).get({ resource: 'timetable', cityId: 1, stopId: 3, date: '2026-10-07' }), error => error.status === 502);
|
||
});
|