import { test } from 'node:test'; import assert from 'node:assert/strict'; import { coordinates } from '../server/coordinates.mjs'; import { createRideBusClient, normalizeCities, normalizeStop, normalizeStops, parseRequest } from '../server/ridebus.mjs'; import { distanceMeters, hasCoordinates, walkingLink } from '../src/lib/geo.ts'; test('map coordinates accept numeric strings and preserve missing/invalid pairs as missing', () => { assert.deepEqual(coordinates('55.517553', 28.776448), { latitude: 55.517553, longitude: 28.776448 }); assert.deepEqual(coordinates(0, 0), { latitude: 0, longitude: 0 }); for (const [lat, lon] of [[null, 28], ['', 28], [55, undefined], [91, 28], [55, -181], [true, 28], ['bad', 28], [NaN, 28]]) { assert.deepEqual(coordinates(lat, lon), {}); assert.equal(hasCoordinates({ latitude: lat, longitude: lon }), false); } assert.equal(hasCoordinates({ latitude: 0, longitude: 0 }), true); assert.deepEqual(normalizeCities([{ id: 1, name: 'Полоцк', center_lat: '55.4869', center_lon: '28.7856' }])[0].latitude, 55.4869); }); test('stop catalog retains platform coordinates and detail exposes only active distinct routes', () => { const raw = { id: 95, name: '11 Июля', latitude: 55.517553, longitude: 28.776448, address: ' улица Ленина ', description: ' на Лозовку ', routes_count: '4' }; assert.equal(normalizeStops([raw])[0].address, 'улица Ленина'); assert.equal(normalizeStops([raw])[0].longitude, 28.776448); const detail = normalizeStop({ ...raw, secret: 'hidden', routes: [ { id: 2, route_number: '12', name: 'Центр', is_active: true }, { id: 1, route_number: '4', name: 'Вокзал' }, { id: 1, route_number: '4', name: 'Вокзал' }, { id: 3, route_number: '2', name: 'Больница', is_active: false }, ] }); assert.deepEqual(detail.routes.map(route => route.number), ['4', '12']); assert.equal(detail.routesCount, 2); assert.equal('secret' in detail, false); assert.throws(() => normalizeStop({ ...raw, routes: null }), error => error.status === 502); }); test('stop details validate identifiers, coalesce loads and isolate city caches', async () => { assert.deepEqual(parseRequest(new URLSearchParams('resource=stop&cityId=1&stopId=95')), { resource: 'stop', cityId: 1, stopId: 95 }); for (const input of ['resource=stop&cityId=1', 'resource=stop&cityId=1&stopId=-1', 'resource=stop&cityId=1&stopId=../1']) assert.throws(() => parseRequest(new URLSearchParams(input)), error => error.status === 400); const calls = []; const client = createRideBusClient({ apiKey: 'private-key', fetchData: async (url, options) => { calls.push([url.pathname, options.headers['X-City-Id']]); return { ok: true, json: async () => ({ id: 95, name: '11 Июля', routes: [] }) }; } }); const input = { resource: 'stop', cityId: 1, stopId: 95 }; await Promise.all([client.get(input), client.get(input)]); await client.get(input); await client.get({ ...input, cityId: 2 }); assert.deepEqual(calls, [['/api/v1/stops/95', '1'], ['/api/v1/stops/95', '2']]); }); test('nearby distances use geographic coordinates and walking links use the selected platform', () => { const a = { latitude: 55.4849, longitude: 28.7685 }, b = { latitude: 55.4852, longitude: 28.7691 }; assert.equal(distanceMeters(a, a), 0); assert.ok(distanceMeters(a, b) > 40 && distanceMeters(a, b) < 60); assert.equal(distanceMeters(a, b), distanceMeters(b, a)); const link = new URL(walkingLink(b)); assert.equal(link.searchParams.get('destination'), '55.4852,28.7691'); assert.equal(link.searchParams.get('travelmode'), 'walking'); });