diff --git a/src/app.ts b/src/app.ts index c5508a6..cd789ba 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,12 +1,14 @@ import express from "express"; -import mbus from "./routes/api" +import mbus from "./routes/api"; +import v4 from "./routes/v4"; import * as documented from "./routes/documented"; const app = express(); app.use(express.json()); documented.addRouter(documented.globalContext, app, "/mbus/api/v3", mbus); +documented.addRouter(documented.globalContext, app, "/api/v4", v4); app.use("/docs", express.static("docs")); const PORT = process.env.PORT || 3000; diff --git a/src/routes/api.ts b/src/routes/api.ts index eda7f80..9e64f97 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -9,6 +9,7 @@ import * as journeyService from '../services/journey'; import * as reminderService from '../services/reminder'; import * as graphBuilder from '../services/graphBuilder'; import { startBackgroundJobs } from '../jobs'; +import { BusRouteLineSchema } from "../services/bustimeCommon"; import * as documented from "./documented"; /** @@ -188,27 +189,15 @@ export function getRidePositions(req: express.Request, res: express.Response) { } router.get('/getRidePositions', getRidePositions); -/** - * Returns all cached route patterns. - * @param req - Express request - * @param res - Express response - * @returns JSON object with `routes` mapping route IDs to patterns. - */ -export function getAllRoutes(req: express.Request, res: express.Response) { - res.json({ routes: state.cachedRoutes }); -} -router.get('/getAllRoutes', getAllRoutes); +// remove when mb2 support is dropped +router.get('/getAllRoutes', (_, res) => { + res.json({ routes: state.cachedRoutesLegacy }) +}); -/** - * Returns all cached ride route patterns. - * @param req - Express request - * @param res - Express response - * @returns JSON object with `routes` mapping route IDs to patterns. - */ -export function getAllRideRoutes(req: express.Request, res: express.Response) { - res.json({ routes: state.cachedRideRoutes }); -} -router.get('/getAllRideRoutes', getAllRideRoutes); +// remove when mb2 support is dropped +router.get('/getAllRideRoutes', (_, res) => { + res.json({ routes: state.cachedRideRoutesLegacy }); +}); /** * Returns the route timing cache used for extrapolation. diff --git a/src/routes/v4.ts b/src/routes/v4.ts new file mode 100644 index 0000000..5615284 --- /dev/null +++ b/src/routes/v4.ts @@ -0,0 +1,31 @@ +/** + * Changes to the served api that are NOT backwards compatible with mb2 should go here. + * + * Try to use documented instead of raw express. + * @module + */ + +import express from 'express'; +import * as z from 'zod'; +import * as state from '@/state/transitState'; +import { BusRouteLineSchema } from '@/services/bustimeCommon'; +import * as documented from './documented'; + +const router = express.Router(); +const ctx = documented.globalContext; + +documented.addGetRoute( + ctx, router, '/getAllMbusRoutes', + { ...documented.emptyFormat, resBody: z.array(BusRouteLineSchema) }, + async () => documented.makeSuccessResponse(Object.values(state.cachedRoutes).flat(1)), + { description: 'get all cached route patterns' } +); + +documented.addGetRoute( + ctx, router, '/getAllRideRoutes', + { ...documented.emptyFormat, resBody: z.array(BusRouteLineSchema) }, + async () => documented.makeSuccessResponse(Object.values(state.cachedRideRoutes).flat(1)), + { description: 'get all cached ride route patterns' }, +) + +export default router; diff --git a/src/services/bustimeCommon.ts b/src/services/bustimeCommon.ts new file mode 100644 index 0000000..d5d6596 --- /dev/null +++ b/src/services/bustimeCommon.ts @@ -0,0 +1,144 @@ +import z from "zod"; + +const PatternPtSchema = z.object({ + seq: z.int(), + typ: z.string(), + stpid: z.optional(z.string()), + stpnm: z.optional(z.string()), + pdist: z.optional(z.number()), + lat: z.number(), + lon: z.number(), +}).meta({ id: 'PatternPt' }); + +export const PatternSchema = z.object({ + pid: z.int(), + ln: z.number(), + rtdir: z.string(), + pt: z.array(PatternPtSchema), + dtrid: z.optional(z.string()), + dtrpt: z.optional(z.array(PatternPtSchema)), +}).meta({ id: 'Pattern' }); +export type Pattern = z.infer + +export const PatternsArraySchema = z.array(PatternSchema); + +export const LatLonSchema = z.object({ lat: z.number(), lon: z.number() }).meta({ id: 'LatLon' }); + +export const BusStopSchema = z.object({ + id: z.string(), + name: z.string(), + location: LatLonSchema, + routeId: z.string(), + rotation: z.number(), + isRide: z.boolean(), +}).meta({ id: 'BusStop' }); +export type BusStop = z.infer; + +export function makeBusStop( + { id, name, lat, lon }: { id?: string, name?: string, lat?: number, lon?: number }, + routeId: string, rotation: number, isRide: boolean +): BusStop { + return { + id: id ?? '', + name: name ? normalizeStopName(name) : '', + location: { lat: lat ?? 0, lon: lon ?? 0 }, + routeId, rotation, isRide, + }; +} + +/** doesn't include color or image url, which are still handled by the frontend */ +export const BusRouteLineSchema = z.object({ + routeId: z.string(), + routeDirection: z.string(), + points: z.array(LatLonSchema), + stops: z.array(z.object({ index: z.int(), stop: BusStopSchema })), +}).meta({ id: 'BusRouteLine' }); +export type BusRouteLine = z.infer; + +export function makeBusRouteLines(rt: string, pattern: Pattern, isRide: boolean): BusRouteLine[] { + + const process = (pointList: Pattern['pt']): { + points: BusRouteLine['points'], + stops: BusRouteLine['stops'] + } => { + const points = []; + const stops = []; + for (let i = 0; i < pointList.length; i++) { + const point = pointList[i]; + const isLast = i == pointList.length - 1; // bool to check if last + points.push({ lat: point.lat, lon: point.lon }); + if (point.typ === 'S') { + // get rotation of stop + let stopRotation; + if (isLast) { + // use the previous 2 points to calculate rotation + stopRotation = pointRotation( + pointList[i - 2]?.lat ?? 0, + pointList[i - 2]?.lon ?? 0, + pointList[i - 1]?.lat ?? 0, + pointList[i - 1]?.lon ?? 0, + ); + } else { + // use the next 2 points to calculate rotation + stopRotation = pointRotation( + pointList[i + 1]?.lat ?? 0, + pointList[i + 1]?.lon ?? 0, + pointList[i + 2]?.lat ?? 0, + pointList[i + 2]?.lon ?? 0, + ); + } + stops.push({ + index: i, + stop : makeBusStop( + { id: point.stpid, name: point.stpnm, lat: point.lat, lon: point.lon }, + rt, stopRotation, isRide + ) + }); + } + } + return { points, stops }; + } + + const lines: BusRouteLine[] = []; + { + const { points, stops } = process(pattern.pt); + lines.push({ routeId: rt, points, stops, routeDirection: pattern.rtdir }); + } + + // Handle detour points if present + if (pattern.dtrpt) { + const { points, stops } = process(pattern.dtrpt); + lines.push({ routeId: rt, points, stops, routeDirection: pattern.rtdir }); + } + + return lines; +} + +/** + * Function to calculate rotation angle between two geographical points + * (used for bus stop icon orientation) + */ +export function pointRotation(lat1: number, lon1: number, lat2: number, lon2: number): number { + const dLat = lat2 - lat1; + const dLon = lon2 - lon1; + + // Scale longitude by cos(lat) to correct for east-west distance + const x = dLon * (Math.cos(lat1 * Math.PI / 180.0)); + const y = dLat; + + let angle = Math.atan2(x, y) * 180.0 / Math.PI; + + // Normalize to [0, 360) + if (angle < 0) angle += 360; + + return angle; +} + +// KEEP THIS IN SYNC WITH THE CORRESPONDING FUNCTION IN THE FRONTEND +function normalizeStopName(rawStopName: string): string { + return rawStopName + .replaceAll('%', '') + .replaceAll(/\s+/g, ' ') + .trim(); +} + diff --git a/src/services/bustimeTypes.ts b/src/services/bustimeTypes.ts deleted file mode 100644 index f34cf5b..0000000 --- a/src/services/bustimeTypes.ts +++ /dev/null @@ -1,25 +0,0 @@ -import z from "zod"; - -const PatternPtSchema = z.object({ - seq: z.number(), - typ: z.string(), - stpid: z.optional(z.string()), - stpnm: z.optional(z.string()), - pdist: z.optional(z.number()), - lat: z.number(), - lon: z.number(), -}); - -export const PatternSchema = z.object({ - pid: z.number(), - ln: z.number(), - rtdir: z.string(), - pt: z.array(PatternPtSchema), - dtrid: z.optional(z.string()), - dtrpt: z.optional(z.array(PatternPtSchema)), -}); - -export const PatternsArraySchema = z.array(PatternSchema); - -export type Pattern = z.infer - diff --git a/src/services/graphBuilder.ts b/src/services/graphBuilder.ts index 7a8bcca..158fc3c 100644 --- a/src/services/graphBuilder.ts +++ b/src/services/graphBuilder.ts @@ -7,6 +7,7 @@ import * as process from "node:process"; import { MaxPriorityQueue } from '@datastructures-js/priority-queue'; import * as fs from 'fs'; import * as path from 'path'; +import { makeBusRouteLines } from './bustimeCommon'; const DEFAULT_ROUTES = ["BB", "CN", "CS", "CSX", "DD", "MX", "NE", "NW", "NX", "OS", "NES", "WS", "WX"]; const DEFAULT_RIDE_ROUTES = ["3", "4", "5", "6", "22", "23", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "42", "43", "44", "45", "46", "47", "61", "62", "63", "64", "65", "66", "67", "68", "104"]; @@ -32,13 +33,15 @@ export async function initializeRoutes() { await Promise.all(routesData.map(async (r: any) => { state.validRoutes.add(r.rt); const patterns = await mbus.fetchPatterns(r.rt); - if (patterns) state.cachedRoutes[r.rt] = patterns; + state.cachedRoutes[r.rt] = patterns.map((p) => makeBusRouteLines(r.rt, p, false)).flat(1); + state.cachedRoutesLegacy[r.rt] = patterns; })); await Promise.all(rideRoutesData.map(async (r: any) => { state.validRideRoutes.add(r.rt); const patterns = await rideBus.fetchPatterns(r.rt); - if (patterns) state.cachedRideRoutes[r.rt] = patterns; + state.cachedRideRoutes[r.rt] = patterns.map((p) => makeBusRouteLines(r.rt, p, true)).flat(1); + state.cachedRideRoutesLegacy[r.rt] = patterns; })); buildStopLocationMap(); @@ -57,11 +60,10 @@ export async function rebuildGraph() { try { console.log(`Rebuilding graph...`); const allStopIds = new Set(); - Object.values(state.cachedRoutes).forEach((patterns) => { - patterns.forEach((p) => p.pt.forEach((pt) => { - if (pt.stpid) allStopIds.add(pt.stpid); - })); - }); + Object.values(state.cachedRoutes) + .flat(1) + .flatMap((line) => line.stops) + .forEach(({ index: _, stop }) => allStopIds.add(stop.id)); const rawPreds = await mbus.fetchPredictions(Array.from(allStopIds), DEFAULT_ROUTES); const formattedPreds = processPredictions(rawPreds); @@ -74,11 +76,10 @@ export async function rebuildGraph() { // extra stuff to update the busses for the ride const rideStopIds = new Set(); - Object.values(state.cachedRideRoutes).forEach((patterns) => { - patterns.forEach((p) => p.pt.forEach((pt) => { - if (pt.stpid) rideStopIds.add(pt.stpid); - })); - }); + Object.values(state.cachedRideRoutes) + .flat(1) + .flatMap((line) => line.stops) + .forEach(({ index: _, stop }) => rideStopIds.add(stop.id)); const rawRidePreds = await rideBus.fetchPredictions(Array.from(rideStopIds), DEFAULT_RIDE_ROUTES); const formattedRidePreds = processRidePredictions(rawRidePreds); populateRideLookupMaps(formattedRidePreds); @@ -100,13 +101,10 @@ export async function rebuildGraph() { * @param preds List of processed predictions */ function populateLookupMaps(preds: any[]) { - Object.values(state.cachedRoutes).forEach((patterns) => { - patterns.forEach((p) => p.pt.forEach((pt) => { - if (pt.stpid && pt.stpnm) { - state.stopIdToName[pt.stpid] = pt.stpnm; - } - })); - }); + Object.values(state.cachedRoutes) + .flat(1) + .flatMap((line) => line.stops) + .forEach(({ index: _, stop }) => state.stopIdToName[stop.id] = stop.name); preds.forEach((trip: any) => { trip.stops.forEach((stop: any) => { if (stop.stpid && stop.stpnm) { @@ -130,13 +128,10 @@ function populateLookupMaps(preds: any[]) { * @param preds List of processed predictions from the ride */ function populateRideLookupMaps(preds: any[]) { - Object.values(state.cachedRideRoutes).forEach((patterns) => { - patterns.forEach((p) => p.pt.forEach((pt) => { - if (pt.stpid && pt.stpnm) { - state.rideStopIdToName[pt.stpid] = pt.stpnm; - } - })); - }); + Object.values(state.cachedRideRoutes) + .flat(1) + .flatMap((line) => line.stops) + .forEach(({ index: _, stop }) => state.rideStopIdToName[stop.id] = stop.name); preds.forEach((trip: any) => { trip.stops.forEach((stop: any) => { if (stop.stpid && stop.stpnm) { @@ -152,13 +147,12 @@ function populateRideLookupMaps(preds: any[]) { */ function buildStopLocationMap() { const locs: Record = {}; - Object.values(state.cachedRoutes).forEach((patterns) => { - patterns.forEach((p) => p.pt.forEach((pt) => { - if (pt.stpid && pt.lat) { - locs[pt.stpid] = { name: pt.stpnm, lat: pt.lat, lon: pt.lon }; - } - })); - }); + Object.values(state.cachedRoutes) + .flat(1) + .flatMap((line) => line.stops) + .forEach(({ index: _, stop }) => + locs[stop.id] = { name: stop.name, lat: stop.location.lat, lon: stop.location.lon } + ); state.setCachedStopLocations(locs); walking.buildStopNodeMap(locs); } @@ -168,13 +162,11 @@ function buildStopLocationMap() { */ function buildRideStops() { const locs: Record = {}; - Object.values(state.cachedRideRoutes).forEach((patterns) => { - patterns.forEach((p) => p.pt.forEach((pt) => { - if (pt.stpid && pt.lat) { - locs[pt.stpid] = { name: pt.stpnm, lat: pt.lat, lon: pt.lon }; - } - })); - }); + Object.values(state.cachedRideRoutes) + .flat(1) + .flatMap((line) => line.stops) + .forEach(({ index: _, stop }) => + locs[stop.id] = { name: stop.name, lat: stop.location.lat, lon: stop.location.lon }); state.setCachedRideStopLocations(locs); } @@ -244,13 +236,11 @@ function processPredictions(rawChunks: any[]) { const routeInfoFilter: Record = {}; for (const [routeName, routeList] of Object.entries(state.cachedRoutes)) { for (const route of routeList) { - const rtdir = route.rtdir; + const rtdir = route.routeDirection; const routeKey = routeName + rtdir; if (!routeInfoFilter[routeKey]) routeInfoFilter[routeKey] = []; - for (const point of route.pt) { - if (point.typ !== "W" && point.stpid) { - routeInfoFilter[routeKey].push({ stpid: point.stpid, rtdir }); - } + for (const { index: _, stop } of route.stops) { + routeInfoFilter[routeKey].push({ stpid: stop.id, rtdir }); } } } diff --git a/src/services/mbus.ts b/src/services/mbus.ts index 168977e..e163c9d 100644 --- a/src/services/mbus.ts +++ b/src/services/mbus.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import * as process from "node:process"; import dotenv from "dotenv"; -import { Pattern, PatternsArraySchema } from './bustimeTypes'; +import { Pattern, PatternsArraySchema } from './bustimeCommon'; dotenv.config(); diff --git a/src/services/ride.ts b/src/services/ride.ts index 38ca704..cb7a08b 100644 --- a/src/services/ride.ts +++ b/src/services/ride.ts @@ -3,7 +3,7 @@ import axios from 'axios'; import * as process from "node:process"; import dotenv from "dotenv"; -import { Pattern, PatternsArraySchema } from './bustimeTypes'; +import { Pattern, PatternsArraySchema } from './bustimeCommon'; dotenv.config(); diff --git a/src/state/transitState.ts b/src/state/transitState.ts index 2d69ecc..037c2ed 100644 --- a/src/state/transitState.ts +++ b/src/state/transitState.ts @@ -1,14 +1,18 @@ -import { Pattern } from "@/services/bustimeTypes"; import { Trip, TransfersByOrigin, Interchange } from "../raptor/types"; +import { BusRouteLine, Pattern } from "@/services/bustimeCommon"; /** Current positions of all buses. */ export const curBusPositions = { buses: [] as any[] }; /** Current positions of all ride buses. */ export const curRidePositions = { buses: [] as any[] }; /** Cache of route patterns and static data. */ -export const cachedRoutes: Record = {}; +export const cachedRoutes: Record = {}; /** Cache of route patterns and static data for the ride. */ -export const cachedRideRoutes: Record = {}; +export const cachedRideRoutes: Record = {}; + +// Remove when support for mb2 is dropped +export const cachedRoutesLegacy: Record = {}; +export const cachedRideRoutesLegacy: Record = {}; /** Represents a bus prediction. */ export type Prediction = { diff --git a/test/api.test.ts b/test/api.test.ts index 328e7dc..18d7f38 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; const SERVER_PORT = 3000; const BASE_URL = `http://localhost:${SERVER_PORT}/mbus/api/v3`; +const V4_BASE_URL = `http://localhost:${SERVER_PORT}/api/v4`; describe('API Endpoints', () => { beforeAll(async () => { @@ -41,7 +42,7 @@ describe('API Endpoints', () => { console.log(`GET /getSelectableRoutes: ${response.data['bustime-response'].routes.length} routes found.`); }); - it('should get all cached routes and confirm structure', async () => { + it('should get all cached routes and confirm structure (mb2 legacy)', async () => { const response = await axios.get(`${BASE_URL}/getAllRoutes`); expect(response.status).toBe(200); expect(response.data).toHaveProperty('routes'); @@ -49,6 +50,13 @@ describe('API Endpoints', () => { console.log(`GET /getAllRoutes: ${Object.keys(response.data.routes).length} cached routes found.`); }); + it('should get all cached routes and confirm structure', async () => { + const response = await axios.get(`${V4_BASE_URL}/getAllMbusRoutes`); + expect(response.status).toBe(200); + expect(typeof response.data).toBe('object'); // should be array + console.log(`GET /getAllMbusRoutes: ${Object.keys(response.data).length} cached routes found.`); + }); + it('should get all bus predictions and log stop IDs', async () => { try { const response = await axios.get(`${BASE_URL}/getAllPredictions`); @@ -171,13 +179,13 @@ describe('API Endpoints', () => { expect(response.status).toBe(200); expect(response.data).toHaveProperty('journeys'); expect(Array.isArray(response.data.journeys)).toBe(true); - console.log('GET /plan-journey (test 1):', JSON.stringify(response.data.journeys, null, 2)); + // console.log('GET /plan-journey (test 1):', JSON.stringify(response.data.journeys, null, 2)); const response2 = await axios.get(`${BASE_URL}/plan-journey?originLat=42.27389558&originLon=-83.73739576&destLat=42.29303061&destLon=-83.7163671`); expect(response2.status).toBe(200); expect(response2.data).toHaveProperty('journeys'); expect(Array.isArray(response2.data.journeys)).toBe(true); - console.log('GET /plan-journey (test 2):', JSON.stringify(response2.data.journeys, null, 2)); + // console.log('GET /plan-journey (test 2):', JSON.stringify(response2.data.journeys, null, 2)); } catch (error) { if (axios.isAxiosError(error)) { console.error('Error fetching path:', error.message); diff --git a/test/bustimeCommon.test.ts b/test/bustimeCommon.test.ts new file mode 100644 index 0000000..3449f97 --- /dev/null +++ b/test/bustimeCommon.test.ts @@ -0,0 +1,99 @@ +import { makeBusRouteLines, Pattern } from "@/services/bustimeCommon"; +import { describe, expect, it } from "vitest"; + +describe('makeBusRouteLines', () => { + + it('should handle short routes', () => { + const pointsSingleStop: Pattern['pt'] = [ + makeStop(0, 45.0, 46.0, 'C1', 'Central'), + ]; + const pattern = makePattern(0, 0, '', pointsSingleStop, '', pointsSingleStop); + const lines = makeBusRouteLines('', pattern, false); + expect(lines.length).toBe(2); + expect(lines[0]).toEqual(lines[1]); + const line = lines[0]; + expect(line.points).toEqual([{ lat: 45.0, lon: 46.0 }]); + const stop = line.stops[0].stop; + expect(stop.id).toEqual('C1'); + expect(stop.name).toEqual('Central'); + expect(stop.location.lat).toEqual(45); + expect(stop.location.lon).toEqual(46); + }); + + it('should pass through isRide and rt', () => { + for (const rt of ["BB", "CN"]) { + for (const isRide of [true, false]) { + const pointsSingleStop: Pattern['pt'] = [ + makeStop(0, 45.0, 46.0, 'C1', 'Central'), + ]; + const pattern = makePattern(0, 0, '', pointsSingleStop, null, null); + const lines = makeBusRouteLines(rt, pattern, isRide); + const line = lines[0]; + expect(line.routeId).toEqual(rt); + for (const stop of line.stops) { + expect(stop.stop.routeId).toBe(rt); + expect(stop.stop.isRide).toBe(isRide); + } + } + } + }); + + it('should handle both route and detour', () => { + const points1: Pattern['pt'] = [ + makeStop(0, 45.0, 46.0, 'C1', 'Central'), + makeWaypoint(1, 45.1, 45.9), + makeStop(2, 45.2, 45.8, 'C2', 'Community'), + ]; + const points2: Pattern['pt'] = [ + makeStop(0, 45.0, 46.0, 'C1', 'Central'), + makeWaypoint(1, 0.0, 0.0), + makeWaypoint(1, 2.0, 2.0), + makeStop(2, 45.2, 45.8, 'C2', 'Community'), + ]; + + const positions = (points: Pattern['pt']) => + points.map((p) => { return { lat: p.lat, lon: p.lon}; }); + + for (const [points, detourPts] of [[points1, points2], [points2, points1]]) { + const pattern = makePattern(0, 0, '', points, '', detourPts); + const lines = makeBusRouteLines('', pattern, false); + expect(lines[0].points).toEqual(positions(points)); + expect(lines[1].points).toEqual(positions(detourPts)); + } + }); +}); + +function makeStop(seq: number, lat: number, lon: number, stpid: string, stpnm: string): Pattern['pt'][0] { + return { + seq, + typ: "S", + lat, + lon, + pdist: 0.0, + stpid, + stpnm, + }; +} + +function makeWaypoint(seq: number, lat: number, lon: number): Pattern['pt'][0] { + return { + seq, + typ: "W", + lat, + lon, + }; +} + +function makePattern( + pid: number, ln: number, rtdir: string, points: Pattern['pt'], + dtrid: string | null, dtrpt: Pattern['pt'] | null, +): Pattern { + return { + pid: pid, + ln: ln, + rtdir: rtdir, + pt: points, + dtrid: dtrid ?? undefined, + dtrpt: dtrpt ?? undefined, + }; +} diff --git a/test/ride.test.ts b/test/ride.test.ts index b58d451..9c1125b 100644 --- a/test/ride.test.ts +++ b/test/ride.test.ts @@ -3,6 +3,7 @@ import { describe, it, expect } from 'vitest'; const SERVER_PORT = 3000; const BASE_URL = `http://localhost:${SERVER_PORT}/mbus/api/v3`; +const V4_BASE_URL = `http://localhost:${SERVER_PORT}/api/v4`; describe('The Ride (AAATA) API Endpoints', () => { @@ -24,7 +25,7 @@ describe('The Ride (AAATA) API Endpoints', () => { }); // --- Ride Routes --- - it('should get all Ride routes', async () => { + it('should get all Ride routes (mb2 legacy)', async () => { const response = await axios.get(`${BASE_URL}/getAllRideRoutes`); expect(response.status).toBe(200); expect(response.data).toHaveProperty('routes'); @@ -35,6 +36,16 @@ describe('The Ride (AAATA) API Endpoints', () => { expect(routeCount).toBeGreaterThanOrEqual(0); }); + it('should get all Ride routes', async () => { + const response = await axios.get(`${V4_BASE_URL}/getAllRideRoutes`); + expect(response.status).toBe(200); + expect(typeof response.data).toBe('object'); + + const routeCount = Object.keys(response.data).length; + console.log(`GET /getAllRideRoutes: ${routeCount} Ride routes found.`); + expect(routeCount).toBeGreaterThanOrEqual(0); + }); + // --- Ride Stops --- it('should get all Ride stops', async () => { const response = await axios.get(`${BASE_URL}/getAllRideStops`);