import { beforeEach, describe, expect, jest, test } from "@jest/globals"; import { ApiBasedRepository, ApiBasedRepositoryCache, ApiBasedRepositoryMillisecondTTLs } from "../../src/repositories/ApiBasedRepository"; import { IEta, IShuttle, IStop } from "../../src/entities/entities"; /** * Update the global fetch function to return a specific object. * @param obj */ function updateGlobalFetchMockJson(obj: any) { // @ts-ignore global.fetch = jest.fn(() => { return Promise.resolve({ json: () => Promise.resolve(obj) }) }) as jest.Mock; } /** * Reset the global fetch function mock's JSON to return an empty object. * @param obj */ function resetGlobalFetchMockJson() { updateGlobalFetchMockJson({}) } beforeEach(() => { resetGlobalFetchMockJson(); }) // Snapshots taken from the Passio GO! API const genericEtaDataByStopId = { "ETAs": { "177666": [ { "OOS": 0, "busName": "08", "distance": 1, "speed": 10.028535400123669, "routeBlockId": "142270", "actualRouteBlockId": "142270", "arrived": null, "eta": "10 min ", "color": "#000000", "bg": "#ffea3f", "order": 0, "dwell": null, "stopsAmount": 2, "secondsSpent": 587, "etaR": "10", "error": null, "outdated": 0, "routeId": "53966", "serviceTime": "", "scheduleTimes": [], "goShowSchedule": 0, "looping": "1", "routeGroupId": "6703", "busId": 5577, "tripId": 751430, "deviceId": 402840, "created": "2025-01-07 15:00:09", "routePointPosition": 6, "routeStopPosition": 1, "stopRoutePointPosition": 217, "timezoneOffset": -10800, "busLatLng": [ 33.7933406, -117.8539321 ], "busProjectionLatlng": { "lat": 33.79331052666975, "lng": -117.85392945849208 }, "busProjectionError": 3, "stopId": "177666", "theStop": { "name": "Chapman Court", "position": 3, "userId": "263", "routeStopId": "1348785", "busId": 5577, "routeName": "Red Route", "shortName": null, "routeId": "53966", "stopId": "177666" } }, { "OOS": 0, "busName": "07", "distance": 1, "speed": 12.160256921380398, "routeBlockId": "142270", "actualRouteBlockId": "142270", "arrived": null, "eta": "11 min ", "color": "#000000", "bg": "#ffea3f", "order": 0, "dwell": null, "stopsAmount": 2, "secondsSpent": 635, "etaR": "11", "error": null, "outdated": 0, "routeId": "53966", "serviceTime": "", "scheduleTimes": [], "goShowSchedule": 0, "looping": "1", "routeGroupId": "6703", "busId": 5576, "tripId": 751430, "deviceId": 441065, "created": "2025-01-07 15:00:10", "routePointPosition": 448, "routeStopPosition": 4, "stopRoutePointPosition": 217, "timezoneOffset": -10800, "busLatLng": [ 33.7933284, -117.855132 ], "busProjectionLatlng": { "lat": 33.79332033922653, "lng": -117.85513217762522 }, "busProjectionError": 1, "stopId": "177666", "theStop": { "name": "Chapman Court", "position": 3, "userId": "263", "routeStopId": "1348785", "busId": 5576, "routeName": "Red Route", "shortName": null, "routeId": "53966", "stopId": "177666" } }, ] }, } const genericShuttleDataBySystemId = { "alertCRC": "23c1b91c", "buses": { "402840": [ { "deviceId": 402840, "created": "08:24 PM", "createdTime": "08:24 PM", "paxLoad": 0, "bus": "08", "busId": 5577, "userId": "263", "routeBlockId": "142270", "latitude": "33.791781800", "longitude": "-117.858964600", "calculatedCourse": "351.796001302109", "outOfService": 0, "more": "102", "totalCap": 20, "color": "#d62728", "busName": "08", "busType": "", "routeId": "53966", "route": "Red Route", "outdated": 0 } ], "404873": [ { "deviceId": 404873, "created": "08:24 PM", "createdTime": "08:24 PM", "paxLoad": 0, "bus": "10", "busId": 7105, "userId": "263", "routeBlockId": "142270", "latitude": "33.789331300", "longitude": "-117.888790600", "calculatedCourse": "76.005762226701", "outOfService": 0, "more": "101", "totalCap": 20, "color": "#d62728", "busName": "10", "busType": "", "routeId": "53966", "route": "Red Route", "outdated": 0 } ], "421421": [ { "deviceId": 421421, "created": "08:24 PM", "createdTime": "08:24 PM", "paxLoad": 0, "bus": "17", "busId": 12502, "userId": "263", "routeBlockId": "142660", "latitude": "33.790699500", "longitude": "-117.890385500", "calculatedCourse": "10.65684824528148", "outOfService": 0, "more": "101", "totalCap": 32, "color": "#bd9e39", "busName": "17", "busType": "Shuttle Bus", "routeId": "54256", "route": "Gold Route", "outdated": 0 } ], "441065": [ { "deviceId": 441065, "created": "08:19 PM", "createdTime": "08:19 PM", "paxLoad": 0, "bus": "07", "busId": 5576, "userId": "263", "routeBlockId": "142270", "latitude": "33.793278900", "longitude": "-117.852629400", "calculatedCourse": "299.74488110904485", "outOfService": 0, "more": "22", "totalCap": 20, "color": "#d62728", "busName": "07", "busType": "", "routeId": "53966", "route": "Red Route", "outdated": 0 } ] }, "microtime": 0.023222923278808594, "time": { "263": "08:24 PM" } } describe("getEtaForShuttleAndStopId", () => { test("getEtaForShuttleAndStopId returns correct ETA data", async () => { updateGlobalFetchMockJson(genericEtaDataByStopId); const initialCache: ApiBasedRepositoryCache = { etasForStopId: { "177666": [ { secondsRemaining: 587, shuttleId: "5577", stopId: "177666", millisecondsSinceEpoch: Date.now(), } ], }, } const repository = new ApiBasedRepository(initialCache); repository.getShuttleById = jest.fn(async () => { const shuttle: IShuttle = { id: "5577", name: "08", coordinates: { latitude: 33.7933406, longitude: -117.8539321, }, routeId: "53966", systemId: "1", }; return shuttle; }); repository.updateEtasForSystemIfTTL = jest.fn(async () => { }); const result = await repository.getEtaForShuttleAndStopId("5577", "177666"); expect(result?.secondsRemaining).toEqual(587); expect(result?.millisecondsSinceEpoch).toBeDefined(); }); test("getEtaForShuttleAndStopId returns null if API call is invalid and cache is empty", async () => { const repository = new ApiBasedRepository(); const result = await repository.getEtaForShuttleAndStopId("5577", "177666"); expect(result).toEqual(null); }); }); describe("getEtasForShuttleId", () => { test("getEtasForShuttleId returns correct ETA data", async () => { updateGlobalFetchMockJson(genericEtaDataByStopId); const initialCache: ApiBasedRepositoryCache = { etasForStopId: {}, etasForShuttleId: { "5577": [ { secondsRemaining: 587, shuttleId: "5577", stopId: "177666", millisecondsSinceEpoch: Date.now(), } ] }, }; const ttls: ApiBasedRepositoryMillisecondTTLs = { etasForShuttleId: 100000, etasForStopId: 100000, }; const repository = new ApiBasedRepository(initialCache, ttls); repository.updateEtasForSystemIfTTL = jest.fn(async () => { }); repository.getShuttleById = jest.fn(async () => { const shuttle: IShuttle = { id: "5577", name: "08", coordinates: { latitude: 33.7933406, longitude: -117.8539321, }, routeId: "53966", systemId: "1", }; return shuttle; }); const result = await repository.getEtasForShuttleId("5577"); // @ts-ignore expect(result).toEqual(initialCache.etasForShuttleId["5577"]); }); test("getEtasForShuttleId returns empty array if no data available", async () => { const repository = new ApiBasedRepository(); repository.updateEtasForSystemIfTTL = jest.fn(async () => { }); const result = await repository.getEtasForShuttleId("5577"); expect(result).toEqual([]); }); }); describe("getEtasForStopId", () => { test("getEtasForStopId returns correct ETA data", async () => { // Because I'm testing updateEtasForSystemIfTTL separately, // stub it out here updateGlobalFetchMockJson(genericEtaDataByStopId); const initialCache: ApiBasedRepositoryCache = { etasForStopId: { "177666": [ { secondsRemaining: 587, shuttleId: "5577", stopId: "177666", millisecondsSinceEpoch: Date.now(), } ] }, etasForShuttleId: {} }; const ttls: ApiBasedRepositoryMillisecondTTLs = { etasForShuttleId: 100000, etasForStopId: 100000, }; const repository = new ApiBasedRepository(initialCache, ttls); repository.getStopById = jest.fn(async () => { const stop: IStop = { name: "Chapman Court", systemId: "1", id: "177666", coordinates: { latitude: 33.796796, longitude: -117.889293 }, }; return stop; }); repository.updateEtasForSystemIfTTL = jest.fn(async () => { }); const result = await repository.getEtasForStopId("177666"); expect(result).toEqual(initialCache.etasForStopId!["177666"]); }); test("getEtasForStopId returns empty array if no data available", async () => { const repository = new ApiBasedRepository(); repository.updateEtasForSystemIfTTL = jest.fn(async () => { }); const result = await repository.getEtasForShuttleId("5577"); expect(result).toEqual([]); }); }); describe("updateEtasForSystemIfTTL", () => { // test("updateEtasForSystemIfTTL does nothing if data is not TTL", async () => { // updateGlobalFetchMockJson(genericEtaDataByStopId); // // // If ETA data is not TTL, then don't do anything // const expectedEta: IEta = { // secondsRemaining: 587, // shuttleId: "5577", // stopId: "177666", // millisecondsSinceEpoch: Date.now() - 1000, // }; // // const initialCache: ApiBasedRepositoryCache = { // etasForShuttleId: { // "5577": [ // expectedEta, // ], // }, // etasForStopId: { // "177666": [ // expectedEta, // ], // }, // stopsBySystemId: { // "1": [ // { // systemId: "1", // millisecondsSinceEpoch: Date.now() - 1000, // name: "Chapman Court", // id: "177666", // coordinates: { // latitude: 33.796796, // longitude: -117.889293 // }, // } // ], // }, // }; // // const ttls: ApiBasedRepositoryMillisecondTTLs = { // etasForShuttleId: 100000, // etasForStopId: 100000, // }; // // const repository = new ApiBasedRepository(initialCache, ttls); // await repository.updateEtasForSystemIfTTL("1"); // // const updatedResult = await repository.getEtaForShuttleAndStopId( // "5577", // "177666", // ); // expect(updatedResult?.millisecondsSinceEpoch).toEqual(expectedEta.millisecondsSinceEpoch); // }); test("updateEtasForSystemIfTTL updates all ETA data if data is TTL", async () => { updateGlobalFetchMockJson(genericEtaDataByStopId); const sampleStop: IStop = { name: "Chapman Court", systemId: "1", id: "177666", coordinates: { latitude: 33.796796, longitude: -117.889293 }, } const repository = new ApiBasedRepository(); repository.getStopsBySystemId = jest.fn(async () => { return [ sampleStop ]; }); repository.getStopById = jest.fn(async () => { return sampleStop; }); await repository.updateEtasForSystemIfTTL("1"); const updatedResult = await repository.getEtasForStopId("177666"); expect(updatedResult.length).toEqual(2); }); }); describe("getShuttleById", () => { test("getShuttleById returns null if unseeded cache", async () => { updateGlobalFetchMockJson(genericShuttleDataBySystemId); const initialCache: ApiBasedRepositoryCache = {}; const repository = new ApiBasedRepository(initialCache); const shuttle = await repository.getShuttleById("5577"); expect(shuttle).toBeNull(); }); test("getShuttleById returns old data if not expired", async () => { updateGlobalFetchMockJson(genericShuttleDataBySystemId); const expectedShuttle = { coordinates: { latitude: 33.791781, longitude: -117.8589646, }, name: "08", routeId: "53966", systemId: "1", id: "5577", millisecondsSinceEpoch: Date.now() - 1000, } const initialCache: ApiBasedRepositoryCache = { shuttleByShuttleId: { "5577": expectedShuttle } }; const ttls: ApiBasedRepositoryMillisecondTTLs = { shuttleByShuttleId: 100000, }; const repository = new ApiBasedRepository(initialCache, ttls); const shuttle = await repository.getShuttleById("5577"); expect(shuttle).toEqual(expectedShuttle); }); test("getShuttleById returns fresh data if expired", async () => { updateGlobalFetchMockJson(genericShuttleDataBySystemId); const initialCacheShuttle = { coordinates: { latitude: 33.791781, longitude: -117.8589646, }, name: "08", routeId: "53966", systemId: "1", id: "5577", millisecondsSinceEpoch: Date.now() - 100000, } const initialCache: ApiBasedRepositoryCache = { shuttleByShuttleId: { "5577": initialCacheShuttle } }; const ttls: ApiBasedRepositoryMillisecondTTLs = { shuttleByShuttleId: 1000, }; const repository = new ApiBasedRepository(initialCache, ttls); const shuttle = await repository.getShuttleById("5577"); expect(shuttle?.id).toEqual(initialCacheShuttle.id); expect(initialCacheShuttle.millisecondsSinceEpoch).not.toEqual(shuttle?.millisecondsSinceEpoch); }); }); describe("getShuttlesBySystemId", () => { test("getShuttlesBySystemId returns old data if not expired", async () => { updateGlobalFetchMockJson(genericShuttleDataBySystemId); const initialCacheShuttle = { coordinates: { latitude: 33.791781, longitude: -117.8589646, }, name: "08", routeId: "53966", systemId: "1", id: "5577", millisecondsSinceEpoch: Date.now() - 1000, }; const initialCache: ApiBasedRepositoryCache = { shuttlesBySystemId: { "1": [ initialCacheShuttle, ] }, shuttleByShuttleId: { "5577": initialCacheShuttle, } }; const ttls: ApiBasedRepositoryMillisecondTTLs = { shuttleByShuttleId: 100000, shuttlesBySystemId: 100000, }; const repository = new ApiBasedRepository(initialCache, ttls); const shuttles = await repository.getShuttlesBySystemId("1"); expect(shuttles.length).toEqual(1); expect(shuttles[0].id).toEqual(initialCacheShuttle.id); }); test("getShuttlesBySystemId returns fresh data if expired", async () => { updateGlobalFetchMockJson(genericShuttleDataBySystemId); // TODO: move construction of shuttle into method const initialCacheShuttle = { coordinates: { latitude: 33.791781, longitude: -117.8589646, }, name: "08", routeId: "53966", systemId: "1", id: "5577", millisecondsSinceEpoch: Date.now() - 100000, }; const initialCache: ApiBasedRepositoryCache = { shuttlesBySystemId: { "1": [ initialCacheShuttle, ] }, shuttleByShuttleId: { "5577": initialCacheShuttle, } }; const ttls: ApiBasedRepositoryMillisecondTTLs = { shuttleByShuttleId: 1000, shuttlesBySystemId: 1000, }; const repository = new ApiBasedRepository(initialCache, ttls); const shuttles = await repository.getShuttlesBySystemId("1"); expect(shuttles.length).toEqual(1); expect(shuttles[0].id).toEqual("5577"); expect(shuttles[0].millisecondsSinceEpoch).not.toEqual(initialCacheShuttle.millisecondsSinceEpoch); }); test("getShuttlesBySystemId returns fresh data if no seeded data", async () => { updateGlobalFetchMockJson(genericShuttleDataBySystemId); const repository = new ApiBasedRepository(); const shuttles = await repository.getShuttlesBySystemId("1"); expect(shuttles.length).toEqual(1); }); });