Merge pull request #78 from brendan-ch/chore/implement-cleaning-of-passio-data

This commit is contained in:
2025-09-26 17:08:50 -07:00
committed by GitHub
9 changed files with 365 additions and 150 deletions

View File

@@ -22,3 +22,5 @@ export const RATE_LIMIT_DELAY_MULTIPLIER_MS = process.env.RATE_LIMIT_DELAY_MULTI
: 1000; : 1000;
export const REDIS_RECONNECT_INTERVAL = 30000; export const REDIS_RECONNECT_INTERVAL = 30000;
export const SHUTTLE_TO_ROUTE_COORDINATE_MAXIMUM_DISTANCE_MILES = 5;

View File

@@ -1,3 +1,3 @@
export interface RepositoryLoader { export interface RepositoryLoader {
fetchAndUpdateAll(): Promise<void>; updateAll(): Promise<void>;
} }

View File

@@ -33,7 +33,7 @@ export class TimedApiBasedRepositoryLoader {
if (!this.shouldBeRunning) return; if (!this.shouldBeRunning) return;
try { try {
await this.loader.fetchAndUpdateAll(); await this.loader.updateAll();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@@ -1,4 +1,4 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { TimedApiBasedRepositoryLoader } from "../TimedApiBasedRepositoryLoader"; import { TimedApiBasedRepositoryLoader } from "../TimedApiBasedRepositoryLoader";
import { resetGlobalFetchMockJson } from "../../../testHelpers/fetchMockHelpers"; import { resetGlobalFetchMockJson } from "../../../testHelpers/fetchMockHelpers";
import { UnoptimizedInMemoryShuttleRepository } from "../../repositories/shuttle/UnoptimizedInMemoryShuttleRepository"; import { UnoptimizedInMemoryShuttleRepository } from "../../repositories/shuttle/UnoptimizedInMemoryShuttleRepository";
@@ -23,7 +23,7 @@ describe("TimedApiBasedRepositoryLoader", () => {
); );
spies = { spies = {
fetchAndUpdateAll: jest.spyOn(mockLoader, 'fetchAndUpdateAll'), updateAll: jest.spyOn(mockLoader, 'updateAll'),
}; };
Object.values(spies).forEach((spy: any) => { Object.values(spies).forEach((spy: any) => {

View File

@@ -21,7 +21,7 @@ export class ChapmanApiBasedParkingRepositoryLoader implements ParkingRepository
this.fetchAndUpdateParkingStructures = this.fetchAndUpdateParkingStructures.bind(this); this.fetchAndUpdateParkingStructures = this.fetchAndUpdateParkingStructures.bind(this);
} }
async fetchAndUpdateAll() { async updateAll() {
await this.fetchAndUpdateParkingStructures(); await this.fetchAndUpdateParkingStructures();
} }

View File

@@ -34,7 +34,7 @@ describe("ChapmanApiBasedParkingRepositoryLoader", () => {
spy.mockResolvedValue(undefined); spy.mockResolvedValue(undefined);
}); });
await loader.fetchAndUpdateAll(); await loader.updateAll();
Object.values(spies).forEach((spy: any) => { Object.values(spies).forEach((spy: any) => {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();

View File

@@ -1,8 +1,9 @@
import { ShuttleGetterSetterRepository } from "../../repositories/shuttle/ShuttleGetterSetterRepository"; import { ShuttleGetterSetterRepository } from "../../repositories/shuttle/ShuttleGetterSetterRepository";
import { IEta, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities"; import { IEta, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader"; import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader";
import { IEntityWithId } from "../../entities/SharedEntities"; import { ICoordinates, IEntityWithId } from "../../entities/SharedEntities";
import { ApiResponseError } from "../ApiResponseError"; import { ApiResponseError } from "../ApiResponseError";
import { SHUTTLE_TO_ROUTE_COORDINATE_MAXIMUM_DISTANCE_MILES } from "../../environment";
/** /**
* Class which can load data into a repository from the * Class which can load data into a repository from the
@@ -10,12 +11,13 @@ import { ApiResponseError } from "../ApiResponseError";
* which inherit from `IEntityWithId`. * which inherit from `IEntityWithId`.
*/ */
export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader { export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader {
baseUrl = "https://passiogo.com/mapGetData.php"; readonly baseUrl = "https://passiogo.com/mapGetData.php";
constructor( constructor(
public passioSystemId: string, public passioSystemId: string,
public systemIdForConstructedData: string, public systemIdForConstructedData: string,
public repository: ShuttleGetterSetterRepository, public repository: ShuttleGetterSetterRepository,
readonly shuttleToRouteCoordinateMaximumDistanceMiles = SHUTTLE_TO_ROUTE_COORDINATE_MAXIMUM_DISTANCE_MILES,
) { ) {
} }
@@ -28,23 +30,49 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
return ids; return ids;
} }
public async fetchAndUpdateAll() { public async updateAll() {
await this.fetchAndUpdateRouteDataForSystem(); await this.updateRouteDataForSystem();
await this.fetchAndUpdateStopAndPolylineDataForRoutesInSystem(); await this.updateStopAndPolylineDataForRoutesInSystem();
await this.fetchAndUpdateShuttleDataForSystem(); await this.updateShuttleDataForSystemBasedOnProximityToRoutes();
// Because ETA method doesn't support pruning yet, // Because ETA method doesn't support pruning yet,
// add a call to the clear method here // add a call to the clear method here
await this.repository.clearEtaData(); await this.repository.clearEtaData();
await this.fetchAndUpdateEtaDataForExistingStopsForSystem(); await this.updateEtaDataForExistingStopsForSystem();
} }
public async fetchAndUpdateRouteDataForSystem() { public async updateRouteDataForSystem() {
const systemId = this.passioSystemId; try {
const json = await this.fetchRouteDataJson();
const routes = this.constructRoutesFromJson(json);
if (routes !== null) {
await this.updateRouteDataInRepository(routes);
} else {
console.warn(`Route update failed for the following JSON: ${JSON.stringify(json)}`);
}
} catch(e: any) {
throw new ApiResponseError(e.message);
}
}
private async updateRouteDataInRepository(routes: IRoute[]) {
const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => { const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => {
return await this.repository.getRoutes(); return await this.repository.getRoutes();
}); });
await Promise.all(routes.map(async (route) => {
await this.repository.addOrUpdateRoute(route);
routeIdsToPrune.delete(route.id);
}));
await Promise.all(Array.from(routeIdsToPrune).map(async (routeId) => {
await this.repository.removeRouteIfExists(routeId);
}));
}
private async fetchRouteDataJson() {
const systemId = this.passioSystemId;
const params = { const params = {
getRoutes: "2", getRoutes: "2",
}; };
@@ -52,21 +80,22 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
const formDataJsonObject = { const formDataJsonObject = {
"systemSelected0": systemId, "systemSelected0": systemId,
"amount": "1", "amount": "1",
} };
const formData = new FormData(); const formData = new FormData();
formData.set("json", JSON.stringify(formDataJsonObject)); formData.set("json", JSON.stringify(formDataJsonObject));
const query = new URLSearchParams(params).toString(); const query = new URLSearchParams(params).toString();
try {
const response = await fetch(`${this.baseUrl}?${query}`, { const response = await fetch(`${this.baseUrl}?${query}`, {
method: "POST", method: "POST",
body: formData, body: formData,
}); });
const json = await response.json(); return await response.json();
}
private constructRoutesFromJson(json: any): IRoute[] | null {
if (typeof json.all === "object") { if (typeof json.all === "object") {
await Promise.all(json.all.map(async (jsonRoute: any) => { return json.all.map((jsonRoute: any) => {
const constructedRoute: IRoute = { const constructedRoute: IRoute = {
name: jsonRoute.name, name: jsonRoute.name,
color: jsonRoute.color, color: jsonRoute.color,
@@ -75,30 +104,39 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
systemId: this.systemIdForConstructedData, systemId: this.systemIdForConstructedData,
updatedTime: new Date(), updatedTime: new Date(),
}; };
return constructedRoute;
await this.repository.addOrUpdateRoute(constructedRoute); });
routeIdsToPrune.delete(constructedRoute.id);
}))
} }
await Promise.all(Array.from(routeIdsToPrune).map(async (routeId) => { return null;
await this.repository.removeRouteIfExists(routeId); }
}));
public async updateStopAndPolylineDataForRoutesInSystem() {
try {
const json = await this.fetchStopAndPolylineDataJson();
await this.updateStopAndPolylineDataInRepository(json);
} catch(e: any) { } catch(e: any) {
throw new ApiResponseError(e.message); throw new ApiResponseError(e.message);
} }
} }
public async fetchAndUpdateStopAndPolylineDataForRoutesInSystem() { private async updateStopAndPolylineDataInRepository(json: any) {
const passioSystemId = this.passioSystemId;
// Fetch from the API
// Pass JSON output into two different methods to update repository
const stopIdsToPrune = await this.constructExistingEntityIdSet(async () => { const stopIdsToPrune = await this.constructExistingEntityIdSet(async () => {
return await this.repository.getStops(); return await this.repository.getStops();
}); });
await this.updateStopDataForSystemAndApiResponse(json, stopIdsToPrune);
await this.updateOrderedStopDataForExistingStops(json);
await this.updatePolylineDataForExistingRoutesAndApiResponse(json);
await Promise.all(Array.from(stopIdsToPrune).map(async (stopId) => {
await this.repository.removeStopIfExists(stopId);
}));
}
private async fetchStopAndPolylineDataJson() {
const passioSystemId = this.passioSystemId;
const params = { const params = {
getStops: "2", getStops: "2",
}; };
@@ -112,31 +150,46 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
const query = new URLSearchParams(params).toString(); const query = new URLSearchParams(params).toString();
try {
const response = await fetch(`${this.baseUrl}?${query}`, { const response = await fetch(`${this.baseUrl}?${query}`, {
method: "POST", method: "POST",
body: formData, body: formData,
}); });
const json = await response.json(); return await response.json();
}
await this.updateStopDataForSystemAndApiResponse(json, stopIdsToPrune); public async updateShuttleDataForSystemBasedOnProximityToRoutes() {
await this.updateOrderedStopDataForExistingStops(json); try {
await this.updatePolylineDataForExistingRoutesAndApiResponse(json); const json = await this.fetchShuttleDataJson();
let shuttles = this.constructInServiceShuttlesFromJson(json);
await Promise.all(Array.from(stopIdsToPrune).map(async (stopId) => { if (shuttles !== null) {
await this.repository.removeStopIfExists(stopId); shuttles = await this.filterShuttlesByDistanceFromCorrespondingRoute(shuttles);
})); await this.updateShuttleDataInRepository(shuttles);
} else {
console.warn(`Shuttle update failed for the following JSON: ${JSON.stringify(json)}`)
}
} catch(e: any) { } catch(e: any) {
throw new ApiResponseError(e.message); throw new ApiResponseError(e.message);
} }
} }
public async fetchAndUpdateShuttleDataForSystem() { private async updateShuttleDataInRepository(shuttles: IShuttle[]) {
const systemId = this.passioSystemId;
const shuttleIdsToPrune = await this.constructExistingEntityIdSet(async () => { const shuttleIdsToPrune = await this.constructExistingEntityIdSet(async () => {
return await this.repository.getShuttles(); return await this.repository.getShuttles();
}); });
await Promise.all(shuttles.map(async (shuttle) => {
await this.repository.addOrUpdateShuttle(shuttle);
shuttleIdsToPrune.delete(shuttle.id);
}));
await Promise.all(Array.from(shuttleIdsToPrune).map(async (shuttleId) => {
await this.repository.removeShuttleIfExists(shuttleId);
}));
}
private async fetchShuttleDataJson() {
const systemId = this.passioSystemId;
const params = { const params = {
getBuses: "2" getBuses: "2"
}; };
@@ -151,19 +204,21 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
const query = new URLSearchParams(params).toString(); const query = new URLSearchParams(params).toString();
try {
const response = await fetch(`${this.baseUrl}?${query}`, { const response = await fetch(`${this.baseUrl}?${query}`, {
method: "POST", method: "POST",
body: formData, body: formData,
}); });
const json = await response.json(); return await response.json();
}
private constructInServiceShuttlesFromJson(json: any): IShuttle[] | null {
if (json.buses && json.buses["-1"] === undefined) { if (json.buses && json.buses["-1"] === undefined) {
const jsonBuses = Object.values(json.buses).map((busesArr: any) => { const jsonBuses = Object.values(json.buses).map((busesArr: any) => {
return busesArr[0]; return busesArr[0];
}); });
jsonBuses.filter((bus) => bus.outOfService != 0)
await Promise.all(jsonBuses.map(async (jsonBus: any) => { return jsonBuses.map((jsonBus: any) => {
const constructedShuttle: IShuttle = { const constructedShuttle: IShuttle = {
name: jsonBus.bus, name: jsonBus.bus,
coordinates: { coordinates: {
@@ -176,30 +231,42 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
orientationInDegrees: parseFloat(jsonBus.calculatedCourse), orientationInDegrees: parseFloat(jsonBus.calculatedCourse),
updatedTime: new Date(), updatedTime: new Date(),
} }
return constructedShuttle;
});
}
await this.repository.addOrUpdateShuttle(constructedShuttle); return null;
}
shuttleIdsToPrune.delete(constructedShuttle.id); public async updateEtaDataForExistingStopsForSystem() {
const stops = await this.repository.getStops();
await Promise.all(stops.map(async (stop) => {
let stopId = stop.id;
await this.updateEtaDataForStopId(stopId);
})); }));
} }
await Promise.all(Array.from(shuttleIdsToPrune).map(async (shuttleId) => { public async updateEtaDataForStopId(stopId: string) {
await this.repository.removeShuttleIfExists(shuttleId); try {
})); const json = await this.fetchEtaDataJson(stopId);
const etas = this.constructEtasFromJson(json, stopId);
if (etas !== null) {
await this.updateEtaDataInRepository(etas);
} else {
console.warn(`ETA update failed for stop ${stopId} with the following JSON: ${JSON.stringify(json)}`);
}
} catch(e: any) { } catch(e: any) {
throw new ApiResponseError(e.message); throw new ApiResponseError(e.message);
} }
} }
public async fetchAndUpdateEtaDataForExistingStopsForSystem() { private async updateEtaDataInRepository(etas: IEta[]) {
const stops = await this.repository.getStops(); await Promise.all(etas.map(async (eta) => {
await Promise.all(stops.map(async (stop) => { await this.repository.addOrUpdateEta(eta);
let stopId = stop.id;
await this.fetchAndUpdateEtaDataForStopId(stopId);
})); }));
} }
public async fetchAndUpdateEtaDataForStopId(stopId: string) { private async fetchEtaDataJson(stopId: string) {
const params = { const params = {
eta: "3", eta: "3",
stopIds: stopId, stopIds: stopId,
@@ -207,18 +274,16 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
const query = new URLSearchParams(params).toString(); const query = new URLSearchParams(params).toString();
try {
const response = await fetch(`${this.baseUrl}?${query}`, { const response = await fetch(`${this.baseUrl}?${query}`, {
method: "GET", method: "GET",
}); });
const json = await response.json(); return await response.json();
}
private constructEtasFromJson(json: any, stopId: string): IEta[] | null {
if (json.ETAs && json.ETAs[stopId]) { if (json.ETAs && json.ETAs[stopId]) {
// Continue with the parsing return json.ETAs[stopId].map((jsonEta: any) => {
json.ETAs[stopId].forEach((jsonEta: any) => {
// Update cache
const shuttleId: string = jsonEta.busId; const shuttleId: string = jsonEta.busId;
const eta: IEta = { const eta: IEta = {
secondsRemaining: jsonEta.secondsSpent, secondsRemaining: jsonEta.secondsSpent,
shuttleId: `${shuttleId}`, shuttleId: `${shuttleId}`,
@@ -226,13 +291,11 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
updatedTime: new Date(), updatedTime: new Date(),
systemId: this.systemIdForConstructedData, systemId: this.systemIdForConstructedData,
}; };
return eta;
this.repository.addOrUpdateEta(eta);
}); });
} }
} catch(e: any) {
throw new ApiResponseError(e.message); return null;
}
} }
protected async updateStopDataForSystemAndApiResponse( protected async updateStopDataForSystemAndApiResponse(
@@ -331,4 +394,47 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
})) }))
} }
} }
private filterShuttlesByDistanceFromCorrespondingRoute = async (shuttles: IShuttle[]) => {
let filteredShuttles: IShuttle[] = [];
await Promise.all(shuttles.map(async (shuttle) => {
const route = await this.repository.getRouteById(shuttle.routeId);
if (route != null) {
let closestDistanceMiles = Number.MAX_VALUE;
route.polylineCoordinates.forEach((coordinate) => {
const calculatedDistance = ApiBasedShuttleRepositoryLoader.convertDistanceBetweenCoordinatesToMiles(coordinate, shuttle.coordinates)
if (closestDistanceMiles > calculatedDistance) {
closestDistanceMiles = calculatedDistance;
}
});
if (closestDistanceMiles <= this.shuttleToRouteCoordinateMaximumDistanceMiles) {
filteredShuttles.push(shuttle);
}
} else {
console.warn(`No corresponding route found for ID ${shuttle.routeId} of shuttle ${shuttle.id}`)
}
}));
return filteredShuttles;
};
public static convertDistanceBetweenCoordinatesToMiles = (fromCoordinate: ICoordinates, toCoordinate: ICoordinates): number => {
const earthRadiusMiles = 3959;
const lat1Rad = fromCoordinate.latitude * Math.PI / 180;
const lat2Rad = toCoordinate.latitude * Math.PI / 180;
const deltaLatRad = (toCoordinate.latitude - fromCoordinate.latitude) * Math.PI / 180;
const deltaLonRad = (toCoordinate.longitude - fromCoordinate.longitude) * Math.PI / 180;
const a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return earthRadiusMiles * c;
};
} }

