mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-16 23:40:32 +00:00
Merge pull request #78 from brendan-ch/chore/implement-cleaning-of-passio-data
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface RepositoryLoader {
|
||||
fetchAndUpdateAll(): Promise<void>;
|
||||
updateAll(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export class ChapmanApiBasedParkingRepositoryLoader implements ParkingRepository
|
||||
this.fetchAndUpdateParkingStructures = this.fetchAndUpdateParkingStructures.bind(this);
|
||||
}
|
||||
|
||||
async fetchAndUpdateAll() {
|
||||
async updateAll() {
|
||||
await this.fetchAndUpdateParkingStructures();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("ChapmanApiBasedParkingRepositoryLoader", () => {
|
||||
spy.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
await loader.fetchAndUpdateAll();
|
||||
await loader.updateAll();
|
||||
|
||||
Object.values(spies).forEach((spy: any) => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user