import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createRideBusClient, normalizeRoutes, normalizeRouteStops, parseRequest } from '../server/ridebus.mjs'; import { normalizeWalking } from '../server/walking.mjs'; import { groupRoutes, stopsForRoutes } from '../src/lib/routeGroups.ts'; const geometry = { type: 'LineString', coordinates: [[28.783, 55.486], [28.7855, 55.4865]] }; const foot = { code: 'Ok', routes: [{ distance: 230, duration: 165.4, geometry, legs: [{ private: 'unneeded' }] }] }; test('route and walking parameters require bounded coordinates and fixed identifiers', () => { assert.deepEqual(parseRequest(new URLSearchParams('resource=routes&cityId=1')), { resource: 'routes', cityId: 1 }); assert.deepEqual(parseRequest(new URLSearchParams('resource=routeStops&cityId=1&routeId=10')), { resource: 'routeStops', cityId: 1, routeId: 10 }); assert.deepEqual(parseRequest(new URLSearchParams('resource=walk&cityId=1&stopId=28&lat=0&lon=0')), { resource: 'walk', cityId: 1, stopId: 28, lat: 0, lon: 0 }); for (const params of ['resource=routeStops&cityId=1&routeId=../10', 'resource=walk&cityId=1&stopId=28&lat=91&lon=28', 'resource=walk&cityId=1&stopId=28&lat=55&lon=-181', 'resource=walk&cityId=1&lat=55&lon=28', 'resource=routes&cityId=-1']) assert.throws(() => parseRequest(new URLSearchParams(params)), error => error.status === 400); }); test('route numbers group directions but distinct boarding points retain their identities', () => { const routes = normalizeRoutes([{ id: 10, route_number: '4', name: 'А → Б' }, { id: 11, route_number: '№4', name: 'Б → А' }, { id: 12, route_number: '10', name: 'В → Г' }, { id: 13, route_number: '2', name: 'А → Г', is_active: false }]); assert.deepEqual(groupRoutes(routes).map(group => [group.key, group.routes.map(route => route.id)]), [['4', [10, 11]], ['10', [12]]]); const detail = normalizeRouteStops({ id: 10, route_number: '4', name: 'А → Б', stops: [ { id: 1, name: 'Вокзал', description: 'В центр', sequence: 2, latitude: 55, longitude: 28 }, { id: 2, name: 'Вокзал', description: 'Обратно', sequence: 1, latitude: 55.001, longitude: 28 }, ] }); assert.deepEqual(detail.stops.map(stop => stop.id), [2, 1]); const filtered = stopsForRoutes([{ ...detail.stops[0], direction: 'Уточнённое направление' }], [detail, detail]); assert.deepEqual(filtered.map(stop => stop.id), [2, 1]); assert.equal(filtered[0].direction, 'Уточнённое направление'); assert.equal(filtered[1].latitude, 55); }); test('walking geometry and metrics are validated without inventing a path on failure', () => { assert.deepEqual(normalizeWalking(foot), { geometry, distance: 230, duration: 165.4 }); assert.throws(() => normalizeWalking({ code: 'NoRoute' }), error => error.status === 404 && error.code === 'walking_not_found'); for (const route of [{ ...foot.routes[0], distance: null }, { ...foot.routes[0], duration: -1 }, { ...foot.routes[0], geometry: { type: 'LineString', coordinates: [[28, 55], [999, 55]] } }, { ...foot.routes[0], geometry: { type: 'LineString', coordinates: [[28, 55]] } }]) assert.throws(() => normalizeWalking({ code: 'Ok', routes: [route] }), error => error.status === 502); }); test('walking goes through the verified foot endpoint and never enters origin caches', async () => { const calls = []; const client = createRideBusClient({ apiKey: 'private-key', fetchData: async (url, options) => { calls.push([url, options]); return { ok: true, json: async () => url.pathname.startsWith('/routing/') ? foot : { id: 28, latitude: 55.4869, longitude: 28.7856 } }; } }); const input = { resource: 'walk', cityId: 1, stopId: 28, lat: 55.486, lon: 28.783 }; await client.get(input); await client.get(input); await client.get({ ...input, lat: 55.487 }); const paths = calls.filter(([url]) => url.pathname.startsWith('/routing/')); assert.equal(paths.length, 3); assert.equal(paths[0][0].pathname, '/routing/foot/route/v1/foot/28.783000,55.486000;28.785600,55.486900'); assert.equal(paths[0][0].searchParams.get('geometries'), 'geojson'); assert.equal(paths[0][1].headers['X-City-Id'], '1'); assert.equal(paths[0][1].headers['X-API-Key'], 'private-key'); assert.equal(calls.filter(([url]) => url.pathname.startsWith('/api/v1/stops/')).length, 1); }); test('a no-segment HTTP response maps to a safe walking error', async () => { const client = createRideBusClient({ apiKey: 'key', fetchData: async url => url.pathname.startsWith('/routing/') ? { ok: false, status: 400, json: async () => ({ code: 'NoSegment', message: 'internal details' }) } : { ok: true, json: async () => ({ latitude: 55, longitude: 28 }) } }); await assert.rejects(client.get({ resource: 'walk', cityId: 1, stopId: 28, lat: 55, lon: 28 }), error => error.code === 'walking_not_found' && !error.message.includes('internal')); });