View File

@@ -1,9 +1,9 @@
import { RepositoryLoader } from "../RepositoryLoader"; import { RepositoryLoader } from "../RepositoryLoader";
export interface ShuttleRepositoryLoader extends RepositoryLoader { export interface ShuttleRepositoryLoader extends RepositoryLoader {
fetchAndUpdateRouteDataForSystem(): Promise<void>; updateRouteDataForSystem(): Promise<void>;
fetchAndUpdateStopAndPolylineDataForRoutesInSystem(): Promise<void>; updateStopAndPolylineDataForRoutesInSystem(): Promise<void>;
fetchAndUpdateShuttleDataForSystem(): Promise<void>; updateShuttleDataForSystemBasedOnProximityToRoutes(): Promise<void>;
fetchAndUpdateEtaDataForExistingStopsForSystem(): Promise<void>; updateEtaDataForExistingStopsForSystem(): Promise<void>;
fetchAndUpdateEtaDataForStopId(stopId: string): Promise<void>; updateEtaDataForStopId(stopId: string): Promise<void>;
} }

View File

@@ -16,6 +16,7 @@ import {
updateGlobalFetchMockJsonToThrowSyntaxError updateGlobalFetchMockJsonToThrowSyntaxError
} from "../../../../testHelpers/fetchMockHelpers"; } from "../../../../testHelpers/fetchMockHelpers";
import { assertAsyncCallbackThrowsApiResponseError } from "../../../../testHelpers/assertAsyncCallbackThrowsApiResponseError"; import { assertAsyncCallbackThrowsApiResponseError } from "../../../../testHelpers/assertAsyncCallbackThrowsApiResponseError";
import { IRoute } from "../../../entities/ShuttleRepositoryEntities";
describe("ApiBasedShuttleRepositoryLoader", () => { describe("ApiBasedShuttleRepositoryLoader", () => {
let loader: ApiBasedShuttleRepositoryLoader; let loader: ApiBasedShuttleRepositoryLoader;
@@ -31,20 +32,20 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
const systemId = "1"; const systemId = "1";
describe("fetchAndUpdateAll", () => { describe("updateAll", () => {
it("calls all the correct methods", async () => { it("calls all the correct methods", async () => {
const spies = { const spies = {
fetchAndUpdateRouteDataForSystem: jest.spyOn(loader, "fetchAndUpdateRouteDataForSystem"), updateRouteDataForSystem: jest.spyOn(loader, "updateRouteDataForSystem"),
fetchAndUpdateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, "fetchAndUpdateStopAndPolylineDataForRoutesInSystem"), updateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, "updateStopAndPolylineDataForRoutesInSystem"),
fetchAndUpdateShuttleDataForSystem: jest.spyOn(loader, "fetchAndUpdateShuttleDataForSystem"), updateShuttleDataForSystem: jest.spyOn(loader, "updateShuttleDataForSystemBasedOnProximityToRoutes"),
fetchAndUpdateEtaDataForExistingStopsForSystem: jest.spyOn(loader, "fetchAndUpdateEtaDataForExistingStopsForSystem"), updateEtaDataForExistingStopsForSystem: jest.spyOn(loader, "updateEtaDataForExistingStopsForSystem"),
}; };
Object.values(spies).forEach((spy: any) => { Object.values(spies).forEach((spy: any) => {
spy.mockResolvedValue(undefined); spy.mockResolvedValue(undefined);
}); });
await loader.fetchAndUpdateAll(); await loader.updateAll();
Object.values(spies).forEach((spy: any) => { Object.values(spies).forEach((spy: any) => {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
@@ -52,7 +53,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
}); });
}); });
describe("fetchAndUpdateRouteDataForSystem", () => { describe("updateRouteDataForSystem", () => {
it("updates route data in repository if response received", async () => { it("updates route data in repository if response received", async () => {
// Arrange // Arrange
// Test pruning // Test pruning
@@ -65,7 +66,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
updateGlobalFetchMockJson(fetchRouteDataSuccessfulResponse); updateGlobalFetchMockJson(fetchRouteDataSuccessfulResponse);
// Act // Act
await loader.fetchAndUpdateRouteDataForSystem(); await loader.updateRouteDataForSystem();
// Assert // Assert
const routes = await loader.repository.getRoutes(); const routes = await loader.repository.getRoutes();
@@ -80,12 +81,12 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
updateGlobalFetchMockJsonToThrowSyntaxError(); updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => { await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateRouteDataForSystem(); await loader.updateRouteDataForSystem();
}); });
}); });
}); });
describe("fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId", () => { describe("updateStopAndPolylineDataForRoutesInSystem", () => {
it("updates stop and polyline data if response received", async () => { it("updates stop and polyline data if response received", async () => {
// Arrange // Arrange
// Test pruning of stops only // Test pruning of stops only
@@ -99,7 +100,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
const stopsArray = Object.values(fetchStopAndPolylineDataSuccessfulResponse.stops); const stopsArray = Object.values(fetchStopAndPolylineDataSuccessfulResponse.stops);
await loader.fetchAndUpdateStopAndPolylineDataForRoutesInSystem(); await loader.updateStopAndPolylineDataForRoutesInSystem();
const stops = await loader.repository.getStops(); const stops = await loader.repository.getStops();
expect(stops.length).toEqual(stopsArray.length); expect(stops.length).toEqual(stopsArray.length);
@@ -119,41 +120,147 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
updateGlobalFetchMockJsonToThrowSyntaxError(); updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => { await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateStopAndPolylineDataForRoutesInSystem(); await loader.updateStopAndPolylineDataForRoutesInSystem();
}); });
}) })
}); });
describe("fetchAndUpdateShuttleDataForSystem", () => { describe("updateShuttleDataForSystemBasedOnProximityToRoutes", () => {
it("updates shuttle data in repository if response received", async () => { function generateMockRoutesWithPolylineCoordinates() {
const routes = generateMockRoutes();
routes[0].polylineCoordinates = [
{latitude: 33.78792, longitude: -117.86187},
{latitude: 33.78792, longitude: -117.86200},
{latitude: 33.78792, longitude: -117.86245}
];
return routes;
}
function getMockJsonResponseMatchingRouteAndCoordinates(route: IRoute, longitude: string, latitude: string) {
const modifiedSuccessfulResponse = {
...fetchShuttleDataSuccessfulResponse,
};
Object.keys(modifiedSuccessfulResponse.buses).forEach((busId) => {
const bus = (modifiedSuccessfulResponse.buses as any)[busId][0];
bus.latitude = latitude;
bus.longitude = longitude;
bus.routeId = route.id;
});
return modifiedSuccessfulResponse;
}
async function addMockRoutes(routes: IRoute[]) {
await Promise.all(routes.map(async (route) => {
await loader.repository.addOrUpdateRoute(route);
}));
}
it("updates shuttle data in repository from API if shuttles close enough to route", async () => {
const distanceMiles = 1;
loader = new ApiBasedShuttleRepositoryLoader(
"263",
"1",
new UnoptimizedInMemoryShuttleRepository(),
distanceMiles,
);
const routes = generateMockRoutesWithPolylineCoordinates();
await addMockRoutes(routes);
const modifiedSuccessfulResponse = getMockJsonResponseMatchingRouteAndCoordinates(
routes[0],
"-117.86187",
"33.78792"
);
updateGlobalFetchMockJson(modifiedSuccessfulResponse);
const busesInResponse = Object.values(modifiedSuccessfulResponse.buses);
await loader.updateShuttleDataForSystemBasedOnProximityToRoutes();
const shuttles = await loader.repository.getShuttles();
expect(shuttles.length).toEqual(busesInResponse.length);
});
it("does not update shuttle data in repository from API if shuttles are not close enough to route", async () => {
const distanceMiles = 1;
loader = new ApiBasedShuttleRepositoryLoader(
"263",
"1",
new UnoptimizedInMemoryShuttleRepository(),
distanceMiles,
);
const routes = generateMockRoutesWithPolylineCoordinates();
await addMockRoutes(routes);
const modifiedSuccessfulResponse = getMockJsonResponseMatchingRouteAndCoordinates(
routes[0],
"-116.86187",
"32.78792"
);
updateGlobalFetchMockJson(modifiedSuccessfulResponse);
await loader.updateShuttleDataForSystemBasedOnProximityToRoutes();
const shuttles = await loader.repository.getShuttles();
expect(shuttles.length).toEqual(0);
});
it("prunes shuttles correctly", async () => {
const distanceMiles = 1;
loader = new ApiBasedShuttleRepositoryLoader(
"263",
"1",
new UnoptimizedInMemoryShuttleRepository(),
distanceMiles,
);
// Add mock shuttles to repository (these should be pruned)
const shuttlesToPrune = generateMockShuttles(); const shuttlesToPrune = generateMockShuttles();
await Promise.all(shuttlesToPrune.map(async (shuttle) => { await Promise.all(shuttlesToPrune.map(async (shuttle) => {
shuttle.systemId = systemId; shuttle.systemId = systemId;
await loader.repository.addOrUpdateShuttle(shuttle); await loader.repository.addOrUpdateShuttle(shuttle);
})) }));
updateGlobalFetchMockJson(fetchShuttleDataSuccessfulResponse); const routes = generateMockRoutesWithPolylineCoordinates();
const busesInResponse = Object.values(fetchShuttleDataSuccessfulResponse.buses); await addMockRoutes(routes);
await loader.fetchAndUpdateShuttleDataForSystem(); const modifiedSuccessfulResponse = getMockJsonResponseMatchingRouteAndCoordinates(
routes[0],
"-117.86187",
"33.78792"
);
updateGlobalFetchMockJson(modifiedSuccessfulResponse);
// Update shuttles from API
await loader.updateShuttleDataForSystemBasedOnProximityToRoutes();
// Old shuttles should be pruned, only API shuttles should remain
const shuttles = await loader.repository.getShuttles(); const shuttles = await loader.repository.getShuttles();
const busesInResponse = Object.values(modifiedSuccessfulResponse.buses);
expect(shuttles.length).toEqual(busesInResponse.length); expect(shuttles.length).toEqual(busesInResponse.length);
// Verify none of the original mock shuttles remain
shuttlesToPrune.forEach((originalShuttle) => {
const foundShuttle = shuttles.find(s => s.id === originalShuttle.id);
expect(foundShuttle).toBeUndefined();
});
}); });
it("throws the correct error if the API response contains no data", async () => { it("throws the correct error if the API response contains no data", async () => {
updateGlobalFetchMockJsonToThrowSyntaxError(); updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => { await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateShuttleDataForSystem(); await loader.updateShuttleDataForSystemBasedOnProximityToRoutes();
}); });
}); });
}); });
describe("fetchAndUpdateEtaDataForExistingStopsForSystem", () => { describe("updateEtaDataForExistingStopsForSystem", () => {
it("calls fetchAndUpdateEtaDataForStopId for every stop in repository", async () => { it("calls updateEtaDataForStopId for every stop in repository", async () => {
const spy = jest.spyOn(loader, "fetchAndUpdateEtaDataForStopId"); const spy = jest.spyOn(loader, "updateEtaDataForStopId");
const stops = generateMockStops(); const stops = generateMockStops();
stops.forEach((stop) => { stops.forEach((stop) => {
@@ -164,20 +271,20 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
await loader.repository.addOrUpdateStop(stop); await loader.repository.addOrUpdateStop(stop);
})); }));
await loader.fetchAndUpdateEtaDataForExistingStopsForSystem(); await loader.updateEtaDataForExistingStopsForSystem();
expect(spy.mock.calls.length).toEqual(stops.length); expect(spy.mock.calls.length).toEqual(stops.length);
}); });
}); });
describe("fetchAndUpdateEtaDataForStopId", () => { describe("updateEtaDataForStopId", () => {
const stopId = "177666"; const stopId = "177666";
it("updates ETA data for stop id if response received", async () => { it("updates ETA data for stop id if response received", async () => {
updateGlobalFetchMockJson(fetchEtaDataSuccessfulResponse); updateGlobalFetchMockJson(fetchEtaDataSuccessfulResponse);
// @ts-ignore // @ts-ignore
const etasFromResponse = fetchEtaDataSuccessfulResponse.ETAs[stopId] const etasFromResponse = fetchEtaDataSuccessfulResponse.ETAs[stopId]
await loader.fetchAndUpdateEtaDataForStopId(stopId); await loader.updateEtaDataForStopId(stopId);
const etas = await loader.repository.getEtasForStopId(stopId); const etas = await loader.repository.getEtasForStopId(stopId);
expect(etas.length).toEqual(etasFromResponse.length); expect(etas.length).toEqual(etasFromResponse.length);
@@ -187,7 +294,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
updateGlobalFetchMockJsonToThrowSyntaxError(); updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => { await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateEtaDataForStopId("263"); await loader.updateEtaDataForStopId("263");
}); });
}); });
}); });