mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-17 07:50:31 +00:00
Merge pull request #41 from brendan-ch/feat/parking-repository-loader
[INT-61] feat/parking-repository-loader
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
6
src/loaders/ApiResponseError.ts
Normal file
6
src/loaders/ApiResponseError.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export class ApiResponseError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "ApiResponseError";
|
||||
}
|
||||
}
|
||||
3
src/loaders/RepositoryLoader.ts
Normal file
3
src/loaders/RepositoryLoader.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface RepositoryLoader {
|
||||
fetchAndUpdateAll(): Promise<void>;
|
||||
}
|
||||
43
src/loaders/TimedApiBasedRepositoryLoader.ts
Normal file
43
src/loaders/TimedApiBasedRepositoryLoader.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
5
src/loaders/parking/ParkingRepositoryLoader.ts
Normal file
5
src/loaders/parking/ParkingRepositoryLoader.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { RepositoryLoader } from "../RepositoryLoader";
|
||||
|
||||
export interface ParkingRepositoryLoader extends RepositoryLoader {
|
||||
fetchAndUpdateParkingStructures(): Promise<void>;
|
||||
}
|
||||
15
src/loaders/parking/buildParkingRepositoryLoaderIfExists.ts
Normal file
15
src/loaders/parking/buildParkingRepositoryLoaderIfExists.ts
Normal 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;
|
||||
}
|
||||
@@ -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 () => {
|
||||
@@ -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>;
|
||||
@@ -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[] = [
|
||||
{
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user