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 { ISystem } from "../../src/entities/entities"; import { fetchStopAndPolylineDataSuccessfulResponse } from "../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse"; /** * Function to update behavior of the global `fetch` function. * Note that the Passio GO API returns status code 200 for failed responses. * @param obj * @param status */ function updateGlobalFetchMockJson( obj: any, status: number = 200 ) { // @ts-ignore global.fetch = jest.fn(() => { return Promise.resolve({ json: () => Promise.resolve(obj), status, ok: status.toString().startsWith("2"), // 200-level codes are OK }) }) as jest.Mock; } /** * Reset the global fetch function mock's JSON to return an empty object. * @param obj */ function resetGlobalFetchMockJson() { updateGlobalFetchMockJson({}); } async function assertAsyncCallbackThrowsApiResponseError(callback: () => Promise) { await expect(callback).rejects.toThrow(ApiResponseError); } function updateGlobalFetchMockJsonToThrowSyntaxError() { // @ts-ignore global.fetch = jest.fn(() => { return Promise.resolve({ json: () => Promise.reject(new SyntaxError("Unable to parse JSON")), status: 200, ok: true, }) }) as jest.Mock; } describe("ApiBasedRepositoryLoader", () => { let loader: ApiBasedRepositoryLoader; beforeEach(() => { loader = new ApiBasedRepositoryLoader(new UnoptimizedInMemoryRepository()); resetGlobalFetchMockJson(); }); describe("fetchAndUpdateSystemData", () => { it("updates system data in repository if response received", async () => { const numberOfSystemsInResponse = fetchSystemDataSuccessfulResponse.all.length; updateGlobalFetchMockJson(fetchSystemDataSuccessfulResponse); await loader.fetchAndUpdateSystemData(); 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: ISystem[] = [ { name: "Chapman University", id: "1", }, { name: "City of Monterey Park", id: "2", } ]; await Promise.all(systems.map(async (system) => { await loader.repository.addOrUpdateSystem(system); })); await loader.fetchAndUpdateRouteDataForExistingSystemsInRepository(); expect(spy.mock.calls.length).toBe(2); }); }); describe("fetchAndUpdateRouteDataForSystemId", () => { it("updates route data in repository if there are systems and response received", async () => { updateGlobalFetchMockJson(fetchRouteDataSuccessfulResponse); await loader.fetchAndUpdateRouteDataForSystemId("263"); const routes = await loader.repository.getRoutesBySystemId("263"); 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("263"); }); }); }); describe("fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId", () => { it("updates stop and polyline data if there are systems and response received", async () => { updateGlobalFetchMockJson(fetchStopAndPolylineDataSuccessfulResponse); const stopsArray = Object.values(fetchStopAndPolylineDataSuccessfulResponse.stops); await loader.fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId("263"); const stops = await loader.repository.getStopsBySystemId("263"); 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("263"); 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("263"); }); }) }); describe("fetchAndUpdateShuttleDataForExistingSystems", () => { it("updates shuttle data in repository if there are systems and response received", async () => { }); it("throws the correct error if the API response contains no data", async () => { }); }); describe("fetchAndUpdateEtaDataForExistingSystems", () => { it("updates shuttle data in repository if there are systems and response received", async () => { }); it("throws the correct error if the API response contains no data", async () => { }); }); });