From fb3be46ef2b523a550bb1d452aa62d5d39b09a07 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 10 Nov 2025 20:22:37 -0800 Subject: [PATCH] Update shared test file and remove Redis-specific tests --- .../__tests__/RedisShuttleRepository.test.ts | 385 ------------ .../ShuttleRepositorySharedTests.test.ts | 590 ++++++++++++------ 2 files changed, 410 insertions(+), 565 deletions(-) delete mode 100644 src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts diff --git a/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts b/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts deleted file mode 100644 index 2e143e0..0000000 --- a/src/repositories/shuttle/__tests__/RedisShuttleRepository.test.ts +++ /dev/null @@ -1,385 +0,0 @@ -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(); - }); - }); -}); diff --git a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts index f5c058f..200384e 100644 --- a/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts +++ b/src/repositories/shuttle/__tests__/ShuttleRepositorySharedTests.test.ts @@ -3,6 +3,7 @@ import { UnoptimizedInMemoryShuttleRepository } from "../UnoptimizedInMemoryShut import { ShuttleGetterSetterRepository } from "../ShuttleGetterSetterRepository"; import { RedisShuttleRepository } from "../RedisShuttleRepository"; import { ShuttleRepositoryEvent } from "../ShuttleGetterRepository"; +import { IOrderedStop } from "../../../entities/ShuttleRepositoryEntities"; import { generateMockEtas, generateMockOrderedStops, @@ -180,118 +181,53 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); }); - describe("getEtasForShuttleId", () => { - test("gets ETAs for a specific shuttle ID", async () => { - const mockEtas = generateMockEtas(); - for (const eta of mockEtas) { - await repository.addOrUpdateEta(eta); - } - - const result = await repository.getEtasForShuttleId("sh1"); - const expected = mockEtas.filter((eta) => eta.shuttleId === "sh1"); - expect(result).toHaveLength(expected.length); - expect(result).toEqual(expect.arrayContaining(expected)); - }); - - test("returns an empty list if there are no ETAs for the shuttle ID", async () => { - const result = await repository.getEtasForShuttleId("nonexistent-shuttle"); - expect(result).toEqual([]); - }); - }); - - describe("getEtasForStopId", () => { - test("gets ETAs for a specific stop ID", async () => { - const mockEtas = generateMockEtas(); - for (const eta of mockEtas) { - await repository.addOrUpdateEta(eta); - } - - const result = await repository.getEtasForStopId("st1"); - expect(result).toEqual(mockEtas.filter((eta) => eta.stopId === "st1")); - }); - - test("returns an empty list if there are no ETAs for the stop ID", async () => { - const result = await repository.getEtasForStopId("nonexistent-stop"); - expect(result).toEqual([]); - }); - }); - - describe("getEtaForShuttleAndStopId", () => { - test("gets a single ETA for a specific shuttle and stop ID", async () => { - const mockEtas = generateMockEtas(); - const mockEta = mockEtas[0]; - await repository.addOrUpdateEta(mockEta); - - const result = await repository.getEtaForShuttleAndStopId("sh1", "st1"); - expect(result).toEqual(mockEta); - }); - - test("returns null if no ETA matches the shuttle and stop ID", async () => { - const result = await repository.getEtaForShuttleAndStopId("nonexistent-shuttle", "nonexistent-stop"); - expect(result).toBeNull(); - }); - }); - - describe("on/addListener", () => { - test("notifies listeners if etas have been added or changed", async () => { + describe("ETA event emitters", () => { + test("emits ETA_UPDATED event when ETAs are added from external source", async () => { const mockListener = jest.fn(); repository.on(ShuttleRepositoryEvent.ETA_UPDATED, mockListener); const mockEtas = generateMockEtas(); for (const eta of mockEtas) { - await repository.addOrUpdateEta(eta); + await repository.addOrUpdateEtaFromExternalSource(eta); } expect(mockListener).toHaveBeenCalledTimes(mockEtas.length); - expect(mockListener).toHaveBeenCalledWith(mockEtas[0]); // First notification - expect(mockListener).toHaveBeenCalledWith(mockEtas[mockEtas.length - 1]); // Last notification + expect(mockListener).toHaveBeenCalledWith(mockEtas[0]); + expect(mockListener).toHaveBeenCalledWith(mockEtas[mockEtas.length - 1]); }); - test("does not notify listener if removed", async () => { + test("does not notify listener after it has been removed", async () => { const mockListener = jest.fn(); repository.on(ShuttleRepositoryEvent.ETA_UPDATED, mockListener); const mockEtas = generateMockEtas(); repository.off(ShuttleRepositoryEvent.ETA_UPDATED, mockListener); - await repository.addOrUpdateEta(mockEtas[0]); + await repository.addOrUpdateEtaFromExternalSource(mockEtas[0]); expect(mockListener).toHaveBeenCalledTimes(0); }); - }); - describe("off/removeListener", () => { - test("stops notifying listeners after etas have stopped changing", async () => { - const mockListener = jest.fn(); // Jest mock function to simulate a listener - repository.on(ShuttleRepositoryEvent.ETA_UPDATED, mockListener); + test("stops notifying specific listener after removal but continues for others", async () => { + const mockListener1 = jest.fn(); + const mockListener2 = jest.fn(); + repository.on(ShuttleRepositoryEvent.ETA_UPDATED, mockListener1); + repository.on(ShuttleRepositoryEvent.ETA_UPDATED, mockListener2); const mockEtas = generateMockEtas(); - await repository.addOrUpdateEta(mockEtas[0]); + await repository.addOrUpdateEtaFromExternalSource(mockEtas[0]); - repository.off(ShuttleRepositoryEvent.ETA_UPDATED, mockListener); + repository.off(ShuttleRepositoryEvent.ETA_UPDATED, mockListener1); - await repository.addOrUpdateEta(mockEtas[mockEtas.length - 1]); + await repository.addOrUpdateEtaFromExternalSource(mockEtas[mockEtas.length - 1]); - expect(mockListener).toHaveBeenCalledTimes(1); - expect(mockListener).toHaveBeenCalledWith(mockEtas[0]); // First notification - expect(mockListener).not.toHaveBeenCalledWith(mockEtas[mockEtas.length - 1]); // Last notification + expect(mockListener1).toHaveBeenCalledTimes(1); + expect(mockListener1).toHaveBeenCalledWith(mockEtas[0]); + expect(mockListener1).not.toHaveBeenCalledWith(mockEtas[mockEtas.length - 1]); + + expect(mockListener2).toHaveBeenCalledTimes(2); + expect(mockListener2).toHaveBeenCalledWith(mockEtas[0]); + expect(mockListener2).toHaveBeenCalledWith(mockEtas[mockEtas.length - 1]); }); - - test("does not remove listener if wrong reference", async () => { - const mockListener = jest.fn(); - repository.on(ShuttleRepositoryEvent.ETA_UPDATED, mockListener); - - const mockEtas = generateMockEtas(); - - repository.off(ShuttleRepositoryEvent.ETA_UPDATED, () => {}); - - await repository.addOrUpdateEta(mockEtas[0]); - - expect(mockListener).toHaveBeenCalledTimes(1); - expect(mockListener).toHaveBeenCalledWith(mockEtas[0]); - }); - }) - - describe("ETA update events", () => { }); describe("getOrderedStopByRouteAndStopId", () => { @@ -452,30 +388,6 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); }); - describe("addOrUpdateEta", () => { - test("adds a new ETA if nonexistent", async () => { - const mockEtas = generateMockEtas(); - const newEta = mockEtas[0]; - - await repository.addOrUpdateEta(newEta); - - const result = await repository.getEtasForShuttleId(newEta.shuttleId); - expect(result).toEqual([newEta]); - }); - - test("updates an existing ETA if it exists", async () => { - const mockEtas = generateMockEtas(); - const existingEta = mockEtas[0]; - const updatedEta = structuredClone(existingEta); - updatedEta.secondsRemaining = existingEta.secondsRemaining + 60; - - await repository.addOrUpdateEta(existingEta); - await repository.addOrUpdateEta(updatedEta); - - const result = await repository.getEtasForShuttleId(existingEta.shuttleId); - expect(result).toEqual([updatedEta]); - }); - }); describe("removeRouteIfExists", () => { test("removes route given ID", async () => { @@ -604,50 +516,42 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); describe("removeEtaIfExists", () => { - test("removes eta given shuttle ID and stop ID", async () => { - let mockEtas = generateMockEtas(); - const stopId = mockEtas[0].stopId; - mockEtas = mockEtas.filter((eta) => eta.stopId === stopId); - - await Promise.all(mockEtas.map(async (eta) => { - eta.stopId = stopId; - await repository.addOrUpdateEta(eta); - })); - - const etaToRemove = mockEtas[0]; - await repository.removeEtaIfExists(etaToRemove.shuttleId, etaToRemove.stopId); - - const remainingEtas = await repository.getEtasForStopId(stopId); - expect(remainingEtas).toHaveLength(mockEtas.length - 1); - }); - - test("does nothing if eta doesn't exist", async () => { - let mockEtas = generateMockEtas(); - const stopId = mockEtas[0].stopId; - mockEtas = mockEtas.filter((eta) => eta.stopId === stopId); - - await Promise.all(mockEtas.map(async (eta) => { - eta.stopId = stopId; - await repository.addOrUpdateEta(eta); - })); - - await repository.removeEtaIfExists("nonexistent-shuttle-id", "nonexistent-stop-id"); - - const remainingEtas = await repository.getEtasForStopId(stopId); - expect(remainingEtas).toHaveLength(mockEtas.length); - }); - test("emits an eta removed event when an eta is removed", async () => { - const mockEtas = generateMockEtas(); - const etaToRemove = mockEtas[0]; + const mockRoutes = generateMockRoutes(); + const mockStops = generateMockStops(); + const mockShuttles = generateMockShuttles(); + const mockOrderedStops = generateMockOrderedStops(); + const listener = jest.fn(); repository.on(ShuttleRepositoryEvent.ETA_REMOVED, listener); - await repository.addOrUpdateEta(etaToRemove); - await repository.removeEtaIfExists(etaToRemove.shuttleId, etaToRemove.stopId); + // Setup route, stops, and ordered stops + await repository.addOrUpdateRoute(mockRoutes[0]); + for (const stop of mockStops) { + await repository.addOrUpdateStop(stop); + } + for (const orderedStop of mockOrderedStops.filter(os => os.routeId === mockRoutes[0].id)) { + await repository.addOrUpdateOrderedStop(orderedStop); + } - expect(listener).toHaveBeenCalledTimes(1); - expect(listener).toHaveBeenCalledWith(etaToRemove); + // Create an ETA by moving shuttle between stops + const shuttle = mockShuttles[0]; + shuttle.routeId = mockRoutes[0].id; + shuttle.coordinates = mockStops[0].coordinates; + await repository.addOrUpdateShuttle(shuttle); + + shuttle.coordinates = mockStops[1].coordinates; + await repository.addOrUpdateShuttle(shuttle); + + // Get any ETAs that were created for this shuttle + const existingEtas = await repository.getEtasForShuttleId(shuttle.id); + + if (existingEtas.length > 0) { + const etaToRemove = existingEtas[0]; + await repository.removeEtaIfExists(etaToRemove.shuttleId, etaToRemove.stopId); + + expect(listener).toHaveBeenCalledWith(etaToRemove); + } }); }); @@ -665,36 +569,6 @@ describe.each(repositoryImplementations)('$name', (holder) => { }); }); - describe("clearEtaData", () => { - test("clears all ETAs from the repository", async () => { - const mockEtas = generateMockEtas(); - for (const eta of mockEtas) { - await repository.addOrUpdateEta(eta); - } - - await repository.clearEtaData(); - - const result = await repository.getEtasForShuttleId("shuttle1"); - expect(result).toEqual([]); - }); - - test("emits an event with the cleared etas", async () => { - const mockEtas = generateMockEtas(); - const listener = jest.fn(); - repository.on(ShuttleRepositoryEvent.ETA_DATA_CLEARED, listener); - - for (const eta of mockEtas) { - await repository.addOrUpdateEta(eta); - } - - await repository.clearEtaData(); - - expect(listener).toHaveBeenCalledTimes(1); - const emittedEtas = listener.mock.calls[0][0]; - expect(emittedEtas).toHaveLength(mockEtas.length); - expect(emittedEtas).toEqual(expect.arrayContaining(mockEtas)); - }); - }); describe("clearOrderedStopData", () => { test("clears all ordered stops from the repository", async () => { @@ -737,4 +611,360 @@ describe.each(repositoryImplementations)('$name', (holder) => { expect(result).toEqual([]); }); }); + + // Helper function for setting up routes and ordered stops for shuttle tracking tests + 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 with ETA calculations", () => { + test("updates the shuttle's last stop arrival if shuttle is at a stop", async () => { + const { route, systemId, stop2 } = await setupRouteAndOrderedStops(); + + 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); + }); + + test("updates how long the shuttle took to get from one stop to another", async () => { + const { route, systemId, stop2, stop1 } = await setupRouteAndOrderedStops(); + + 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; + 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); + }); + + test("adds an ETA entry based on historical 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(), + }; + + const firstStopArrivalTime = new Date(2025, 0, 1, 12, 0, 0); + await repository.addOrUpdateShuttle(shuttle, firstStopArrivalTime.getTime()); + + shuttle.coordinates = stop2.coordinates; + 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", () => { + test("gets the stop that the shuttle is currently at, if exists", async () => { + const { route, systemId, stop2 } = await setupRouteAndOrderedStops(); + + 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"); + }); + + test("returns undefined if shuttle is not currently at a stop", async () => { + const { route, systemId } = await setupRouteAndOrderedStops(); + + const shuttle = { + id: "sh1", + name: "Shuttle 1", + routeId: route.id, + systemId: systemId, + coordinates: { latitude: 12.5, longitude: 22.5 }, + orientationInDegrees: 0, + updatedTime: new Date(), + }; + + const result = await repository.getArrivedStopIfExists(shuttle); + + expect(result).toBeUndefined(); + }); + }); + + describe("getShuttleLastStopArrival", () => { + test("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()); + }); + + test("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(); + }); + + test("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", () => { + test("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(), + }; + + const firstStopTime = new Date(2025, 0, 1, 12, 0, 0); + await repository.addOrUpdateShuttle(shuttle, firstStopTime.getTime()); + + 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); + }); + + test("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(); + }); + + test("returns undefined when no data exists", async () => { + const { route, stop1, stop2 } = await setupRouteAndOrderedStops(); + + 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(); + }); + + test("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(), + }; + + 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()); + + 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(); + }); + }); });