diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 9c4436652..6cfe9fe4e 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -873,5 +873,6 @@ "src/tools/queries/aviation.test.mjs", "src/tools/queries/space.test.mjs", "src/tools/queries/media.test.mjs", - "src/tools/queries/places.test.mjs" + "src/tools/queries/places.test.mjs", + "src/layers/transit/source.test.mjs" ] diff --git a/src/layers/transit/source.js b/src/layers/transit/source.js index 6ccdb9e55..702b5ed00 100644 --- a/src/layers/transit/source.js +++ b/src/layers/transit/source.js @@ -5,6 +5,21 @@ export function createTransitSource({ fetchImpl = (...args) => fetch(...args), } = {}) { return { + /** Read the public catalog of enabled feeds. */ + async getFeeds({ signal } = {}) { + signal?.throwIfAborted(); + const response = await fetchImpl('/api/transit/feeds', { + signal, + headers: { Accept: 'application/json' }, + }); + if (!response.ok) + throw new Error('Transit feeds HTTP ' + response.status); + const body = await response.json(); + signal?.throwIfAborted(); + if (!Array.isArray(body?.feeds)) + throw new Error('Malformed transit feed catalog'); + return body.feeds; + }, getHistory(feedId, vehicleId, { signal } = {}) { signal?.throwIfAborted(); return fetchTransitHistory(feedId, vehicleId, signal, fetchImpl); diff --git a/src/layers/transit/source.test.mjs b/src/layers/transit/source.test.mjs new file mode 100644 index 000000000..88d66664a --- /dev/null +++ b/src/layers/transit/source.test.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createTransitSource } from './source.js'; + +const json = (body, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + +test('the feed catalog is read from the public catalog route', async () => { + const requested = []; + const feeds = [ + { + id: 'mbta', + name: 'MBTA', + center: { lat: 42.36, lon: -71.06 }, + loadRadiusKm: 60, + }, + ]; + const source = createTransitSource({ + fetchImpl: async (url) => { + requested.push(url); + return json({ feeds }); + }, + }); + assert.deepEqual(await source.getFeeds(), feeds); + assert.deepEqual(requested, ['/api/transit/feeds']); +}); + +test('feed catalog failures are reported', async () => { + const failing = createTransitSource({ fetchImpl: async () => json({}, 503) }); + await assert.rejects(failing.getFeeds(), /Transit feeds HTTP 503/); + const malformed = createTransitSource({ + fetchImpl: async () => json({ feeds: {} }), + }); + await assert.rejects(malformed.getFeeds(), /Malformed transit feed catalog/); + const aborted = new AbortController(); + aborted.abort(); + await assert.rejects( + createTransitSource({ + fetchImpl: async () => json({ feeds: [] }), + }).getFeeds({ signal: aborted.signal }), + (error) => error.name === 'AbortError', + ); +});