diff --git a/src/loaders/ApiBasedRepositoryLoader.ts b/src/loaders/ApiBasedRepositoryLoader.ts index de48179..af8fd66 100644 --- a/src/loaders/ApiBasedRepositoryLoader.ts +++ b/src/loaders/ApiBasedRepositoryLoader.ts @@ -1,5 +1,5 @@ import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; -import { IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; +import { IEntityWithId, IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; export class ApiResponseError extends Error { constructor(message: string) { @@ -8,6 +8,11 @@ export class ApiResponseError extends Error { } } +/** + * Class which can load data into a repository from the + * Passio Go API. Supports automatic pruning of all data types + * which inherit from `IEntityWithId`. + */ export class ApiBasedRepositoryLoader { supportedSystemIds = ["263"]; baseUrl = "https://passiogo.com/mapGetData.php"; @@ -17,12 +22,25 @@ export class ApiBasedRepositoryLoader { ) { } + private async constructExistingEntityIdSet(entitySearchCallback: () => Promise) { + const existingEntities = await entitySearchCallback(); + const ids = new Set(); + existingEntities.forEach((entity) => { + ids.add(entity.id); + }); + return ids; + } + public async fetchAndUpdateSystemData() { const params = { getSystems: "2", }; const query = new URLSearchParams(params).toString(); + const systemIds = await this.constructExistingEntityIdSet(async () => { + return await this.repository.getSystems(); + }) + try { const response = await fetch(`${this.baseUrl}?${query}`); const json = await response.json(); @@ -41,10 +59,16 @@ export class ApiBasedRepositoryLoader { }; await this.repository.addOrUpdateSystem(constructedSystem); + systemIds.delete(constructedSystem.id); })); } else { throw new Error("Received JSON object does not contain `all` field") } + + // Prune systems + await Promise.all(Array.from(systemIds).map(async (systemId) => { + await this.repository.removeSystemIfExists(systemId); + })); } catch(e: any) { throw new ApiResponseError(e.message); } @@ -58,6 +82,10 @@ export class ApiBasedRepositoryLoader { } public async fetchAndUpdateRouteDataForSystemId(systemId: string) { + const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => { + return await this.repository.getRoutesBySystemId(systemId); + }); + const params = { getRoutes: "2", }; @@ -89,8 +117,14 @@ export class ApiBasedRepositoryLoader { }; await this.repository.addOrUpdateRoute(constructedRoute); + + routeIdsToPrune.delete(constructedRoute.id); })) } + + await Promise.all(Array.from(routeIdsToPrune).map(async (routeId) => { + await this.repository.removeRouteIfExists(routeId); + })); } catch(e: any) { throw new ApiResponseError(e.message); } @@ -106,6 +140,10 @@ export class ApiBasedRepositoryLoader { public async fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId: string) { // Fetch from the API // Pass JSON output into two different methods to update repository + const stopIdsToPrune = await this.constructExistingEntityIdSet(async () => { + return await this.repository.getStopsBySystemId(systemId); + }); + const params = { getStops: "2", }; @@ -126,9 +164,13 @@ export class ApiBasedRepositoryLoader { }); const json = await response.json(); - await this.updateStopDataForSystemAndApiResponse(systemId, json); + await this.updateStopDataForSystemAndApiResponse(systemId, json, stopIdsToPrune); await this.updateOrderedStopDataForExistingStops(json); await this.updatePolylineDataForExistingRoutesAndApiResponse(json); + + await Promise.all(Array.from(stopIdsToPrune).map(async (stopId) => { + await this.repository.removeStopIfExists(stopId); + })); } catch(e: any) { throw new ApiResponseError(e.message); } @@ -143,6 +185,10 @@ export class ApiBasedRepositoryLoader { } public async fetchAndUpdateShuttleDataForSystemId(systemId: string) { + const shuttleIdsToPrune = await this.constructExistingEntityIdSet(async () => { + return await this.repository.getShuttlesBySystemId(systemId); + }); + const params = { getBuses: "2" }; @@ -182,8 +228,14 @@ export class ApiBasedRepositoryLoader { } await this.repository.addOrUpdateShuttle(constructedShuttle); + + shuttleIdsToPrune.delete(constructedShuttle.id); })); } + + await Promise.all(Array.from(shuttleIdsToPrune).map(async (shuttleId) => { + await this.repository.removeShuttleIfExists(shuttleId); + })); } catch(e: any) { throw new ApiResponseError(e.message); } @@ -240,7 +292,11 @@ export class ApiBasedRepositoryLoader { } } - protected async updateStopDataForSystemAndApiResponse(systemId: string, json: any) { + protected async updateStopDataForSystemAndApiResponse( + systemId: string, + json: any, + setOfIdsToPrune: Set = new Set(), + ) { if (json.stops) { const jsonStops = Object.values(json.stops); @@ -256,6 +312,8 @@ export class ApiBasedRepositoryLoader { }; await this.repository.addOrUpdateStop(constructedStop); + + setOfIdsToPrune.delete(constructedStop.id); })); } } diff --git a/src/loaders/TimedApiBasedRepositoryLoader.ts b/src/loaders/TimedApiBasedRepositoryLoader.ts index 89fe34b..863ccc9 100644 --- a/src/loaders/TimedApiBasedRepositoryLoader.ts +++ b/src/loaders/TimedApiBasedRepositoryLoader.ts @@ -47,15 +47,10 @@ export class TimedApiBasedRepositoryLoader extends ApiBasedRepositoryLoader { if (!this.shouldBeRunning) return; try { - await this.repository.clearSystemData(); await this.fetchAndUpdateSystemData(); - await this.repository.clearRouteData(); await this.fetchAndUpdateRouteDataForExistingSystemsInRepository(); - await this.repository.clearStopData(); await this.fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystemsInRepository(); - await this.repository.clearShuttleData(); await this.fetchAndUpdateShuttleDataForExistingSystemsInRepository(); - await this.repository.clearEtaData(); await this.fetchAndUpdateEtaDataForExistingStopsForSystemsInRepository(); } catch (e) { console.error(e); diff --git a/src/repositories/GetterSetterRepository.ts b/src/repositories/GetterSetterRepository.ts index ddcf68b..1c914c0 100644 --- a/src/repositories/GetterSetterRepository.ts +++ b/src/repositories/GetterSetterRepository.ts @@ -19,6 +19,13 @@ export interface GetterSetterRepository extends GetterRepository { addOrUpdateOrderedStop(orderedStop: IOrderedStop): Promise; addOrUpdateEta(eta: IEta): Promise; + removeSystemIfExists(systemId: string): Promise; + removeRouteIfExists(routeId: string): Promise; + removeShuttleIfExists(shuttleId: string): Promise; + removeStopIfExists(stopId: string): Promise; + removeOrderedStopIfExists(stopId: string, routeId: string): Promise; + removeEtaIfExists(shuttleId: string, stopId: string): Promise; + clearSystemData(): Promise; clearRouteData(): Promise; clearShuttleData(): Promise; diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryRepository.ts index c24a3c5..938f4ea 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryRepository.ts @@ -1,13 +1,5 @@ import { GetterSetterRepository } from "./GetterSetterRepository"; -import { - IEntityWithId, - IEta, - IOrderedStop, - IRoute, - IShuttle, - IStop, - ISystem -} from "../entities/entities"; +import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; /** * An unoptimized in memory repository. @@ -148,6 +140,51 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { } } + private async removeEntityByMatcherIfExists(callback: (value: T) => boolean, arrayToSearchIn: T[]) { + const index = arrayToSearchIn.findIndex(callback); + if (index > -1) { + const entityToReturn = arrayToSearchIn[index]; + arrayToSearchIn.splice(index, 1); + return entityToReturn; + } + + return null; + } + + private async removeEntityByIdIfExists(entityId: string, arrayToSearchIn: T[]) { + return await this.removeEntityByMatcherIfExists((value) => value.id === entityId, arrayToSearchIn); + } + + public async removeSystemIfExists(systemId: string): Promise { + return await this.removeEntityByIdIfExists(systemId, this.systems); + } + + public async removeRouteIfExists(routeId: string): Promise { + return await this.removeEntityByIdIfExists(routeId, this.routes); + } + + public async removeShuttleIfExists(shuttleId: string): Promise { + return await this.removeEntityByIdIfExists(shuttleId, this.shuttles); + } + + public async removeStopIfExists(stopId: string): Promise { + return await this.removeEntityByIdIfExists(stopId, this.stops); + } + + public async removeOrderedStopIfExists(stopId: string, routeId: string): Promise { + return await this.removeEntityByMatcherIfExists((orderedStop) => { + return orderedStop.stopId === stopId + && orderedStop.routeId === routeId + }, this.orderedStops); + } + + public async removeEtaIfExists(shuttleId: string, stopId: string): Promise { + return await this.removeEntityByMatcherIfExists((eta) => { + return eta.stopId === stopId + && eta.shuttleId === shuttleId + }, this.etas); + } + public async clearSystemData() { this.systems = []; } @@ -171,4 +208,5 @@ export class UnoptimizedInMemoryRepository implements GetterSetterRepository { public async clearStopData(): Promise { this.stops = []; } + } \ No newline at end of file diff --git a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts index 7bb4e7f..173f696 100644 --- a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/ApiBasedRepositoryLoaderTests.test.ts @@ -7,7 +7,14 @@ import { fetchRouteDataSuccessfulResponse } from "../jsonSnapshots/fetchRouteDat import { fetchStopAndPolylineDataSuccessfulResponse } from "../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse"; -import { generateMockStops, generateMockSystems } from "../generators"; +import { + generateMockEtas, + generateMockOrderedStops, + generateMockRoutes, + generateMockShuttles, + generateMockStops, + generateMockSystems +} from "../generators"; import { IStop } from "../../src/entities/entities"; import { fetchShuttleDataSuccessfulResponse @@ -33,11 +40,19 @@ describe("ApiBasedRepositoryLoader", () => { describe("fetchAndUpdateSystemData", () => { it("updates system data in repository if response received", async () => { + // Arrange + const systemsToPrune = generateMockSystems(); + await Promise.all(systemsToPrune.map(async (system) => { + await loader.repository.addOrUpdateSystem(system); + })); + const numberOfSystemsInResponse = fetchSystemDataSuccessfulResponse.all.length; updateGlobalFetchMockJson(fetchSystemDataSuccessfulResponse); + // Act await loader.fetchAndUpdateSystemData(); + // Assert const systems = await loader.repository.getSystems(); if (loader.supportedSystemIds.length < numberOfSystemsInResponse) { expect(systems).toHaveLength(loader.supportedSystemIds.length); @@ -81,12 +96,23 @@ describe("ApiBasedRepositoryLoader", () => { }); describe("fetchAndUpdateRouteDataForSystemId", () => { + const systemId = "263"; it("updates route data in repository if response received", async () => { + // Arrange + // Test pruning + const routesToPrune = generateMockRoutes(); + await Promise.all(routesToPrune.map(async (route) => { + route.systemId = systemId; + await loader.repository.addOrUpdateRoute(route); + })); + updateGlobalFetchMockJson(fetchRouteDataSuccessfulResponse); - await loader.fetchAndUpdateRouteDataForSystemId("263"); + // Act + await loader.fetchAndUpdateRouteDataForSystemId(systemId); - const routes = await loader.repository.getRoutesBySystemId("263"); + // Assert + const routes = await loader.repository.getRoutesBySystemId(systemId); expect(routes.length).toEqual(fetchRouteDataSuccessfulResponse.all.length) }); @@ -98,7 +124,7 @@ describe("ApiBasedRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateRouteDataForSystemId("263"); + await loader.fetchAndUpdateRouteDataForSystemId(systemId); }); }); }); @@ -120,14 +146,23 @@ describe("ApiBasedRepositoryLoader", () => { }) describe("fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId", () => { + const systemId = "263"; it("updates stop and polyline data if response received", async () => { + // Arrange + // Test pruning of stops only + const stopsToPrune = generateMockStops(); + await Promise.all(stopsToPrune.map(async (stop) => { + stop.systemId = systemId; + await loader.repository.addOrUpdateStop(stop); + })); + updateGlobalFetchMockJson(fetchStopAndPolylineDataSuccessfulResponse); const stopsArray = Object.values(fetchStopAndPolylineDataSuccessfulResponse.stops); - await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId("263"); + await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId); - const stops = await loader.repository.getStopsBySystemId("263"); + const stops = await loader.repository.getStopsBySystemId(systemId); expect(stops.length).toEqual(stopsArray.length); await Promise.all(stops.map(async (stop) => { @@ -135,7 +170,7 @@ describe("ApiBasedRepositoryLoader", () => { expect(orderedStops.length).toBeGreaterThan(0); })); - const routes = await loader.repository.getRoutesBySystemId("263"); + const routes = await loader.repository.getRoutesBySystemId(systemId); routes.forEach((route) => { expect(route.polylineCoordinates.length).toBeGreaterThan(0); }); @@ -145,7 +180,7 @@ describe("ApiBasedRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId("263"); + await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId); }); }) }); @@ -166,13 +201,20 @@ describe("ApiBasedRepositoryLoader", () => { }); describe("fetchAndUpdateShuttleDataForSystemId", () => { + const systemId = "263"; it("updates shuttle data in repository if response received", async () => { + const shuttlesToPrune = generateMockShuttles(); + await Promise.all(shuttlesToPrune.map(async (shuttle) => { + shuttle.systemId = systemId; + await loader.repository.addOrUpdateShuttle(shuttle); + })) + updateGlobalFetchMockJson(fetchShuttleDataSuccessfulResponse); const busesInResponse = Object.values(fetchShuttleDataSuccessfulResponse.buses); - await loader.fetchAndUpdateShuttleDataForSystemId("263"); + await loader.fetchAndUpdateShuttleDataForSystemId(systemId); - const shuttles = await loader.repository.getShuttlesBySystemId("263"); + const shuttles = await loader.repository.getShuttlesBySystemId(systemId); expect(shuttles.length).toEqual(busesInResponse.length); }); @@ -181,7 +223,7 @@ describe("ApiBasedRepositoryLoader", () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { - await loader.fetchAndUpdateShuttleDataForSystemId("263"); + await loader.fetchAndUpdateShuttleDataForSystemId(systemId); }); }); }); @@ -221,9 +263,9 @@ describe("ApiBasedRepositoryLoader", () => { }); describe("fetchAndUpdateEtaDataForStopId", () => { + const stopId = "177666"; it("updates ETA data for stop id if response received", async () => { updateGlobalFetchMockJson(fetchEtaDataSuccessfulResponse); - const stopId = "177666"; // @ts-ignore const etasFromResponse = fetchEtaDataSuccessfulResponse.ETAs[stopId] diff --git a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts index ec37052..18f4616 100644 --- a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts @@ -1,10 +1,9 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader"; import { resetGlobalFetchMockJson } from "../mockHelpers/fetchMockHelpers"; -import { GetterSetterRepository } from "../../src/repositories/GetterSetterRepository"; +import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; describe("TimedApiBasedRepositoryLoader", () => { - let repositoryMock: GetterSetterRepository; let loader: TimedApiBasedRepositoryLoader; let spies: any; @@ -16,15 +15,7 @@ describe("TimedApiBasedRepositoryLoader", () => { beforeEach(() => { resetGlobalFetchMockJson(); - repositoryMock = { - clearSystemData: jest.fn(), - clearRouteData: jest.fn(), - clearStopData: jest.fn(), - clearShuttleData: jest.fn(), - clearEtaData: jest.fn(), - } as unknown as GetterSetterRepository; - - loader = new TimedApiBasedRepositoryLoader(repositoryMock); + loader = new TimedApiBasedRepositoryLoader(new UnoptimizedInMemoryRepository()); spies = { fetchAndUpdateSystemData: jest.spyOn(loader, 'fetchAndUpdateSystemData'), @@ -49,9 +40,6 @@ describe("TimedApiBasedRepositoryLoader", () => { await loader.start(); expect(loader["shouldBeRunning"]).toBe(true); - Object.values(repositoryMock).forEach((mockFn) => { - expect(mockFn).toHaveBeenCalled(); - }); Object.values(spies).forEach((spy: any) => { expect(spy).toHaveBeenCalled(); }); @@ -64,9 +52,6 @@ describe("TimedApiBasedRepositoryLoader", () => { await loader.start(); await loader.start(); - Object.values(repositoryMock).forEach((mockFn) => { - expect(mockFn).toHaveBeenCalledTimes(1); - }); Object.values(spies).forEach((spy: any) => { expect(spy).toHaveBeenCalledTimes(1); }); diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts index e9e8166..906894b 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts @@ -429,6 +429,194 @@ describe("UnoptimizedInMemoryRepository", () => { }); }); + describe("removeSystemIfExists", () => { + test("removes system given ID", async () => { + const mockSystems = generateMockSystems(); + await Promise.all(mockSystems.map(async (system) => { + await repository.addOrUpdateSystem(system); + })); + + const systemToRemove = mockSystems[0]; + await repository.removeSystemIfExists(systemToRemove.id); + + const remainingSystems = await repository.getSystems(); + expect(remainingSystems).toHaveLength(mockSystems.length - 1); + }); + + test("does nothing if system doesn't exist", async () => { + const mockSystems = generateMockSystems(); + await Promise.all(mockSystems.map(async (system) => { + await repository.addOrUpdateSystem(system); + })); + + await repository.removeSystemIfExists("nonexistent-id"); + + const remainingSystems = await repository.getSystems(); + expect(remainingSystems).toHaveLength(mockSystems.length); + }); + }); + + describe("removeRouteIfExists", () => { + test("removes route given ID", async () => { + const systemId = "1"; + const mockRoutes = generateMockRoutes(); + await Promise.all(mockRoutes.map(async (route) => { + route.systemId = systemId; + await repository.addOrUpdateRoute(route); + })); + + const routeToRemove = mockRoutes[0]; + await repository.removeRouteIfExists(routeToRemove.id); + + const remainingRoutes = await repository.getRoutesBySystemId(systemId); + expect(remainingRoutes).toHaveLength(mockRoutes.length - 1); + }); + + test("does nothing if route doesn't exist", async () => { + const systemId = "1"; + const mockRoutes = generateMockRoutes(); + await Promise.all(mockRoutes.map(async (route) => { + route.systemId = systemId; + await repository.addOrUpdateRoute(route); + })); + + await repository.removeRouteIfExists("nonexistent-id"); + + const remainingRoutes = await repository.getRoutesBySystemId(systemId); + expect(remainingRoutes).toHaveLength(mockRoutes.length); + }); + }); + + describe("removeShuttleIfExists", () => { + test("removes shuttle given ID", async () => { + const systemId = "1"; + const mockShuttles = generateMockShuttles(); + await Promise.all(mockShuttles.map(async (shuttle) => { + shuttle.systemId = systemId; + await repository.addOrUpdateShuttle(shuttle); + })); + + const shuttleToRemove = mockShuttles[0]; + await repository.removeShuttleIfExists(shuttleToRemove.id); + + const remainingShuttles = await repository.getShuttlesBySystemId(systemId); + expect(remainingShuttles).toHaveLength(mockShuttles.length - 1); + }); + + test("does nothing if shuttle doesn't exist", async () => { + const systemId = "1"; + const mockShuttles = generateMockShuttles(); + await Promise.all(mockShuttles.map(async (shuttle) => { + shuttle.systemId = systemId; + await repository.addOrUpdateShuttle(shuttle); + })); + + await repository.removeShuttleIfExists("nonexistent-id"); + + const remainingShuttles = await repository.getShuttlesBySystemId(systemId); + expect(remainingShuttles).toHaveLength(mockShuttles.length); + }); + }); + + describe("removeStopIfExists", () => { + test("removes stop given ID", async () => { + const systemId = "1"; + const mockStops = generateMockStops(); + await Promise.all(mockStops.map(async (stop) => { + stop.systemId = systemId; + await repository.addOrUpdateStop(stop); + })); + + const stopToRemove = mockStops[0]; + await repository.removeStopIfExists(stopToRemove.id); + + const remainingStops = await repository.getStopsBySystemId(systemId); + expect(remainingStops).toHaveLength(mockStops.length - 1); + }); + + test("does nothing if stop doesn't exist", async () => { + const systemId = "1"; + const mockStops = generateMockStops(); + await Promise.all(mockStops.map(async (stop) => { + stop.systemId = systemId; + await repository.addOrUpdateStop(stop); + })); + + await repository.removeStopIfExists("nonexistent-id"); + + const remainingStops = await repository.getStopsBySystemId(systemId); + expect(remainingStops).toHaveLength(mockStops.length); + }); + }); + + describe("removeOrderedStopIfExists", () => { + test("removes ordered stop given stop ID and route ID", async () => { + let mockOrderedStops = generateMockOrderedStops(); + const routeId = mockOrderedStops[0].routeId; + mockOrderedStops = mockOrderedStops.filter((orderedStop) => orderedStop.routeId === routeId); + await Promise.all(mockOrderedStops.map(async (stop) => { + stop.routeId = routeId; + await repository.addOrUpdateOrderedStop(stop); + })); + + const orderedStopToRemove = mockOrderedStops[0]; + await repository.removeOrderedStopIfExists(orderedStopToRemove.stopId, orderedStopToRemove.routeId); + + const remainingOrderedStops = await repository.getOrderedStopsByRouteId(routeId); + expect(remainingOrderedStops).toHaveLength(mockOrderedStops.length - 1); + }); + + test("does nothing if ordered stop doesn't exist", async () => { + let mockOrderedStops = generateMockOrderedStops(); + const routeId = mockOrderedStops[0].routeId; + mockOrderedStops = mockOrderedStops.filter((orderedStop) => orderedStop.routeId === routeId); + await Promise.all(mockOrderedStops.map(async (stop) => { + stop.routeId = routeId; + await repository.addOrUpdateOrderedStop(stop); + })); + + await repository.removeOrderedStopIfExists("nonexistent-stop-id", "nonexistent-route-id"); + + const remainingOrderedStops = await repository.getOrderedStopsByRouteId(routeId); + expect(remainingOrderedStops).toHaveLength(mockOrderedStops.length); + }); + }); + + 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); + }); + }); + describe("clearSystemData", () => { test("clears all systems from the repository", async () => { const mockSystems = generateMockSystems();