import { beforeEach, describe, it, expect, afterEach } from "@jest/globals"; import { RedisShuttleRepository } from "../RedisShuttleRepository"; import { generateMockShuttles } from "../../../../testHelpers/mockDataGenerators"; import { IOrderedStop } from "../../../entities/ShuttleRepositoryEntities"; describe("RedisShuttleRepository", () => { let repository: RedisShuttleRepository; beforeEach(async () => { repository = new RedisShuttleRepository(); await repository.connect(); }); afterEach(async () => { await repository.clearAllData(); await repository.disconnect(); }); async function setupRouteAndOrderedStops() { const systemId = "sys1"; const route = { id: "r1", name: "Route 1", color: "red", systemId: systemId, polylineCoordinates: [], updatedTime: new Date(), }; await repository.addOrUpdateRoute(route); const stop1 = { id: "st1", name: "Stop 1", systemId: systemId, coordinates: { latitude: 10.0, longitude: 20.0 }, updatedTime: new Date(), }; const stop2 = { id: "st2", name: "Stop 2", systemId: systemId, coordinates: { latitude: 15.0, longitude: 25.0 }, updatedTime: new Date(), }; await repository.addOrUpdateStop(stop1); await repository.addOrUpdateStop(stop2); const orderedStop1: IOrderedStop = { routeId: route.id, stopId: stop1.id, position: 1, systemId: systemId, updatedTime: new Date(), }; const orderedStop2: IOrderedStop = { routeId: route.id, stopId: stop2.id, position: 2, systemId: systemId, updatedTime: new Date(), }; orderedStop1.nextStop = orderedStop2; orderedStop1.previousStop = orderedStop2; orderedStop2.nextStop = orderedStop1; orderedStop2.previousStop = orderedStop1; await repository.addOrUpdateOrderedStop(orderedStop1); await repository.addOrUpdateOrderedStop(orderedStop2); return { route, systemId, stop1, stop2, }; } describe("addOrUpdateShuttle", () => { it("updates the shuttle's last stop arrival if shuttle is at a stop", async () => { const { route, systemId, stop2 } = await setupRouteAndOrderedStops(); // Shuttle positioned at stop2 const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: stop2.coordinates, orientationInDegrees: 0, updatedTime: new Date(), }; await repository.addOrUpdateShuttle(shuttle); const lastStop = await repository.getShuttleLastStopArrival(shuttle.id); expect(lastStop?.stopId).toEqual(stop2.id) }); it("updates how long the shuttle took to get from one stop to another", async () => { const { route, systemId, stop2, stop1 } = await setupRouteAndOrderedStops(); // Start the shuttle at stop 1, then have it move to stop 2 const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: stop1.coordinates, orientationInDegrees: 0, updatedTime: new Date(), } const firstStopArrivalTime = new Date(2025, 0, 1, 12, 0, 0); await repository.addOrUpdateShuttle(shuttle, firstStopArrivalTime.getTime()); shuttle.coordinates = stop2.coordinates; // 15-minute travel time const secondStopArrivalTime = new Date(2025, 0, 1, 12, 15, 0); await repository.addOrUpdateShuttle(shuttle, secondStopArrivalTime.getTime()); const travelTime = await repository.getAverageTravelTimeSeconds({ routeId: route.id, fromStopId: stop1.id, toStopId: stop2.id, }, { from: new Date(2025, 0, 1, 11, 0, 0), to: new Date(2025, 0, 1, 13, 0, 0), }); expect(travelTime).toEqual(15 * 60) }); it("adds an ETA entry based on historical data", async () => { const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); // Start the shuttle at stop 1, then have it move to stop 2 const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: stop1.coordinates, orientationInDegrees: 0, updatedTime: new Date(), } const firstStopArrivalTime = new Date(2025, 0, 1, 12, 0, 0); await repository.addOrUpdateShuttle(shuttle, firstStopArrivalTime.getTime()); shuttle.coordinates = stop2.coordinates; // 15-minute travel time const secondStopArrivalTime = new Date(2025, 0, 1, 12, 15, 0); await repository.addOrUpdateShuttle(shuttle, secondStopArrivalTime.getTime()); shuttle.coordinates = stop1.coordinates; await repository.addOrUpdateShuttle( shuttle, new Date(2025, 0, 8, 12, 0, 0).getTime(), new Date(2025, 0, 8, 12, 7, 30), ); const eta = await repository.getEtaForShuttleAndStopId(shuttle.id, stop2.id); expect(eta?.secondsRemaining).toEqual(7 * 60 + 30); }); }); describe("getArrivedStopIfExists", () => { it("gets the stop that the shuttle is currently at, if exists", async () => { const { route, systemId, stop2 } = await setupRouteAndOrderedStops(); // Create a shuttle positioned at stop2 const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: stop2.coordinates, orientationInDegrees: 0, updatedTime: new Date(), }; const result = await repository.getArrivedStopIfExists(shuttle); expect(result).toBeDefined(); expect(result?.id).toBe("st2"); expect(result?.name).toBe("Stop 2"); }); it("returns undefined if shuttle is not currently at a stop", async () => { const { route, systemId } = await setupRouteAndOrderedStops(); // Create a shuttle positioned between stops (not at any stop) const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: { latitude: 12.5, longitude: 22.5 }, // Between stop1 and stop2 orientationInDegrees: 0, updatedTime: new Date(), }; const result = await repository.getArrivedStopIfExists(shuttle); expect(result).toBeUndefined(); }); }); describe("getShuttleLastStopArrival", () => { it("gets the shuttle's last stop if existing in the data", async () => { const { route, systemId, stop1 } = await setupRouteAndOrderedStops(); const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: stop1.coordinates, orientationInDegrees: 0, updatedTime: new Date(), }; const stopArrivalTime = new Date("2024-01-15T10:30:00Z"); await repository.addOrUpdateShuttle(shuttle, stopArrivalTime.getTime()); const result = await repository.getShuttleLastStopArrival(shuttle.id); expect(result).toBeDefined(); expect(result?.stopId).toBe(stop1.id); expect(result?.timestamp.getTime()).toBe(stopArrivalTime.getTime()); }); it("returns undefined if the data has never been initialized", async () => { const mockShuttles = generateMockShuttles(); const shuttle = mockShuttles[0]; const result = await repository.getShuttleLastStopArrival(shuttle.id); expect(result).toBeUndefined(); }); it("returns the most recent stop arrival when updated multiple times", async () => { const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: stop1.coordinates, orientationInDegrees: 0, updatedTime: new Date(), }; const firstArrivalTime = new Date("2024-01-15T10:30:00Z"); await repository.addOrUpdateShuttle(shuttle, firstArrivalTime.getTime()); shuttle.coordinates = stop2.coordinates; const secondArrivalTime = new Date("2024-01-15T10:35:00Z"); await repository.addOrUpdateShuttle(shuttle, secondArrivalTime.getTime()); const result = await repository.getShuttleLastStopArrival(shuttle.id); expect(result).toBeDefined(); expect(result?.stopId).toBe(stop2.id); expect(result?.timestamp.getTime()).toBe(secondArrivalTime.getTime()); }); }); describe("getAverageTravelTimeSeconds", () => { it("returns the average travel time when historical data exists", async () => { const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: stop1.coordinates, orientationInDegrees: 0, updatedTime: new Date(), }; // Shuttle arrives at stop1 const firstStopTime = new Date(2025, 0, 1, 12, 0, 0); await repository.addOrUpdateShuttle(shuttle, firstStopTime.getTime()); // Shuttle moves to stop2 (15 minutes later) shuttle.coordinates = stop2.coordinates; const secondStopTime = new Date(2025, 0, 1, 12, 15, 0); await repository.addOrUpdateShuttle(shuttle, secondStopTime.getTime()); const travelTime = await repository.getAverageTravelTimeSeconds({ routeId: route.id, fromStopId: stop1.id, toStopId: stop2.id, }, { from: new Date(2025, 0, 1, 11, 0, 0), to: new Date(2025, 0, 1, 13, 0, 0), }); expect(travelTime).toEqual(15 * 60); }); it("returns average of multiple data points", async () => { const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: stop1.coordinates, orientationInDegrees: 0, updatedTime: new Date(), }; // First trip: 10 minutes travel time await repository.addOrUpdateShuttle(shuttle, new Date(2025, 0, 1, 12, 0, 0).getTime()); shuttle.coordinates = stop2.coordinates; await repository.addOrUpdateShuttle(shuttle, new Date(2025, 0, 1, 12, 10, 0).getTime()); // Second trip: 20 minutes travel time shuttle.coordinates = stop1.coordinates; await repository.addOrUpdateShuttle(shuttle, new Date(2025, 0, 1, 12, 30, 0).getTime()); shuttle.coordinates = stop2.coordinates; await repository.addOrUpdateShuttle(shuttle, new Date(2025, 0, 1, 12, 50, 0).getTime()); const averageTravelTime = await repository.getAverageTravelTimeSeconds({ routeId: route.id, fromStopId: stop1.id, toStopId: stop2.id, }, { from: new Date(2025, 0, 1, 11, 0, 0), to: new Date(2025, 0, 1, 14, 0, 0), }); // Average of 10 minutes and 20 minutes = 15 minutes = 900 seconds expect(averageTravelTime).toBeDefined(); }); it("returns undefined when no data exists", async () => { const { route, stop1, stop2 } = await setupRouteAndOrderedStops(); // Don't add any data points, just query for data that doesn't exist const averageTravelTime = await repository.getAverageTravelTimeSeconds({ routeId: route.id, fromStopId: stop1.id, toStopId: stop2.id, }, { from: new Date(2025, 0, 1, 11, 0, 0), to: new Date(2025, 0, 1, 14, 0, 0), }); expect(averageTravelTime).toBeUndefined(); }); it("returns undefined when querying outside the time range of data", async () => { const { route, systemId, stop1, stop2 } = await setupRouteAndOrderedStops(); const shuttle = { id: "sh1", name: "Shuttle 1", routeId: route.id, systemId: systemId, coordinates: stop1.coordinates, orientationInDegrees: 0, updatedTime: new Date(), }; // Add data on Jan 1 await repository.addOrUpdateShuttle(shuttle, new Date(2025, 0, 1, 12, 0, 0).getTime()); shuttle.coordinates = stop2.coordinates; await repository.addOrUpdateShuttle(shuttle, new Date(2025, 0, 1, 12, 15, 0).getTime()); // Query for Jan 2 (no data should exist in this range) const averageTravelTime = await repository.getAverageTravelTimeSeconds({ routeId: route.id, fromStopId: stop1.id, toStopId: stop2.id, }, { from: new Date(2025, 0, 2, 11, 0, 0), to: new Date(2025, 0, 2, 13, 0, 0), }); expect(averageTravelTime).toBeUndefined(); }); }); });