import { beforeEach, describe, expect, it, jest, test } from "@jest/globals"; import { ApiBasedRepositoryLoader, ApiResponseError } from "../../src/loaders/ApiBasedRepositoryLoader"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; import { fetchSystemDataSuccessfulResponse } from "../jsonSnapshots/fetchSystemData/fetchSystemDataSuccessfulResponse"; import { fetchSystemDataFailedResponse } from "../jsonSnapshots/fetchSystemData/fetchSystemDataFailedResponse"; import { fetchRouteDataSuccessfulResponse } from "../jsonSnapshots/fetchRouteData/fetchRouteDataSuccessfulResponse"; import { fetchStopAndPolylineDataSuccessfulResponse } from "../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse"; import { generateMockEtas, generateMockOrderedStops, generateMockRoutes, generateMockShuttles, generateMockStops, generateMockSystems } from "../generators"; import { IStop } from "../../src/entities/entities"; import { fetchShuttleDataSuccessfulResponse } from "../jsonSnapshots/fetchShuttleData/fetchShuttleDataSuccessfulResponse"; import { fetchEtaDataSuccessfulResponse } from "../jsonSnapshots/fetchEtaData/fetchEtaDataSuccessfulResponse"; import { resetGlobalFetchMockJson, updateGlobalFetchMockJson, updateGlobalFetchMockJsonToThrowSyntaxError } from "../testHelpers/fetchMockHelpers"; async function assertAsyncCallbackThrowsApiResponseError(callback: () => Promise) { await expect(callback).rejects.toThrow(ApiResponseError); } describe("ApiBasedRepositoryLoader", () => { let loader: ApiBasedRepositoryLoader; beforeEach(() => { loader = new ApiBasedRepositoryLoader(new UnoptimizedInMemoryRepository()); resetGlobalFetchMockJson(); }); 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); } else { expect(systems).toHaveLength(numberOfSystemsInResponse); } }); it("throws the correct error if the API response contains no data", async () => { updateGlobalFetchMockJson(fetchSystemDataFailedResponse); await assertAsyncCallbackThrowsApiResponseError(async () => { await loader.fetchAndUpdateSystemData(); }); }); it("throws the correct error if HTTP status code is not 200", async () => { updateGlobalFetchMockJson(fetchSystemDataFailedResponse, 400); await assertAsyncCallbackThrowsApiResponseError(async () => { await loader.fetchAndUpdateSystemData(); }); }); }); describe("fetchAndUpdateRouteDataForExistingSystemsInRepository", () => { test("calls fetchAndUpdateRouteDataForSystemId for all systems in repository", async () => { const spy = jest.spyOn(loader, "fetchAndUpdateRouteDataForSystemId"); const systems = generateMockSystems(); await Promise.all(systems.map(async (system) => { await loader.repository.addOrUpdateSystem(system); })); await loader.fetchAndUpdateRouteDataForExistingSystemsInRepository(); expect(spy.mock.calls.length).toBe(systems.length); }); }); 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); // Act await loader.fetchAndUpdateRouteDataForSystemId(systemId); // Assert const routes = await loader.repository.getRoutesBySystemId(systemId); expect(routes.length).toEqual(fetchRouteDataSuccessfulResponse.all.length) }); it("throws the correct error if the API response contains no data", async () => { // The Passio API returns some invalid JSON if there is no data, // so simulate a JSON parsing error updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { await loader.fetchAndUpdateRouteDataForSystemId(systemId); }); }); }); describe("fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystemsInRepository", () => { it("calls fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId for every system", async () => { const spy = jest.spyOn(loader, "fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId"); const systems = generateMockSystems(); await Promise.all(systems.map(async (system) => { await loader.repository.addOrUpdateSystem(system); })); await loader.fetchAndUpdateStopAndPolylineDataForRoutesInExistingSystemsInRepository(); expect(spy.mock.calls.length).toBe(systems.length); }); }) 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(systemId); const stops = await loader.repository.getStopsBySystemId(systemId); expect(stops.length).toEqual(stopsArray.length); await Promise.all(stops.map(async (stop) => { const orderedStops = await loader.repository.getOrderedStopsByStopId(stop.id) expect(orderedStops.length).toBeGreaterThan(0); })); const routes = await loader.repository.getRoutesBySystemId(systemId); routes.forEach((route) => { expect(route.polylineCoordinates.length).toBeGreaterThan(0); }); }); it("throws the correct error if the API response contains no data", async () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId(systemId); }); }) }); describe("fetchAndUpdateShuttleDataForExistingSystemsInRepository", () => { it("calls fetchAndUpdateShuttleDataForSystemId for every system", async () => { const spy = jest.spyOn(loader, "fetchAndUpdateShuttleDataForSystemId"); const systems = generateMockSystems(); await Promise.all(systems.map(async (system) => { await loader.repository.addOrUpdateSystem(system); })) await loader.fetchAndUpdateShuttleDataForExistingSystemsInRepository(); expect(spy.mock.calls.length).toBe(systems.length); }); }); 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(systemId); const shuttles = await loader.repository.getShuttlesBySystemId(systemId); expect(shuttles.length).toEqual(busesInResponse.length); }); it("throws the correct error if the API response contains no data", async () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { await loader.fetchAndUpdateShuttleDataForSystemId(systemId); }); }); }); describe("fetchAndUpdateEtaDataForExistingStopsForSystemsInRepository", () => { it("calls fetchAndUpdateEtaDataFoExistingStopsForSystemId for every system in repository", async () => { const spy = jest.spyOn(loader, "fetchAndUpdateEtaDataForExistingStopsForSystemId"); const systems = generateMockSystems(); await Promise.all(systems.map(async (system) => { await loader.repository.addOrUpdateSystem(system); })); await loader.fetchAndUpdateEtaDataForExistingStopsForSystemsInRepository(); expect(spy.mock.calls.length).toBe(systems.length); }); }); describe("fetchAndUpdateEtaDataForExistingStopsForSystemId", () => { it("calls fetchAndUpdateEtaDataForStopId for every stop in repository", async () => { const spy = jest.spyOn(loader, "fetchAndUpdateEtaDataForStopId"); const stops = generateMockStops(); stops.forEach((stop) => { stop.systemId = "1"; }); await Promise.all(stops.map(async (stop) => { await loader.repository.addOrUpdateStop(stop); })); await loader.fetchAndUpdateEtaDataForExistingStopsForSystemId("1"); expect(spy.mock.calls.length).toEqual(stops.length); }); }); describe("fetchAndUpdateEtaDataForStopId", () => { const stopId = "177666"; it("updates ETA data for stop id if response received", async () => { updateGlobalFetchMockJson(fetchEtaDataSuccessfulResponse); // @ts-ignore const etasFromResponse = fetchEtaDataSuccessfulResponse.ETAs[stopId] await loader.fetchAndUpdateEtaDataForStopId(stopId); const etas = await loader.repository.getEtasForStopId(stopId); expect(etas.length).toEqual(etasFromResponse.length); }); it("throws the correct error if the API response contains no data", async () => { updateGlobalFetchMockJsonToThrowSyntaxError(); await assertAsyncCallbackThrowsApiResponseError(async () => { await loader.fetchAndUpdateEtaDataForStopId("263"); }); }); }); });