Merge pull request #41 from brendan-ch/feat/parking-repository-loader

[INT-61] feat/parking-repository-loader
This commit is contained in:
2025-04-11 17:20:09 -07:00
committed by GitHub
17 changed files with 436 additions and 118 deletions

View File

@@ -1,13 +1,12 @@
import { ShuttleRepositoryLoader } from "../loaders/ShuttleRepositoryLoader";
import { ETANotificationScheduler } from "../notifications/schedulers/ETANotificationScheduler";
import { TimedApiBasedShuttleRepositoryLoader } from "../loaders/TimedApiBasedShuttleRepositoryLoader";
import { TimedApiBasedRepositoryLoader } from "../loaders/TimedApiBasedRepositoryLoader";
import { UnoptimizedInMemoryShuttleRepository } from "../repositories/UnoptimizedInMemoryShuttleRepository";
import { RedisNotificationRepository } from "../repositories/RedisNotificationRepository";
import { NotificationRepository } from "../repositories/NotificationRepository";
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
import { InMemoryNotificationRepository } from "../repositories/InMemoryNotificationRepository";
import { AppleNotificationSender } from "../notifications/senders/AppleNotificationSender";
import { ApiBasedShuttleRepositoryLoader } from "../loaders/ApiBasedShuttleRepositoryLoader";
import { ApiBasedShuttleRepositoryLoader } from "../loaders/shuttle/ApiBasedShuttleRepositoryLoader";
export interface InterchangeSystemBuilderArguments {
name: string;
@@ -27,7 +26,7 @@ export class InterchangeSystem {
constructor(
public name: string,
public id: string,
public shuttleDataLoader: ShuttleRepositoryLoader,
public shuttleTimedDataLoader: TimedApiBasedRepositoryLoader,
public shuttleRepository: ShuttleGetterSetterRepository,
public notificationScheduler: ETANotificationScheduler,
public notificationRepository: NotificationRepository,
@@ -43,12 +42,15 @@ export class InterchangeSystem {
args: InterchangeSystemBuilderArguments,
) {
const shuttleRepository = new UnoptimizedInMemoryShuttleRepository();
const shuttleDataLoader = new TimedApiBasedShuttleRepositoryLoader(
const shuttleDataLoader = new ApiBasedShuttleRepositoryLoader(
args.passioSystemId,
args.id,
shuttleRepository
);
await shuttleDataLoader.start();
const timedShuttleDataLoader = new TimedApiBasedRepositoryLoader(
shuttleDataLoader,
);
await timedShuttleDataLoader.start();
const notificationRepository = new RedisNotificationRepository();
await notificationRepository.connect();
@@ -62,7 +64,7 @@ export class InterchangeSystem {
return new InterchangeSystem(
args.name,
args.id,
shuttleDataLoader,
timedShuttleDataLoader,
shuttleRepository,
notificationScheduler,
notificationRepository,
@@ -84,6 +86,11 @@ export class InterchangeSystem {
args.id,
shuttleRepository
);
// Note that this loader should not be started,
// so the test data doesn't get overwritten
const timedShuttleLoader = new TimedApiBasedRepositoryLoader(
shuttleDataLoader,
);
const notificationRepository = new InMemoryNotificationRepository();
const notificationScheduler = new ETANotificationScheduler(
@@ -96,7 +103,7 @@ export class InterchangeSystem {
return new InterchangeSystem(
args.name,
args.id,
shuttleDataLoader,
timedShuttleLoader,
shuttleRepository,
notificationScheduler,
notificationRepository,

View File

@@ -3,7 +3,7 @@ import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
import { MergedResolvers } from "./MergedResolvers";
import { ServerContext } from "./ServerContext";
import { loadShuttleTestData, supportedIntegrationTestSystems } from "./loaders/loadShuttleTestData";
import { loadShuttleTestData, supportedIntegrationTestSystems } from "./loaders/shuttle/loadShuttleTestData";
import { InterchangeSystem, InterchangeSystemBuilderArguments } from "./entities/InterchangeSystem";
const typeDefs = readFileSync("./schema.graphqls", "utf8");

View File

@@ -0,0 +1,6 @@
export class ApiResponseError extends Error {
constructor(message: string) {
super(message);
this.name = "ApiResponseError";
}
}

View File

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

View File

@@ -0,0 +1,43 @@
import { RepositoryLoader } from "./RepositoryLoader";
// To break down timed loading in the future:
// Add flags to the repository indicating which data users are subscribed to
// In the loader's `fetchAll` method, check flags and update only needed data
export class TimedApiBasedRepositoryLoader {
private shouldBeRunning: boolean = false;
private timer: any;
constructor(
public loader: RepositoryLoader,
public readonly timeoutMs: number = 10000,
) {
this.startFetchDataAndUpdate = this.startFetchDataAndUpdate.bind(this);
}
public async start() {
if (this.shouldBeRunning) {
console.warn("DataLoader timer is already running");
return;
}
this.shouldBeRunning = true;
await this.startFetchDataAndUpdate();
}
public stop() {
this.shouldBeRunning = false;
}
private async startFetchDataAndUpdate() {
if (!this.shouldBeRunning) return;
try {
await this.loader.fetchAndUpdateAll();
} catch (e) {
console.error(e);
}
this.timer = setTimeout(this.startFetchDataAndUpdate, this.timeoutMs);
}
}

View File

@@ -1,65 +0,0 @@
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
import { ApiBasedShuttleRepositoryLoader } from "./ApiBasedShuttleRepositoryLoader";
// Ideas to break this into smaller pieces in the future:
// Have one repository data loader running for each supported system
// Each data loader independently updates data based on frequency of usage
// Notes on this: we only need to reload ETA data frequently
// Other data can be reloaded periodically
// Detailed list:
// - ETA: reload frequently or switch to write-through approach
// - Shuttles: reload every minute
// - Routes: reload every few minutes
// - Stops: reload every few minutes
// - OrderedStops: reload every few minutes
// - Systems: reload once a day
export class TimedApiBasedShuttleRepositoryLoader extends ApiBasedShuttleRepositoryLoader {
private shouldBeRunning: boolean = false;
private timer: any;
readonly timeout = 10000;
constructor(
public passioSystemId: string,
public systemIdForConstructedData: string,
repository: ShuttleGetterSetterRepository,
) {
super(passioSystemId, systemIdForConstructedData, repository);
this.startFetchDataAndUpdate = this.startFetchDataAndUpdate.bind(this);
}
public async start() {
if (this.shouldBeRunning) {
console.warn("DataLoader timer is already running");
return;
}
this.shouldBeRunning = true;
await this.startFetchDataAndUpdate();
}
public stop() {
this.shouldBeRunning = false;
}
private async startFetchDataAndUpdate() {
if (!this.shouldBeRunning) return;
try {
await this.fetchAndUpdateRouteDataForSystem();
await this.fetchAndUpdateStopAndPolylineDataForRoutesInSystem();
await this.fetchAndUpdateShuttleDataForSystem();
// Because ETA method doesn't support pruning yet,
// add a call to the clear method here
await this.repository.clearEtaData();
await this.fetchAndUpdateEtaDataForExistingStopsForSystem();
} catch (e) {
console.error(e);
}
this.timer = setTimeout(this.startFetchDataAndUpdate, this.timeout);
}
}

View File

@@ -0,0 +1,83 @@
import { ParkingRepositoryLoader } from "./ParkingRepositoryLoader";
import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository";
import { createHash } from "node:crypto";
import { ApiResponseError } from "../ApiResponseError";
import { IParkingStructure } from "../../entities/ParkingRepositoryEntities";
class ApiParseError extends Error {
constructor(message: string) {
super(message);
this.name = "ApiParseError";
}
}
export class ChapmanApiBasedParkingRepositoryLoader implements ParkingRepositoryLoader {
public static readonly id = "chapman-parking-loader";
private readonly fetchUrl = "https://webfarm.chapman.edu/ParkingService/ParkingService/counts";
constructor(
public repository: ParkingGetterSetterRepository
) {
this.fetchAndUpdateParkingStructures = this.fetchAndUpdateParkingStructures.bind(this);
}
async fetchAndUpdateAll() {
await this.fetchAndUpdateParkingStructures();
}
async fetchAndUpdateParkingStructures(): Promise<void> {
let json: any;
try {
const response = await fetch(this.fetchUrl);
json = await response.json();
} catch(e: any) {
throw new ApiResponseError(e.message);
}
try {
if (typeof json.Structures === "object") {
const parkingStructures: IParkingStructure[] = json.Structures.map(this.constructIParkingStructureFromJson);
await Promise.all(parkingStructures.map(async (structure: IParkingStructure) => {
await this.repository.addOrUpdateParkingStructure(structure);
}));
}
} catch(e: any) {
throw new ApiParseError(e.message);
}
}
private constructIParkingStructureFromJson(jsonStructure: any) {
const structureToReturn: IParkingStructure = {
capacity: jsonStructure.Capacity,
coordinates: {
latitude: jsonStructure.Latitude,
longitude: jsonStructure.Longitude,
},
id: ChapmanApiBasedParkingRepositoryLoader.generateId(jsonStructure.Address),
name: jsonStructure.Name,
spotsAvailable: jsonStructure.CurrentCount,
address: jsonStructure.Address
}
return structureToReturn;
}
private static normalizeAddress(address: string): string {
return address
.toLowerCase()
.split(/\s+/)
.filter(part => part.length > 0)
.join(' ');
}
public static generateId(address: string): string {
const normalized = this.normalizeAddress(address);
const hash = createHash('sha256')
.update(normalized)
.digest('hex');
return hash.substring(0, 32);
}
}

View File

@@ -0,0 +1,5 @@
import { RepositoryLoader } from "../RepositoryLoader";
export interface ParkingRepositoryLoader extends RepositoryLoader {
fetchAndUpdateParkingStructures(): Promise<void>;
}

View File

@@ -0,0 +1,15 @@
import { ParkingGetterSetterRepository } from "../../repositories/ParkingGetterSetterRepository";
import { ChapmanApiBasedParkingRepositoryLoader } from "./ChapmanApiBasedParkingRepositoryLoader";
interface ParkingRepositoryBuilderArguments {
id: string;
repository: ParkingGetterSetterRepository;
}
export function buildParkingRepositoryLoaderIfExists(args: ParkingRepositoryBuilderArguments) {
if (args.id === ChapmanApiBasedParkingRepositoryLoader.id) {
return new ChapmanApiBasedParkingRepositoryLoader(args.repository);
}
return null;
}

View File

@@ -1,14 +1,8 @@
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
import { IEta, IRoute, IShuttle, IStop } from "../entities/ShuttleRepositoryEntities";
import { ShuttleGetterSetterRepository } from "../../repositories/ShuttleGetterSetterRepository";
import { IEta, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader";
import { IEntityWithId } from "../entities/SharedEntities";
export class ApiResponseError extends Error {
constructor(message: string) {
super(message);
this.name = "ApiResponseError";
}
}
import { IEntityWithId } from "../../entities/SharedEntities";
import { ApiResponseError } from "../ApiResponseError";
/**
* Class which can load data into a repository from the
@@ -34,6 +28,17 @@ export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader
return ids;
}
public async fetchAndUpdateAll() {
await this.fetchAndUpdateRouteDataForSystem();
await this.fetchAndUpdateStopAndPolylineDataForRoutesInSystem();
await this.fetchAndUpdateShuttleDataForSystem();
// Because ETA method doesn't support pruning yet,
// add a call to the clear method here
await this.repository.clearEtaData();
await this.fetchAndUpdateEtaDataForExistingStopsForSystem();
}
public async fetchAndUpdateRouteDataForSystem() {
const systemId = this.passioSystemId;
const routeIdsToPrune = await this.constructExistingEntityIdSet(async () => {

View File

@@ -1,4 +1,6 @@
export interface ShuttleRepositoryLoader {
import { RepositoryLoader } from "../RepositoryLoader";
export interface ShuttleRepositoryLoader extends RepositoryLoader {
fetchAndUpdateRouteDataForSystem(): Promise<void>;
fetchAndUpdateStopAndPolylineDataForRoutesInSystem(): Promise<void>;
fetchAndUpdateShuttleDataForSystem(): Promise<void>;

View File

@@ -1,7 +1,7 @@
// Mock data
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../entities/ShuttleRepositoryEntities";
import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository";
import { InterchangeSystemBuilderArguments } from "../entities/InterchangeSystem";
import { IEta, IOrderedStop, IRoute, IShuttle, IStop } from "../../entities/ShuttleRepositoryEntities";
import { ShuttleGetterSetterRepository } from "../../repositories/ShuttleGetterSetterRepository";
import { InterchangeSystemBuilderArguments } from "../../entities/InterchangeSystem";
export const supportedIntegrationTestSystems: InterchangeSystemBuilderArguments[] = [
{

View File

@@ -0,0 +1,92 @@
export const chapmanParkingStructureData = {
"CenterOnLatitude": 33.793379,
"CenterOnLongitude": -117.853099,
"LatitudeZoom": 0.0045,
"LongitudeZoom": 0.0045,
"Structures": [
{
"Address": "300 E Walnut, Orange, CA 92867",
"Capacity": 871,
"CurrentCount": 211,
"HdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/lastinger-hdpi.png",
"Latitude": 33.7945513,
"LdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/lastinger-ldpi.png",
"Levels": [
{
"Capacity": 871,
"CurrentCount": 211,
"FriendlyName": "All Levels",
"SystemName": "All"
},
{
"Capacity": 401,
"CurrentCount": 26,
"FriendlyName": "B1 Level",
"SystemName": "L1"
},
{
"Capacity": 470,
"CurrentCount": 185,
"FriendlyName": "B2 Level",
"SystemName": "L2"
}
],
"Longitude": -117.8518707,
"MdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/lastinger-mdpi.png",
"Name": "Anderson Structure",
"Timestamp": "/Date(1744327687147-0700)/",
"XhdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/lastinger-xhdpi.png"
},
{
"Address": "200 W Sycamore Ave, Orange, CA 92866-1053",
"Capacity": 692,
"CurrentCount": 282,
"HdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/barrera-hdpi.png",
"Latitude": 33.792937,
"LdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/barrera-ldpi.png",
"Levels": [
{
"Capacity": 692,
"CurrentCount": 282,
"FriendlyName": "All Levels",
"SystemName": "All"
},
{
"Capacity": 85,
"CurrentCount": 0,
"FriendlyName": "Level 1",
"SystemName": "L1"
},
{
"Capacity": 145,
"CurrentCount": 21,
"FriendlyName": "Level 2",
"SystemName": "L2"
},
{
"Capacity": 150,
"CurrentCount": 50,
"FriendlyName": "Level 3",
"SystemName": "L3"
},
{
"Capacity": 150,
"CurrentCount": 79,
"FriendlyName": "Level 4",
"SystemName": "L4"
},
{
"Capacity": 162,
"CurrentCount": 132,
"FriendlyName": "Level 5",
"SystemName": "L5"
}
],
"Longitude": -117.854782,
"MdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/barrera-mdpi.png",
"Name": "Barrera",
"Timestamp": "/Date(1744327647113-0700)/",
"XhdpiDetailImage": "https://webfarm.chapman.edu/ParkingService/barrera-xhdpi.png"
}
]
}

View File

@@ -1,10 +1,11 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { TimedApiBasedShuttleRepositoryLoader } from "../../src/loaders/TimedApiBasedShuttleRepositoryLoader";
import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader";
import { resetGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers";
import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository";
import { ApiBasedShuttleRepositoryLoader } from "../../src/loaders/shuttle/ApiBasedShuttleRepositoryLoader";
describe("TimedApiBasedRepositoryLoader", () => {
let loader: TimedApiBasedShuttleRepositoryLoader;
let timedLoader: TimedApiBasedRepositoryLoader;
let spies: any;
beforeAll(() => {
@@ -15,17 +16,17 @@ describe("TimedApiBasedRepositoryLoader", () => {
beforeEach(() => {
resetGlobalFetchMockJson();
loader = new TimedApiBasedShuttleRepositoryLoader(
const mockLoader = new ApiBasedShuttleRepositoryLoader(
"1",
"1",
new UnoptimizedInMemoryShuttleRepository()
new UnoptimizedInMemoryShuttleRepository(),
);
timedLoader = new TimedApiBasedRepositoryLoader(
mockLoader,
);
spies = {
fetchAndUpdateRouteDataForSystem: jest.spyOn(loader, 'fetchAndUpdateRouteDataForSystem'),
fetchAndUpdateStopAndPolylineDataForRoutesInSystem: jest.spyOn(loader, 'fetchAndUpdateStopAndPolylineDataForRoutesInSystem'),
fetchAndUpdateShuttleDataForSystem: jest.spyOn(loader, 'fetchAndUpdateShuttleDataForSystem'),
fetchAndUpdateEtaDataForExistingStopsForSystem: jest.spyOn(loader, 'fetchAndUpdateEtaDataForExistingStopsForSystem')
fetchAndUpdateAll: jest.spyOn(mockLoader, 'fetchAndUpdateAll'),
};
Object.values(spies).forEach((spy: any) => {
@@ -40,20 +41,20 @@ describe("TimedApiBasedRepositoryLoader", () => {
describe("start", () => {
it("should update internal state, call data fetching methods, and start a timer", async () => {
await loader.start();
expect(loader["shouldBeRunning"]).toBe(true);
await timedLoader.start();
expect(timedLoader["shouldBeRunning"]).toBe(true);
Object.values(spies).forEach((spy: any) => {
expect(spy).toHaveBeenCalled();
});
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), loader.timeout);
expect(loader.timeout).not.toBeUndefined();
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), timedLoader.timeoutMs);
expect(timedLoader.timeoutMs).not.toBeUndefined();
});
it("does nothing if timer is already running", async () => {
await loader.start();
await loader.start();
await timedLoader.start();
await timedLoader.start();
Object.values(spies).forEach((spy: any) => {
expect(spy).toHaveBeenCalledTimes(1);
@@ -63,8 +64,8 @@ describe("TimedApiBasedRepositoryLoader", () => {
describe("stop", () => {
it("should update internal state", async () => {
loader.stop();
expect(loader['shouldBeRunning']).toBe(false);
timedLoader.stop();
expect(timedLoader['shouldBeRunning']).toBe(false);
});
});
});

View File

@@ -0,0 +1,92 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import {
ChapmanApiBasedParkingRepositoryLoader
} from "../../../src/loaders/parking/ChapmanApiBasedParkingRepositoryLoader";
import { InMemoryParkingRepository } from "../../../src/repositories/InMemoryParkingRepository";
import {
resetGlobalFetchMockJson,
updateGlobalFetchMockJson,
updateGlobalFetchMockJsonToThrowSyntaxError
} from "../../testHelpers/fetchMockHelpers";
import {
chapmanParkingStructureData
} from "../../jsonSnapshots/chapmanParkingStructureData/chapmanParkingStructureData";
import { IParkingStructure } from "../../../src/entities/ParkingRepositoryEntities";
import { assertAsyncCallbackThrowsApiResponseError } from "../../testHelpers/assertAsyncCallbackThrowsApiResponseError";
describe("ChapmanApiBasedParkingRepositoryLoader", () => {
let loader: ChapmanApiBasedParkingRepositoryLoader;
beforeEach(() => {
loader = new ChapmanApiBasedParkingRepositoryLoader(
new InMemoryParkingRepository(),
);
resetGlobalFetchMockJson();
});
describe("fetchAndUpdateAll", () => {
it("calls all the correct methods", async () => {
const spies = {
fetchAndUpdateParkingStructures: jest.spyOn(loader, "fetchAndUpdateParkingStructures"),
};
Object.values(spies).forEach((spy: any) => {
spy.mockResolvedValue(undefined);
});
await loader.fetchAndUpdateAll();
Object.values(spies).forEach((spy: any) => {
expect(spy).toHaveBeenCalled();
});
});
});
describe("fetchAndUpdateParkingStructures", () => {
it("fetches and update parking structures with unique IDs", async () => {
updateGlobalFetchMockJson(chapmanParkingStructureData);
await loader.fetchAndUpdateParkingStructures();
let expectedStructures: IParkingStructure[] = [
{
address: "300 E Walnut, Orange, CA 92867",
capacity: 871,
spotsAvailable: 211,
coordinates: {
latitude: 33.7945513,
longitude: -117.8518707,
},
name: "Anderson Structure",
id: "",
},
{
address: "200 W Sycamore Ave, Orange, CA 92866-1053",
capacity: 692,
spotsAvailable: 282,
coordinates: {
latitude: 33.792937,
longitude: -117.854782
},
name: "Barrera",
id: "",
}
];
expectedStructures[0].id = ChapmanApiBasedParkingRepositoryLoader.generateId(expectedStructures[0].address);
expectedStructures[1].id = ChapmanApiBasedParkingRepositoryLoader.generateId(expectedStructures[1].address);
const structuresFromLoader = await loader.repository.getParkingStructures();
expect(structuresFromLoader).toEqual(expectedStructures);
});
it("throws ApiResponseError if data is incorrect", async () => {
updateGlobalFetchMockJsonToThrowSyntaxError();
await assertAsyncCallbackThrowsApiResponseError(async () => {
await loader.fetchAndUpdateParkingStructures();
});
})
});
});

View File

@@ -1,26 +1,23 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { ApiBasedShuttleRepositoryLoader, ApiResponseError } from "../../src/loaders/ApiBasedShuttleRepositoryLoader";
import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository";
import { fetchRouteDataSuccessfulResponse } from "../jsonSnapshots/fetchRouteData/fetchRouteDataSuccessfulResponse";
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { ApiBasedShuttleRepositoryLoader } from "../../../src/loaders/shuttle/ApiBasedShuttleRepositoryLoader";
import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/UnoptimizedInMemoryShuttleRepository";
import { fetchRouteDataSuccessfulResponse } from "../../jsonSnapshots/fetchRouteData/fetchRouteDataSuccessfulResponse";
import {
fetchStopAndPolylineDataSuccessfulResponse
} from "../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse";
import { generateMockRoutes, generateMockShuttles, generateMockStops } from "../testHelpers/mockDataGenerators";
} from "../../jsonSnapshots/fetchStopAndPolylineData/fetchStopAndPolylineDataSuccessfulResponse";
import { generateMockRoutes, generateMockShuttles, generateMockStops } from "../../testHelpers/mockDataGenerators";
import {
fetchShuttleDataSuccessfulResponse
} from "../jsonSnapshots/fetchShuttleData/fetchShuttleDataSuccessfulResponse";
import { fetchEtaDataSuccessfulResponse } from "../jsonSnapshots/fetchEtaData/fetchEtaDataSuccessfulResponse";
} from "../../jsonSnapshots/fetchShuttleData/fetchShuttleDataSuccessfulResponse";
import { fetchEtaDataSuccessfulResponse } from "../../jsonSnapshots/fetchEtaData/fetchEtaDataSuccessfulResponse";
import {
resetGlobalFetchMockJson,
updateGlobalFetchMockJson,
updateGlobalFetchMockJsonToThrowSyntaxError
} from "../testHelpers/fetchMockHelpers";
} from "../../testHelpers/fetchMockHelpers";
import { assertAsyncCallbackThrowsApiResponseError } from "../../testHelpers/assertAsyncCallbackThrowsApiResponseError";
async function assertAsyncCallbackThrowsApiResponseError(callback: () => Promise<any>) {
await expect(callback).rejects.toThrow(ApiResponseError);
}
describe("ApiBasedRepositoryLoader", () => {
describe("ApiBasedShuttleRepositoryLoader", () => {
let loader: ApiBasedShuttleRepositoryLoader;
beforeEach(() => {
@@ -28,7 +25,33 @@ describe("ApiBasedRepositoryLoader", () => {
resetGlobalFetchMockJson();
});
afterEach(() => {
jest.clearAllMocks();
});
const systemId = "1";
describe("fetchAndUpdateAll", () => {
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"),
};
Object.values(spies).forEach((spy: any) => {
spy.mockResolvedValue(undefined);
});
await loader.fetchAndUpdateAll();
Object.values(spies).forEach((spy: any) => {
expect(spy).toHaveBeenCalled();
});
});
});
describe("fetchAndUpdateRouteDataForSystem", () => {
it("updates route data in repository if response received", async () => {
// Arrange

View File

@@ -0,0 +1,6 @@
import { expect } from "@jest/globals";
import { ApiResponseError } from "../../src/loaders/ApiResponseError";
export async function assertAsyncCallbackThrowsApiResponseError(callback: () => Promise<any>) {
await expect(callback).rejects.toThrow(ApiResponseError);
}