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;
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 {
fetchAndUpdateAll(): Promise<void>;
updateAll(): Promise<void>;
}

View File

@@ -33,7 +33,7 @@ export class TimedApiBasedRepositoryLoader {
if (!this.shouldBeRunning) return;
try {
await this.loader.fetchAndUpdateAll();
await this.loader.updateAll();
} catch (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 { resetGlobalFetchMockJson } from "../../../testHelpers/fetchMockHelpers";
import { UnoptimizedInMemoryShuttleRepository } from "../../repositories/shuttle/UnoptimizedInMemoryShuttleRepository";
@@ -23,7 +23,7 @@ describe("TimedApiBasedRepositoryLoader", () => {
);
spies = {
fetchAndUpdateAll: jest.spyOn(mockLoader, 'fetchAndUpdateAll'),
updateAll: jest.spyOn(mockLoader, 'updateAll'),
};
Object.values(spies).forEach((spy: any) => {

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import { ShuttleGetterSetterRepository } from "../../repositories/shuttle/ShuttleGetterSetterRepository";
import { IEta, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader";
import { IEntityWithId } from "../../entities/SharedEntities";
import { ICoordinates, IEntityWithId } from "../../entities/SharedEntities";
import { ApiResponseError } from "../ApiResponseError";
import { SHUTTLE_TO_ROUTE_COORDINATE_MAXIMUM_DISTANCE_MILES } from "../../environment";
/**
* Class which can load data into a repository from the
@@ -10,12 +11,13 @@ import { ApiResponseError } from "../ApiResponseError";
* which inherit from `IEntityWithId`.
*/
export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader {
baseUrl = "https://passiogo.com/mapGetData.php";
readonly baseUrl = "https://passiogo.com/mapGetData.php";
constructor(
public passioSystemId: string,
public systemIdForConstructedData: string,
public repository: ShuttleGetterSetterRepository,
readonly shuttleToRouteCoordinateMaximumDistanceMiles = SHUTTLE_TO_ROUTE_COORDINATE_MAXIMUM_DISTANCE_MILES,
) {
}
@@ -28,23 +30,49 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
return ids;
}
public async fetchAndUpdateAll() {
await this.fetchAndUpdateRouteDataForSystem();
await this.fetchAndUpdateStopAndPolylineDataForRoutesInSystem();
await this.fetchAndUpdateShuttleDataForSystem();
public async updateAll() {
await this.updateRouteDataForSystem();
await this.updateStopAndPolylineDataForRoutesInSystem();
await this.updateShuttleDataForSystemBasedOnProximityToRoutes();
// Because ETA method doesn't support pruning yet,
// add a call to the clear method here
await this.repository.clearEtaData();
await this.fetchAndUpdateEtaDataForExistingStopsForSystem();
await this.updateEtaDataForExistingStopsForSystem();
}
public async fetchAndUpdateRouteDataForSystem() {
const systemId = this.passioSystemId;
public async updateRouteDataForSystem() {
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 () => {
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 = {
getRoutes: "2",
};
@@ -52,53 +80,63 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
const formDataJsonObject = {
"systemSelected0": systemId,
"amount": "1",
}
};
const formData = new FormData();
formData.set("json", JSON.stringify(formDataJsonObject));
const query = new URLSearchParams(params).toString();
try {
const response = await fetch(`${this.baseUrl}?${query}`, {
method: "POST",
body: formData,
const response = await fetch(`${this.baseUrl}?${query}`, {
method: "POST",
body: formData,
});
return await response.json();
}
private constructRoutesFromJson(json: any): IRoute[] | null {
if (typeof json.all === "object") {
return json.all.map((jsonRoute: any) => {
const constructedRoute: IRoute = {
name: jsonRoute.name,
color: jsonRoute.color,
id: jsonRoute.myid,
polylineCoordinates: [],
systemId: this.systemIdForConstructedData,
updatedTime: new Date(),
};
return constructedRoute;
});
const json = await response.json();
}
if (typeof json.all === "object") {
await Promise.all(json.all.map(async (jsonRoute: any) => {
const constructedRoute: IRoute = {
name: jsonRoute.name,
color: jsonRoute.color,
id: jsonRoute.myid,
polylineCoordinates: [],
systemId: this.systemIdForConstructedData,
updatedTime: new Date(),
};
return null;
}
await this.repository.addOrUpdateRoute(constructedRoute);
routeIdsToPrune.delete(constructedRoute.id);
}))
}
await Promise.all(Array.from(routeIdsToPrune).map(async (routeId) => {
await this.repository.removeRouteIfExists(routeId);
}));
public async updateStopAndPolylineDataForRoutesInSystem() {
try {
const json = await this.fetchStopAndPolylineDataJson();
await this.updateStopAndPolylineDataInRepository(json);
} catch(e: any) {
throw new ApiResponseError(e.message);
}
}
public async fetchAndUpdateStopAndPolylineDataForRoutesInSystem() {
const passioSystemId = this.passioSystemId;
// Fetch from the API
// Pass JSON output into two different methods to update repository
private async updateStopAndPolylineDataInRepository(json: any) {
const stopIdsToPrune = await this.constructExistingEntityIdSet(async () => {
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 = {
getStops: "2",
};
@@ -112,31 +150,46 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
const query = new URLSearchParams(params).toString();
const response = await fetch(`${this.baseUrl}?${query}`, {
method: "POST",
body: formData,
});
return await response.json();
}
public async updateShuttleDataForSystemBasedOnProximityToRoutes() {
try {
const response = await fetch(`${this.baseUrl}?${query}`, {
method: "POST",
body: formData,
});
const json = await response.json();
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);
}));
const json = await this.fetchShuttleDataJson();
let shuttles = this.constructInServiceShuttlesFromJson(json);
if (shuttles !== null) {
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) {
throw new ApiResponseError(e.message);
}
}
public async fetchAndUpdateShuttleDataForSystem() {
const systemId = this.passioSystemId;
private async updateShuttleDataInRepository(shuttles: IShuttle[]) {
const shuttleIdsToPrune = await this.constructExistingEntityIdSet(async () => {
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 = {
getBuses: "2"
};
@@ -151,55 +204,69 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
const query = new URLSearchParams(params).toString();
try {
const response = await fetch(`${this.baseUrl}?${query}`, {
method: "POST",
body: formData,
const response = await fetch(`${this.baseUrl}?${query}`, {
method: "POST",
body: formData,
});
return await response.json();
}
private constructInServiceShuttlesFromJson(json: any): IShuttle[] | null {
if (json.buses && json.buses["-1"] === undefined) {
const jsonBuses = Object.values(json.buses).map((busesArr: any) => {
return busesArr[0];
});
const json = await response.json();
jsonBuses.filter((bus) => bus.outOfService != 0)
if (json.buses && json.buses["-1"] === undefined) {
const jsonBuses = Object.values(json.buses).map((busesArr: any) => {
return busesArr[0];
});
return jsonBuses.map((jsonBus: any) => {
const constructedShuttle: IShuttle = {
name: jsonBus.bus,
coordinates: {
latitude: parseFloat(jsonBus.latitude),
longitude: parseFloat(jsonBus.longitude),
},
routeId: jsonBus.routeId,
systemId: this.systemIdForConstructedData,
id: `${jsonBus.busId}`,
orientationInDegrees: parseFloat(jsonBus.calculatedCourse),
updatedTime: new Date(),
}
return constructedShuttle;
});
}
await Promise.all(jsonBuses.map(async (jsonBus: any) => {
const constructedShuttle: IShuttle = {
name: jsonBus.bus,
coordinates: {
latitude: parseFloat(jsonBus.latitude),
longitude: parseFloat(jsonBus.longitude),
},
routeId: jsonBus.routeId,
systemId: this.systemIdForConstructedData,
id: `${jsonBus.busId}`,
orientationInDegrees: parseFloat(jsonBus.calculatedCourse),
updatedTime: new Date(),
}
return null;
}
await this.repository.addOrUpdateShuttle(constructedShuttle);
public async updateEtaDataForExistingStopsForSystem() {
const stops = await this.repository.getStops();
await Promise.all(stops.map(async (stop) => {
let stopId = stop.id;
await this.updateEtaDataForStopId(stopId);
}));
}
shuttleIdsToPrune.delete(constructedShuttle.id);
}));
public async updateEtaDataForStopId(stopId: string) {
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)}`);
}
await Promise.all(Array.from(shuttleIdsToPrune).map(async (shuttleId) => {
await this.repository.removeShuttleIfExists(shuttleId);
}));
} catch(e: any) {
throw new ApiResponseError(e.message);
}
}
public async fetchAndUpdateEtaDataForExistingStopsForSystem() {
const stops = await this.repository.getStops();
await Promise.all(stops.map(async (stop) => {
let stopId = stop.id;
await this.fetchAndUpdateEtaDataForStopId(stopId);
private async updateEtaDataInRepository(etas: IEta[]) {
await Promise.all(etas.map(async (eta) => {
await this.repository.addOrUpdateEta(eta);
}));
}
public async fetchAndUpdateEtaDataForStopId(stopId: string) {
private async fetchEtaDataJson(stopId: string) {
const params = {
eta: "3",
stopIds: stopId,
@@ -207,32 +274,28 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
const query = new URLSearchParams(params).toString();
try {
const response = await fetch(`${this.baseUrl}?${query}`, {
method: "GET",
const response = await fetch(`${this.baseUrl}?${query}`, {
method: "GET",
});
return await response.json();
}
private constructEtasFromJson(json: any, stopId: string): IEta[] | null {
if (json.ETAs && json.ETAs[stopId]) {
return json.ETAs[stopId].map((jsonEta: any) => {
const shuttleId: string = jsonEta.busId;
const eta: IEta = {
secondsRemaining: jsonEta.secondsSpent,
shuttleId: `${shuttleId}`,
stopId: stopId,
updatedTime: new Date(),
systemId: this.systemIdForConstructedData,
};
return eta;
});
const json = await response.json();
if (json.ETAs && json.ETAs[stopId]) {
// Continue with the parsing
json.ETAs[stopId].forEach((jsonEta: any) => {
// Update cache
const shuttleId: string = jsonEta.busId;
const eta: IEta = {
secondsRemaining: jsonEta.secondsSpent,
shuttleId: `${shuttleId}`,
stopId: stopId,
updatedTime: new Date(),
systemId: this.systemIdForConstructedData,
};
this.repository.addOrUpdateEta(eta);
});
}
} catch(e: any) {
throw new ApiResponseError(e.message);
}
return null;
}
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";
export interface ShuttleRepositoryLoader extends RepositoryLoader {
fetchAndUpdateRouteDataForSystem(): Promise<void>;
fetchAndUpdateStopAndPolylineDataForRoutesInSystem(): Promise<void>;
fetchAndUpdateShuttleDataForSystem(): Promise<void>;
fetchAndUpdateEtaDataForExistingStopsForSystem(): Promise<void>;
fetchAndUpdateEtaDataForStopId(stopId: string): Promise<void>;
updateRouteDataForSystem(): Promise<void>;
updateStopAndPolylineDataForRoutesInSystem(): Promise<void>;
updateShuttleDataForSystemBasedOnProximityToRoutes(): Promise<void>;
updateEtaDataForExistingStopsForSystem(): Promise<void>;
updateEtaDataForStopId(stopId: string): Promise<void>;
}

View File

@@ -16,6 +16,7 @@ import {
updateGlobalFetchMockJsonToThrowSyntaxError
} from "../../../../testHelpers/fetchMockHelpers";
import { assertAsyncCallbackThrowsApiResponseError } from "../../../../testHelpers/assertAsyncCallbackThrowsApiResponseError";
import { IRoute } from "../../../entities/ShuttleRepositoryEntities";
describe("ApiBasedShuttleRepositoryLoader", () => {
let loader: ApiBasedShuttleRepositoryLoader;
@@ -31,20 +32,20 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
const systemId = "1";
describe("fetchAndUpdateAll", () => {
describe("updateAll", () => {
it("calls all the correct methods", async () => {
const spies = {
fetchAndUpdateRouteDataForSystem: jest.spyOn(loader, "fetchAndUpdateRouteDataForSystem"),
fetchAndUpdateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, "fetchAndUpdateStopAndPolylineDataForRoutesInSystem"),
fetchAndUpdateShuttleDataForSystem: jest.spyOn(loader, "fetchAndUpdateShuttleDataForSystem"),
fetchAndUpdateEtaDataForExistingStopsForSystem: jest.spyOn(loader, "fetchAndUpdateEtaDataForExistingStopsForSystem"),
updateRouteDataForSystem: jest.spyOn(loader, "updateRouteDataForSystem"),
updateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, "updateStopAndPolylineDataForRoutesInSystem"),
updateShuttleDataForSystem: jest.spyOn(loader, "updateShuttleDataForSystemBasedOnProximityToRoutes"),
updateEtaDataForExistingStopsForSystem: jest.spyOn(loader, "updateEtaDataForExistingStopsForSystem"),
};
Object.values(spies).forEach((spy: any) => {
spy.mockResolvedValue(undefined);
});
await loader.fetchAndUpdateAll();
await loader.updateAll();
Object.values(spies).forEach((spy: any) => {
expect(spy).toHaveBeenCalled();
@@ -52,7 +53,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
});
});
describe("fetchAndUpdateRouteDataForSystem", () => {
describe("updateRouteDataForSystem", () => {
it("updates route data in repository if response received", async () => {
// Arrange
// Test pruning
@@ -65,7 +66,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
updateGlobalFetchMockJson(fetchRouteDataSuccessfulResponse);
// Act
await loader.fetchAndUpdateRouteDataForSystem();
await loader.updateRouteDataForSystem();
// Assert
const routes = await loader.repository.getRoutes();
@@ -80,12 +81,12 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateRouteDataForSystem();
await loader.updateRouteDataForSystem();
});
});
});
describe("fetchAndUpdateStopAndPolylineDataForRoutesWithSystemId", () => {
describe("updateStopAndPolylineDataForRoutesInSystem", () => {
it("updates stop and polyline data if response received", async () => {
// Arrange
// Test pruning of stops only
@@ -99,7 +100,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
const stopsArray = Object.values(fetchStopAndPolylineDataSuccessfulResponse.stops);
await loader.fetchAndUpdateStopAndPolylineDataForRoutesInSystem();
await loader.updateStopAndPolylineDataForRoutesInSystem();
const stops = await loader.repository.getStops();
expect(stops.length).toEqual(stopsArray.length);
@@ -119,41 +120,147 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateStopAndPolylineDataForRoutesInSystem();
await loader.updateStopAndPolylineDataForRoutesInSystem();
});
})
});
describe("fetchAndUpdateShuttleDataForSystem", () => {
it("updates shuttle data in repository if response received", async () => {
describe("updateShuttleDataForSystemBasedOnProximityToRoutes", () => {
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();
await Promise.all(shuttlesToPrune.map(async (shuttle) => {
shuttle.systemId = systemId;
await loader.repository.addOrUpdateShuttle(shuttle);
}))
}));
updateGlobalFetchMockJson(fetchShuttleDataSuccessfulResponse);
const busesInResponse = Object.values(fetchShuttleDataSuccessfulResponse.buses);
const routes = generateMockRoutesWithPolylineCoordinates();
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 busesInResponse = Object.values(modifiedSuccessfulResponse.buses);
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 () => {
updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateShuttleDataForSystem();
await loader.updateShuttleDataForSystemBasedOnProximityToRoutes();
});
});
});
describe("fetchAndUpdateEtaDataForExistingStopsForSystem", () => {
it("calls fetchAndUpdateEtaDataForStopId for every stop in repository", async () => {
const spy = jest.spyOn(loader, "fetchAndUpdateEtaDataForStopId");
describe("updateEtaDataForExistingStopsForSystem", () => {
it("calls updateEtaDataForStopId for every stop in repository", async () => {
const spy = jest.spyOn(loader, "updateEtaDataForStopId");
const stops = generateMockStops();
stops.forEach((stop) => {
@@ -164,20 +271,20 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
await loader.repository.addOrUpdateStop(stop);
}));
await loader.fetchAndUpdateEtaDataForExistingStopsForSystem();
await loader.updateEtaDataForExistingStopsForSystem();
expect(spy.mock.calls.length).toEqual(stops.length);
});
});
describe("fetchAndUpdateEtaDataForStopId", () => {
describe("updateEtaDataForStopId", () => {
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);
await loader.updateEtaDataForStopId(stopId);
const etas = await loader.repository.getEtasForStopId(stopId);
expect(etas.length).toEqual(etasFromResponse.length);
@@ -187,7 +294,7 @@ describe("ApiBasedShuttleRepositoryLoader", () => {
updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateEtaDataForStopId("263");
await loader.updateEtaDataForStopId("263");
});
});
});