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