diff --git a/docker-compose.yml b/docker-compose.yml index 05a69c7..335f941 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,7 +31,7 @@ services: ports: - "4000:4000" depends_on: - - redis + - redis-no-persistence environment: <<: *common-server-environment @@ -42,7 +42,7 @@ services: build: . command: npm run test depends_on: - - redis + - redis-no-persistence environment: <<: *common-server-environment @@ -52,3 +52,12 @@ services: image: redis:alpine ports: - "6379:6379" + volumes: + - ./redis.conf:/usr/local/etc/redis/redis.conf + command: redis-server /usr/local/etc/redis/redis.conf + + redis-no-persistence: + image: redis:alpine + ports: + - "6379:6379" + diff --git a/package-lock.json b/package-lock.json index 4d485f2..2bbd249 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "@apollo/server": "^4.11.2", "graphql": "^16.10.0", - "jsonwebtoken": "^9.0.2" + "jsonwebtoken": "^9.0.2", + "redis": "^4.7.0" }, "devDependencies": { "@graphql-codegen/cli": "5.0.3", @@ -628,25 +629,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1262,9 +1263,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1274,14 +1275,14 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -1329,9 +1330,9 @@ "dev": true }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -3413,6 +3414,64 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@repeaterjs/repeater": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", @@ -4496,6 +4555,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -5365,6 +5432,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7993,6 +8068,22 @@ "node": ">= 6" } }, + "node_modules/redis": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.0", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", diff --git a/package.json b/package.json index d8ef7ca..7be6333 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@apollo/server": "^4.11.2", "graphql": "^16.10.0", - "jsonwebtoken": "^9.0.2" + "jsonwebtoken": "^9.0.2", + "redis": "^4.7.0" } } diff --git a/redis.conf b/redis.conf new file mode 100644 index 0000000..b916b95 --- /dev/null +++ b/redis.conf @@ -0,0 +1,28 @@ +# See https://raw.githubusercontent.com/redis/redis/unstable/redis.conf +# for a full example + +############################## APPEND ONLY MODE ############################### + +# By default Redis asynchronously dumps the dataset on disk. This mode is +# good enough in many applications, but an issue with the Redis process or +# a power outage may result into a few minutes of writes lost (depending on +# the configured save points). +# +# The Append Only File is an alternative persistence mode that provides +# much better durability. For instance using the default data fsync policy +# (see later in the config file) Redis can lose just one second of writes in a +# dramatic event like a server power outage, or a single write if something +# wrong with the Redis process itself happens, but the operating system is +# still running correctly. +# +# AOF and RDB persistence can be enabled at the same time without problems. +# If the AOF is enabled on startup Redis will load the AOF, that is the file +# with the better durability guarantees. +# +# Note that changing this value in a config file of an existing database and +# restarting the server can lead to data loss. A conversion needs to be done +# by setting it via CONFIG command on a live server first. +# +# Please check https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/ for more information. + +appendonly yes diff --git a/src/ServerContext.ts b/src/ServerContext.ts index 8dade11..732f7d8 100644 --- a/src/ServerContext.ts +++ b/src/ServerContext.ts @@ -1,7 +1,8 @@ import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; -import { GetterSetterRepository } from "./repositories/GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "./repositories/ShuttleGetterSetterRepository"; +import { NotificationRepository } from "./repositories/NotificationRepository"; export interface ServerContext { - repository: GetterSetterRepository; - notificationService: ETANotificationScheduler; + shuttleRepository: ShuttleGetterSetterRepository; + notificationRepository: NotificationRepository; } diff --git a/src/index.ts b/src/index.ts index 2bff64c..d1a1d14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,11 +3,13 @@ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; import { MergedResolvers } from "./MergedResolvers"; import { ServerContext } from "./ServerContext"; -import { UnoptimizedInMemoryRepository } from "./repositories/UnoptimizedInMemoryRepository"; -import { TimedApiBasedRepositoryLoader } from "./loaders/TimedApiBasedRepositoryLoader"; +import { UnoptimizedInMemoryShuttleRepository } from "./repositories/UnoptimizedInMemoryShuttleRepository"; +import { TimedApiBasedShuttleRepositoryLoader } from "./loaders/TimedApiBasedShuttleRepositoryLoader"; import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; -import { loadTestData } from "./loaders/loadTestData"; +import { loadShuttleTestData } from "./loaders/loadShuttleTestData"; import { AppleNotificationSender } from "./notifications/senders/AppleNotificationSender"; +import { InMemoryNotificationRepository } from "./repositories/InMemoryNotificationRepository"; +import { NotificationRepository } from "./repositories/NotificationRepository"; const typeDefs = readFileSync("./schema.graphqls", "utf8"); @@ -18,29 +20,45 @@ async function main() { introspection: process.env.NODE_ENV !== "production", }); - const repository = new UnoptimizedInMemoryRepository(); + const shuttleRepository = new UnoptimizedInMemoryShuttleRepository(); + + let notificationRepository: NotificationRepository; let notificationService: ETANotificationScheduler; if (process.argv.length > 2 && process.argv[2] == "integration-testing") { console.log("Using integration testing setup") - await loadTestData(repository); + await loadShuttleTestData(shuttleRepository); + const appleNotificationSender = new AppleNotificationSender(false); - notificationService = new ETANotificationScheduler(repository, appleNotificationSender); + notificationRepository = new InMemoryNotificationRepository(); + + notificationService = new ETANotificationScheduler( + shuttleRepository, + notificationRepository, + appleNotificationSender + ); + notificationService.startListeningForUpdates(); } else { - const repositoryDataUpdater = new TimedApiBasedRepositoryLoader( - repository + const repositoryDataUpdater = new TimedApiBasedShuttleRepositoryLoader( + shuttleRepository, ); await repositoryDataUpdater.start(); - notificationService = new ETANotificationScheduler(repository); + + notificationRepository = new InMemoryNotificationRepository(); + notificationService = new ETANotificationScheduler( + shuttleRepository, + notificationRepository + ); + notificationService.startListeningForUpdates(); } const { url } = await startStandaloneServer(server, { listen: { port: process.env.PORT ? parseInt(process.env.PORT) : 4000, }, - context: async ({ req, res }) => { + context: async () => { return { - repository, - notificationService, + shuttleRepository, + notificationRepository, } }, }); diff --git a/src/loaders/ApiBasedRepositoryLoader.ts b/src/loaders/ApiBasedShuttleRepositoryLoader.ts similarity index 97% rename from src/loaders/ApiBasedRepositoryLoader.ts rename to src/loaders/ApiBasedShuttleRepositoryLoader.ts index 50003db..b8a6285 100644 --- a/src/loaders/ApiBasedRepositoryLoader.ts +++ b/src/loaders/ApiBasedShuttleRepositoryLoader.ts @@ -1,6 +1,6 @@ -import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository"; import { IEntityWithId, IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; -import { RepositoryLoader } from "./RepositoryLoader"; +import { ShuttleRepositoryLoader } from "./ShuttleRepositoryLoader"; export class ApiResponseError extends Error { constructor(message: string) { @@ -14,12 +14,12 @@ export class ApiResponseError extends Error { * Passio Go API. Supports automatic pruning of all data types * which inherit from `IEntityWithId`. */ -export class ApiBasedRepositoryLoader implements RepositoryLoader { +export class ApiBasedShuttleRepositoryLoader implements ShuttleRepositoryLoader { supportedSystemIds = ["263"]; baseUrl = "https://passiogo.com/mapGetData.php"; constructor( - public repository: GetterSetterRepository, + public repository: ShuttleGetterSetterRepository, ) { } diff --git a/src/loaders/RepositoryLoader.ts b/src/loaders/ShuttleRepositoryLoader.ts similarity index 94% rename from src/loaders/RepositoryLoader.ts rename to src/loaders/ShuttleRepositoryLoader.ts index 5e34c49..9ffa137 100644 --- a/src/loaders/RepositoryLoader.ts +++ b/src/loaders/ShuttleRepositoryLoader.ts @@ -1,4 +1,4 @@ -export interface RepositoryLoader { +export interface ShuttleRepositoryLoader { fetchAndUpdateSystemData(): Promise; fetchAndUpdateRouteDataForExistingSystemsInRepository(): Promise; fetchAndUpdateRouteDataForSystemId(systemId: string): Promise; diff --git a/src/loaders/TimedApiBasedRepositoryLoader.ts b/src/loaders/TimedApiBasedShuttleRepositoryLoader.ts similarity index 83% rename from src/loaders/TimedApiBasedRepositoryLoader.ts rename to src/loaders/TimedApiBasedShuttleRepositoryLoader.ts index b747b0e..1d15273 100644 --- a/src/loaders/TimedApiBasedRepositoryLoader.ts +++ b/src/loaders/TimedApiBasedShuttleRepositoryLoader.ts @@ -1,6 +1,5 @@ -import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; -import { IEta, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; -import { ApiBasedRepositoryLoader } from "./ApiBasedRepositoryLoader"; +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 @@ -16,14 +15,14 @@ import { ApiBasedRepositoryLoader } from "./ApiBasedRepositoryLoader"; // - OrderedStops: reload every few minutes // - Systems: reload once a day -export class TimedApiBasedRepositoryLoader extends ApiBasedRepositoryLoader { +export class TimedApiBasedShuttleRepositoryLoader extends ApiBasedShuttleRepositoryLoader { private shouldBeRunning: boolean = false; private timer: any; readonly timeout = 10000; constructor( - repository: GetterSetterRepository, + repository: ShuttleGetterSetterRepository, ) { super(repository); this.startFetchDataAndUpdate = this.startFetchDataAndUpdate.bind(this); @@ -62,4 +61,4 @@ export class TimedApiBasedRepositoryLoader extends ApiBasedRepositoryLoader { this.timer = setTimeout(this.startFetchDataAndUpdate, this.timeout); } -} \ No newline at end of file +} diff --git a/src/loaders/loadTestData.ts b/src/loaders/loadShuttleTestData.ts similarity index 99% rename from src/loaders/loadTestData.ts rename to src/loaders/loadShuttleTestData.ts index ad75af4..76cce74 100644 --- a/src/loaders/loadTestData.ts +++ b/src/loaders/loadShuttleTestData.ts @@ -1,6 +1,6 @@ // Mock data import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; -import { GetterSetterRepository } from "../repositories/GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "../repositories/ShuttleGetterSetterRepository"; const systems: ISystem[] = [ { @@ -4454,7 +4454,7 @@ const etas: IEta[] = [ } ]; -export async function loadTestData(repository: GetterSetterRepository) { +export async function loadShuttleTestData(repository: ShuttleGetterSetterRepository) { await Promise.all(systems.map(async (system) => { await repository.addOrUpdateSystem(system); })); diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 8789cdd..98a16a2 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -1,54 +1,31 @@ -import { GetterRepository } from "../../repositories/GetterRepository"; -import { TupleKey } from "../../types/TupleKey"; +import { ShuttleGetterRepository } from "../../repositories/ShuttleGetterRepository"; import { IEta } from "../../entities/entities"; import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; - -export interface NotificationLookupArguments { - deviceId: string; - shuttleId: string; - stopId: string; -} - -export interface NotificationSchedulingArguments extends NotificationLookupArguments { - /** - * Value which specifies the ETA of the shuttle for when - * the notification should fire. - * For example, a secondsThreshold of 180 would mean that the notification - * fires when the ETA drops below 3 minutes. - */ - secondsThreshold: number; -} - -type DeviceIdSecondsThresholdAssociation = { [key: string]: number }; +import { + NotificationRepository, + ScheduledNotification +} from "../../repositories/NotificationRepository"; +import { InMemoryNotificationRepository } from "../../repositories/InMemoryNotificationRepository"; export class ETANotificationScheduler { public static readonly defaultSecondsThresholdForNotificationToFire = 180; - constructor(private repository: GetterRepository, - private appleNotificationSender = new AppleNotificationSender() + constructor( + private shuttleRepository: ShuttleGetterRepository, + private notificationRepository: NotificationRepository = new InMemoryNotificationRepository(), + private appleNotificationSender = new AppleNotificationSender() ) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); - this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); - this.scheduleNotification = this.scheduleNotification.bind(this); } - /** - * An object of device ID arrays to deliver notifications to. - * The key should be a combination of the shuttle ID and - * stop ID, which can be generated using `TupleKey`. - * The value is a dictionary of the device ID to the stored seconds threshold. - * @private - */ - private deviceIdsToDeliverTo: { [key: string]: DeviceIdSecondsThresholdAssociation } = {} - - private async sendEtaNotificationImmediately(notificationData: NotificationSchedulingArguments): Promise { + private async sendEtaNotificationImmediately(notificationData: ScheduledNotification): Promise { const { deviceId, shuttleId, stopId } = notificationData; - const shuttle = await this.repository.getShuttleById(shuttleId); - const stop = await this.repository.getStopById(stopId); - const eta = await this.repository.getEtaForShuttleAndStopId(shuttleId, stopId); + const shuttle = await this.shuttleRepository.getShuttleById(shuttleId); + const stop = await this.shuttleRepository.getStopById(stopId); + const eta = await this.shuttleRepository.getEtaForShuttleAndStopId(shuttleId, stopId); if (!shuttle) { console.warn(`Notification ${notificationData} fell through; no associated shuttle`); return false; @@ -73,33 +50,29 @@ export class ETANotificationScheduler { } private async etaSubscriberCallback(eta: IEta) { - const tuple = new TupleKey(eta.shuttleId, eta.stopId); - const tupleKey = tuple.toString(); - if (this.deviceIdsToDeliverTo[tupleKey] === undefined) { - return; - } - const deviceIdsToRemove = new Set(); - for (let deviceId of Object.keys(this.deviceIdsToDeliverTo[tupleKey])) { - const scheduledNotificationData: NotificationSchedulingArguments = { - deviceId, - secondsThreshold: this.deviceIdsToDeliverTo[tupleKey][deviceId], - shuttleId: eta.shuttleId, - stopId: eta.stopId, - } + const notifications = await this.notificationRepository.getAllNotificationsForShuttleAndStopId( + eta.shuttleId, + eta.stopId + ) - const deliveredSuccessfully = await this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(scheduledNotificationData, eta.secondsRemaining); + for (let notification of notifications) { + const deliveredSuccessfully = await this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(notification, eta.secondsRemaining); if (deliveredSuccessfully) { - deviceIdsToRemove.add(deviceId); + deviceIdsToRemove.add(notification.deviceId); } } deviceIdsToRemove.forEach((deviceId) => { - delete this.deviceIdsToDeliverTo[tupleKey][deviceId] + this.notificationRepository.deleteNotificationIfExists({ + shuttleId: eta.shuttleId, + stopId: eta.stopId, + deviceId, + }) }); } - private async sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(notificationObject: NotificationSchedulingArguments, etaSecondsRemaining: number) { + private async sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold(notificationObject: ScheduledNotification, etaSecondsRemaining: number) { if (etaSecondsRemaining > notificationObject.secondsThreshold) { return false; } @@ -107,82 +80,12 @@ export class ETANotificationScheduler { return await this.sendEtaNotificationImmediately(notificationObject); } - /** - * Queue a notification to be sent. - * @param deviceId The device ID to send the notification to. - * @param shuttleId Shuttle ID of ETA object to check. - * @param stopId Stop ID of ETA object to check. - * @param secondsThreshold Value which specifies the ETA of the shuttle for when - * the notification should fire. - */ - public async scheduleNotification({ deviceId, shuttleId, stopId, secondsThreshold }: NotificationSchedulingArguments) { - const tuple = new TupleKey(shuttleId, stopId); - if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { - this.deviceIdsToDeliverTo[tuple.toString()] = {}; - } - - this.deviceIdsToDeliverTo[tuple.toString()][deviceId] = secondsThreshold; - - this.repository.unsubscribeFromEtaUpdates(this.etaSubscriberCallback); - this.repository.subscribeToEtaUpdates(this.etaSubscriberCallback); + // The following is a workaround for the constructor being called twice + public startListeningForUpdates() { + this.shuttleRepository.subscribeToEtaUpdates(this.etaSubscriberCallback); } - /** - * Cancel a pending notification. - * @param deviceId The device ID of the notification. - * @param shuttleId Shuttle ID of the ETA object. - * @param stopId Stop ID of the ETA object. - */ - public async cancelNotificationIfExists({ deviceId, shuttleId, stopId }: NotificationLookupArguments) { - const tupleKey = new TupleKey(shuttleId, stopId); - if ( - this.deviceIdsToDeliverTo[tupleKey.toString()] === undefined - || !(deviceId in this.deviceIdsToDeliverTo[tupleKey.toString()]) - ) { - return; - } - - delete this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; - } - - /** - * Check whether the notification is scheduled. - */ - public isNotificationScheduled(lookupArguments: NotificationLookupArguments): boolean { - return this.getSecondsThresholdForScheduledNotification(lookupArguments) != null; - } - - public getSecondsThresholdForScheduledNotification({ deviceId, shuttleId, stopId }: NotificationLookupArguments): number | null { - const tuple = new TupleKey(shuttleId, stopId); - if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { - return null; - } - return this.deviceIdsToDeliverTo[tuple.toString()][deviceId]; - } - - /** - * Return all scheduled notification for the given device ID. - * @param deviceId - */ - public async getAllScheduledNotificationsForDevice(deviceId: string): Promise { - const scheduledNotifications: NotificationSchedulingArguments[] = []; - - for (const key of Object.keys(this.deviceIdsToDeliverTo)) { - if (deviceId in this.deviceIdsToDeliverTo[key]) { - const tupleKey = TupleKey.fromExistingStringKey(key); - const shuttleId = tupleKey.tuple[0] - const stopId = tupleKey.tuple[1]; - const secondsThreshold = this.deviceIdsToDeliverTo[key][deviceId]; - - scheduledNotifications.push({ - shuttleId, - stopId, - deviceId, - secondsThreshold, - }); - } - } - - return scheduledNotifications; + public stopListeningForUpdates() { + this.shuttleRepository.subscribeToEtaUpdates(this.etaSubscriberCallback); } } diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts index d64df98..8e5904a 100644 --- a/src/notifications/senders/AppleNotificationSender.ts +++ b/src/notifications/senders/AppleNotificationSender.ts @@ -17,6 +17,9 @@ export class AppleNotificationSender { private _lastRefreshedTimeMs: number | undefined = undefined; constructor(private shouldActuallySendNotifications = true) { + this.sendNotificationImmediately = this.sendNotificationImmediately.bind(this); + this.lastReloadedTimeForAPNsIsTooRecent = this.lastReloadedTimeForAPNsIsTooRecent.bind(this); + this.reloadAPNsTokenIfTimePassed = this.reloadAPNsTokenIfTimePassed.bind(this); } get lastRefreshedTimeMs(): number | undefined { diff --git a/src/repositories/InMemoryNotificationRepository.ts b/src/repositories/InMemoryNotificationRepository.ts new file mode 100644 index 0000000..513f7b7 --- /dev/null +++ b/src/repositories/InMemoryNotificationRepository.ts @@ -0,0 +1,144 @@ +import { + Listener, + NotificationEvent, + NotificationLookupArguments, + NotificationRepository, + ScheduledNotification +} from "./NotificationRepository"; +import { TupleKey } from "../types/TupleKey"; + +type DeviceIdSecondsThresholdAssociation = { [key: string]: number }; + +export class InMemoryNotificationRepository implements NotificationRepository { + /** + * An object of device ID arrays to deliver notifications to. + * The key should be a combination of the shuttle ID and + * stop ID, which can be generated using `TupleKey`. + * The value is a dictionary of the device ID to the stored seconds threshold. + * @private + */ + private deviceIdsToDeliverTo: { [key: string]: DeviceIdSecondsThresholdAssociation } = {} + + private listeners: Listener[] = []; + + constructor() { + this.getAllNotificationsForShuttleAndStopId = this.getAllNotificationsForShuttleAndStopId.bind(this); + this.getSecondsThresholdForNotificationIfExists = this.getSecondsThresholdForNotificationIfExists.bind(this); + this.deleteNotificationIfExists = this.deleteNotificationIfExists.bind(this); + this.addOrUpdateNotification = this.addOrUpdateNotification.bind(this); + this.isNotificationScheduled = this.isNotificationScheduled.bind(this); + this.subscribeToNotificationChanges = this.subscribeToNotificationChanges.bind(this); + this.unsubscribeFromNotificationChanges = this.unsubscribeFromNotificationChanges.bind(this); + } + + async getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string) { + const tuple = new TupleKey(shuttleId, stopId); + if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { + return []; + } + + return Object.keys(this.deviceIdsToDeliverTo[tuple.toString()]) + .map((deviceId) => { + return { + shuttleId, + stopId, + deviceId, + secondsThreshold: this.deviceIdsToDeliverTo[tuple.toString()][deviceId] + } + }); + } + + async getSecondsThresholdForNotificationIfExists({ + shuttleId, + stopId, + deviceId + }: NotificationLookupArguments) { + const tuple = new TupleKey(shuttleId, stopId); + if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { + return null; + } + return this.deviceIdsToDeliverTo[tuple.toString()][deviceId]; + } + + async isNotificationScheduled(lookupArguments: NotificationLookupArguments): Promise { + const threshold = await this.getSecondsThresholdForNotificationIfExists(lookupArguments); + return threshold !== null; + } + + async addOrUpdateNotification({ + shuttleId, + stopId, + deviceId, + secondsThreshold + }: ScheduledNotification) { + const tuple = new TupleKey(shuttleId, stopId); + if (this.deviceIdsToDeliverTo[tuple.toString()] === undefined) { + this.deviceIdsToDeliverTo[tuple.toString()] = {}; + } + + this.deviceIdsToDeliverTo[tuple.toString()][deviceId] = secondsThreshold; + this.listeners.forEach((listener: Listener) => { + const event: NotificationEvent = { + event: 'addOrUpdate', + notification: { + shuttleId, + stopId, + deviceId, + secondsThreshold + }, + } + + listener(event); + }) + } + + async deleteNotificationIfExists({ + deviceId, + shuttleId, + stopId + }: NotificationLookupArguments) { + const tupleKey = new TupleKey(shuttleId, stopId); + if ( + this.deviceIdsToDeliverTo[tupleKey.toString()] === undefined + || !(deviceId in this.deviceIdsToDeliverTo[tupleKey.toString()]) + ) { + return; + } + + const secondsThreshold = this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; + delete this.deviceIdsToDeliverTo[tupleKey.toString()][deviceId]; + + if (Object.keys(this.deviceIdsToDeliverTo[tupleKey.toString()]).length === 0) { + // no more device IDs remaining for this key combination + delete this.deviceIdsToDeliverTo[tupleKey.toString()]; + } + + this.listeners.forEach((listener) => { + const event: NotificationEvent = { + event: 'delete', + notification: { + deviceId, + shuttleId, + stopId, + secondsThreshold + } + } + + listener(event); + }) + } + + public subscribeToNotificationChanges(listener: Listener): void { + const index = this.listeners.findIndex((existingListener) => existingListener == listener); + if (index < 0) { + this.listeners.push(listener); + } + } + + public unsubscribeFromNotificationChanges(listener: Listener): void { + const index = this.listeners.findIndex((existingListener) => existingListener == listener); + if (index >= 0) { + this.listeners.splice(index, 1); + } + } +} diff --git a/src/repositories/NotificationRepository.ts b/src/repositories/NotificationRepository.ts new file mode 100644 index 0000000..0f4ff86 --- /dev/null +++ b/src/repositories/NotificationRepository.ts @@ -0,0 +1,33 @@ +export interface NotificationLookupArguments { + deviceId: string; + shuttleId: string; + stopId: string; +} + +export interface ScheduledNotification extends NotificationLookupArguments { + /** + * Value which specifies the ETA of the shuttle for when + * the notification should fire. + * For example, a secondsThreshold of 180 would mean that the notification + * fires when the ETA drops below 3 minutes. + */ + secondsThreshold: number; +} + +export type Listener = ((event: NotificationEvent) => any); + +export interface NotificationEvent { + notification: ScheduledNotification, + event: 'delete' | 'addOrUpdate' +} + +export interface NotificationRepository { + getAllNotificationsForShuttleAndStopId(shuttleId: string, stopId: string): Promise; + getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; + isNotificationScheduled(lookupArguments: NotificationLookupArguments): Promise; + addOrUpdateNotification(notification: ScheduledNotification): Promise; + deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise; + + subscribeToNotificationChanges(listener: Listener): void; + unsubscribeFromNotificationChanges(listener: Listener): void; +} diff --git a/src/repositories/GetterRepository.ts b/src/repositories/ShuttleGetterRepository.ts similarity index 97% rename from src/repositories/GetterRepository.ts rename to src/repositories/ShuttleGetterRepository.ts index 9c5f27f..664328f 100644 --- a/src/repositories/GetterRepository.ts +++ b/src/repositories/ShuttleGetterRepository.ts @@ -1,6 +1,6 @@ import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; -export interface GetterRepository { +export interface ShuttleGetterRepository { getSystems(): Promise; getSystemById(systemId: string): Promise; diff --git a/src/repositories/GetterSetterRepository.ts b/src/repositories/ShuttleGetterSetterRepository.ts similarity index 85% rename from src/repositories/GetterSetterRepository.ts rename to src/repositories/ShuttleGetterSetterRepository.ts index 1c914c0..0c0bf90 100644 --- a/src/repositories/GetterSetterRepository.ts +++ b/src/repositories/ShuttleGetterSetterRepository.ts @@ -1,16 +1,16 @@ // If types match closely, we can use TypeScript "casting" // to convert from data repo to GraphQL schema -import { GetterRepository } from "./GetterRepository"; +import { ShuttleGetterRepository } from "./ShuttleGetterRepository"; import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; /** - * GetterRepository interface for data derived from Passio API. + * ShuttleGetterRepository interface for data derived from Passio API. * The repository is not designed to have write locks in place. * Objects passed from/to the repository should be treated * as disposable. */ -export interface GetterSetterRepository extends GetterRepository { +export interface ShuttleGetterSetterRepository extends ShuttleGetterRepository { // Setter methods addOrUpdateSystem(system: ISystem): Promise; addOrUpdateRoute(route: IRoute): Promise; diff --git a/src/repositories/UnoptimizedInMemoryRepository.ts b/src/repositories/UnoptimizedInMemoryShuttleRepository.ts similarity index 97% rename from src/repositories/UnoptimizedInMemoryRepository.ts rename to src/repositories/UnoptimizedInMemoryShuttleRepository.ts index e347803..59afac9 100644 --- a/src/repositories/UnoptimizedInMemoryRepository.ts +++ b/src/repositories/UnoptimizedInMemoryShuttleRepository.ts @@ -1,4 +1,4 @@ -import { GetterSetterRepository } from "./GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "./ShuttleGetterSetterRepository"; import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities"; /** @@ -6,7 +6,7 @@ import { IEntityWithId, IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } f * (I would optimize it with actual data structures, but I'm * switching to another data store later anyways) */ -export class UnoptimizedInMemoryRepository implements GetterSetterRepository { +export class UnoptimizedInMemoryShuttleRepository implements ShuttleGetterSetterRepository { private systems: ISystem[] = []; private stops: IStop[] = []; private routes: IRoute[] = []; diff --git a/src/resolvers/EtaResolvers.ts b/src/resolvers/EtaResolvers.ts index 96bd426..b5e7c65 100644 --- a/src/resolvers/EtaResolvers.ts +++ b/src/resolvers/EtaResolvers.ts @@ -4,10 +4,10 @@ import { ServerContext } from "../ServerContext"; export const EtaResolvers: Resolvers = { ETA: { stop: async (parent, args, contextValue, info) => { - return await contextValue.repository.getStopById(parent.stopId); + return await contextValue.shuttleRepository.getStopById(parent.stopId); }, shuttle: async (parent, args, contextValue, info) => { - return await contextValue.repository.getShuttleById(parent.shuttleId); + return await contextValue.shuttleRepository.getShuttleById(parent.shuttleId); }, }, -} \ No newline at end of file +} diff --git a/src/resolvers/MutationResolvers.ts b/src/resolvers/MutationResolvers.ts index ac8e1ea..9b48a7d 100644 --- a/src/resolvers/MutationResolvers.ts +++ b/src/resolvers/MutationResolvers.ts @@ -2,20 +2,20 @@ import { NotificationResponse, Resolvers } from "../generated/graphql"; import { ServerContext } from "../ServerContext"; import { ETANotificationScheduler, - NotificationSchedulingArguments } from "../notifications/schedulers/ETANotificationScheduler"; +import { ScheduledNotification } from "../repositories/NotificationRepository"; export const MutationResolvers: Resolvers = { Mutation: { scheduleNotification: async (_parent, args, context, _info) => { - const shuttle = await context.repository.getShuttleById(args.input.shuttleId); + const shuttle = await context.shuttleRepository.getShuttleById(args.input.shuttleId); if (!shuttle) { return { message: "Shuttle ID doesn't exist", success: false, } } - const stop = await context.repository.getStopById(args.input.stopId); + const stop = await context.shuttleRepository.getStopById(args.input.stopId); if (!stop) { return { message: "Stop ID doesn't exist", @@ -23,14 +23,14 @@ export const MutationResolvers: Resolvers = { } } - const notificationData: NotificationSchedulingArguments = { + const notificationData: ScheduledNotification = { ...args.input, secondsThreshold: typeof args.input.secondsThreshold === 'number' ? args.input.secondsThreshold : ETANotificationScheduler.defaultSecondsThresholdForNotificationToFire, } - await context.notificationService.scheduleNotification(notificationData); + await context.notificationRepository.addOrUpdateNotification(notificationData); const response: NotificationResponse = { message: "Notification scheduled", @@ -40,8 +40,9 @@ export const MutationResolvers: Resolvers = { return response; }, cancelNotification: async (_parent, args, context, _info) => { - if (context.notificationService.isNotificationScheduled(args.input)) { - await context.notificationService.cancelNotificationIfExists(args.input); + const isScheduled = await context.notificationRepository.isNotificationScheduled(args.input) + if (isScheduled) { + await context.notificationRepository.deleteNotificationIfExists(args.input); return { success: true, message: "Notification cancelled", diff --git a/src/resolvers/OrderedStopResolvers.ts b/src/resolvers/OrderedStopResolvers.ts index ad6c8e6..fe047aa 100644 --- a/src/resolvers/OrderedStopResolvers.ts +++ b/src/resolvers/OrderedStopResolvers.ts @@ -7,13 +7,13 @@ export const OrderedStopResolvers: Resolvers = { const routeId = parent.routeId; const stopId = parent.stopId; - const currentOrderedStop = await contextValue.repository.getOrderedStopByRouteAndStopId(routeId, stopId); + const currentOrderedStop = await contextValue.shuttleRepository.getOrderedStopByRouteAndStopId(routeId, stopId); if (!currentOrderedStop) return null; const nextOrderedStop = currentOrderedStop.nextStop; if (!nextOrderedStop) return null; - const nextOrderedStopObject = await contextValue.repository.getStopById(nextOrderedStop.stopId); + const nextOrderedStopObject = await contextValue.shuttleRepository.getStopById(nextOrderedStop.stopId); if (!nextOrderedStopObject) return null; return { @@ -26,13 +26,13 @@ export const OrderedStopResolvers: Resolvers = { const routeId = parent.routeId; const stopId = parent.stopId; - const currentOrderedStop = await contextValue.repository.getOrderedStopByRouteAndStopId(routeId, stopId); + const currentOrderedStop = await contextValue.shuttleRepository.getOrderedStopByRouteAndStopId(routeId, stopId); if (!currentOrderedStop) return null; const previousOrderedStop = currentOrderedStop.previousStop; if (!previousOrderedStop) return null; - const previousOrderedStopObject = await contextValue.repository.getStopById(previousOrderedStop.stopId); + const previousOrderedStopObject = await contextValue.shuttleRepository.getStopById(previousOrderedStop.stopId); if (!previousOrderedStopObject) return null; return { @@ -42,11 +42,11 @@ export const OrderedStopResolvers: Resolvers = { } }, stop: async (parent, args, contextValue, info) => { - return await contextValue.repository.getStopById(parent.stopId); + return await contextValue.shuttleRepository.getStopById(parent.stopId); }, route: async (parent, args, contextValue, info) => { - return await contextValue.repository.getRouteById(parent.routeId); + return await contextValue.shuttleRepository.getRouteById(parent.routeId); }, }, -} \ No newline at end of file +} diff --git a/src/resolvers/QueryResolvers.ts b/src/resolvers/QueryResolvers.ts index 725ae8d..06df1a9 100644 --- a/src/resolvers/QueryResolvers.ts +++ b/src/resolvers/QueryResolvers.ts @@ -4,11 +4,11 @@ import { Resolvers } from "../generated/graphql"; export const QueryResolvers: Resolvers = { Query: { systems: async (_parent, args, contextValue, _info) => { - return await contextValue.repository.getSystems(); + return await contextValue.shuttleRepository.getSystems(); }, system: async (_parent, args, contextValue, _info) => { if (!args.id) return null; - const system = await contextValue.repository.getSystemById(args.id); + const system = await contextValue.shuttleRepository.getSystemById(args.id); if (system === null) return null; return { @@ -18,11 +18,11 @@ export const QueryResolvers: Resolvers = { }, isNotificationScheduled: async (_parent, args, contextValue, _info) => { const notificationData = args.input; - return contextValue.notificationService.isNotificationScheduled(notificationData); + return await contextValue.notificationRepository.isNotificationScheduled(notificationData); }, secondsThresholdForNotification: async (_parent, args, contextValue, _info) => { const notificationData = args.input; - return contextValue.notificationService.getSecondsThresholdForScheduledNotification(notificationData); + return await contextValue.notificationRepository.getSecondsThresholdForNotificationIfExists(notificationData); }, }, } diff --git a/src/resolvers/RouteResolvers.ts b/src/resolvers/RouteResolvers.ts index d5a5810..a0adc01 100644 --- a/src/resolvers/RouteResolvers.ts +++ b/src/resolvers/RouteResolvers.ts @@ -4,7 +4,7 @@ import { ServerContext } from "../ServerContext"; export const RouteResolvers: Resolvers = { Route: { shuttles: async (parent, args, contextValue, info) => { - const shuttles = await contextValue.repository.getShuttlesByRouteId(parent.id); + const shuttles = await contextValue.shuttleRepository.getShuttlesByRouteId(parent.id); return shuttles.map(({ coordinates, @@ -22,10 +22,10 @@ export const RouteResolvers: Resolvers = { }, orderedStop: async (parent, args, contextValue, info) => { if (!args.forStopId) return null; - const orderedStop = await contextValue.repository.getOrderedStopByRouteAndStopId(parent.id, args.forStopId); + const orderedStop = await contextValue.shuttleRepository.getOrderedStopByRouteAndStopId(parent.id, args.forStopId); if (!orderedStop) return null; - const stop = await contextValue.repository.getStopById(orderedStop.stopId); + const stop = await contextValue.shuttleRepository.getStopById(orderedStop.stopId); if (!stop) return null; return { diff --git a/src/resolvers/ShuttleResolvers.ts b/src/resolvers/ShuttleResolvers.ts index 2350987..b264306 100644 --- a/src/resolvers/ShuttleResolvers.ts +++ b/src/resolvers/ShuttleResolvers.ts @@ -5,7 +5,7 @@ export const ShuttleResolvers: Resolvers = { Shuttle: { eta: async (parent, args, contextValue, info) => { if (!args.forStopId) return null; - const etaForStopId = await contextValue.repository.getEtaForShuttleAndStopId(parent.id, args.forStopId); + const etaForStopId = await contextValue.shuttleRepository.getEtaForShuttleAndStopId(parent.id, args.forStopId); if (etaForStopId === null) return null; return { @@ -16,7 +16,7 @@ export const ShuttleResolvers: Resolvers = { }; }, etas: async (parent, args, contextValue, info) => { - const etasForShuttle = await contextValue.repository.getEtasForShuttleId(parent.id); + const etasForShuttle = await contextValue.shuttleRepository.getEtasForShuttleId(parent.id); if (!etasForShuttle) return null; const computedEtas = await Promise.all(etasForShuttle.map(async ({ @@ -38,7 +38,7 @@ export const ShuttleResolvers: Resolvers = { return []; }, route: async (parent, args, contextValue, info) => { - const route = await contextValue.repository.getRouteById(parent.routeId); + const route = await contextValue.shuttleRepository.getRouteById(parent.routeId); if (route === null) return null; return { @@ -49,4 +49,4 @@ export const ShuttleResolvers: Resolvers = { } } }, -} \ No newline at end of file +} diff --git a/src/resolvers/StopResolvers.ts b/src/resolvers/StopResolvers.ts index 6ccbdf1..860e3be 100644 --- a/src/resolvers/StopResolvers.ts +++ b/src/resolvers/StopResolvers.ts @@ -4,10 +4,10 @@ import { ServerContext } from "../ServerContext"; export const StopResolvers: Resolvers = { Stop: { orderedStops: async (parent, args, contextValue, info) => { - return await contextValue.repository.getOrderedStopsByStopId(parent.id); + return await contextValue.shuttleRepository.getOrderedStopsByStopId(parent.id); }, etas: async (parent, args, contextValue, info) => { - return await contextValue.repository.getEtasForStopId(parent.id); + return await contextValue.shuttleRepository.getEtasForStopId(parent.id); }, }, -} \ No newline at end of file +} diff --git a/src/resolvers/SystemResolvers.ts b/src/resolvers/SystemResolvers.ts index dbb98ca..ae9903a 100644 --- a/src/resolvers/SystemResolvers.ts +++ b/src/resolvers/SystemResolvers.ts @@ -4,14 +4,14 @@ import { ServerContext } from "../ServerContext"; export const SystemResolvers: Resolvers = { System: { routes: async (parent, args, contextValue, info) => { - return await contextValue.repository.getRoutesBySystemId(parent.id); + return await contextValue.shuttleRepository.getRoutesBySystemId(parent.id); }, stops: async (parent, args, contextValue, info) => { - return await contextValue.repository.getStopsBySystemId(parent.id); + return await contextValue.shuttleRepository.getStopsBySystemId(parent.id); }, stop: async (parent, args, contextValue, info) => { if (!args.id) return null; - const stop = await contextValue.repository.getStopById(args.id); + const stop = await contextValue.shuttleRepository.getStopById(args.id); if (stop === null) return null; if (stop.systemId !== parent.id) return null; @@ -24,7 +24,7 @@ export const SystemResolvers: Resolvers = { }, route: async (parent, args, contextValue, info) => { if (!args.id) return null; - const route = await contextValue.repository.getRouteById(args.id); + const route = await contextValue.shuttleRepository.getRouteById(args.id); if (route === null) return null; if (route.systemId !== parent.id) return null; @@ -38,7 +38,7 @@ export const SystemResolvers: Resolvers = { }, shuttle: async (parent, args, contextValue, info) => { if (!args.id) return null; - const shuttle = await contextValue.repository.getShuttleById(args.id); + const shuttle = await contextValue.shuttleRepository.getShuttleById(args.id); if (shuttle === null) return null; if (shuttle.systemId !== parent.id) return null; @@ -46,7 +46,7 @@ export const SystemResolvers: Resolvers = { return shuttle; }, shuttles: async (parent, args, contextValue, info) => { - return await contextValue.repository.getShuttlesBySystemId(parent.id); + return await contextValue.shuttleRepository.getShuttlesBySystemId(parent.id); } }, -} \ No newline at end of file +} diff --git a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts b/test/loaders/ApiBasedShuttleRepositoryLoaderTests.test.ts similarity index 96% rename from test/loaders/ApiBasedRepositoryLoaderTests.test.ts rename to test/loaders/ApiBasedShuttleRepositoryLoaderTests.test.ts index 87cb896..cead5ec 100644 --- a/test/loaders/ApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/ApiBasedShuttleRepositoryLoaderTests.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, jest, test } from "@jest/globals"; -import { ApiBasedRepositoryLoader, ApiResponseError } from "../../src/loaders/ApiBasedRepositoryLoader"; -import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { ApiBasedShuttleRepositoryLoader, ApiResponseError } from "../../src/loaders/ApiBasedShuttleRepositoryLoader"; +import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import { fetchSystemDataSuccessfulResponse } from "../jsonSnapshots/fetchSystemData/fetchSystemDataSuccessfulResponse"; import { fetchSystemDataFailedResponse } from "../jsonSnapshots/fetchSystemData/fetchSystemDataFailedResponse"; import { fetchRouteDataSuccessfulResponse } from "../jsonSnapshots/fetchRouteData/fetchRouteDataSuccessfulResponse"; @@ -23,10 +23,10 @@ async function assertAsyncCallbackThrowsApiResponseError(callback: () => Promise } describe("ApiBasedRepositoryLoader", () => { - let loader: ApiBasedRepositoryLoader; + let loader: ApiBasedShuttleRepositoryLoader; beforeEach(() => { - loader = new ApiBasedRepositoryLoader(new UnoptimizedInMemoryRepository()); + loader = new ApiBasedShuttleRepositoryLoader(new UnoptimizedInMemoryShuttleRepository()); resetGlobalFetchMockJson(); }); diff --git a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts b/test/loaders/TimedApiBasedShuttleRepositoryLoaderTests.test.ts similarity index 84% rename from test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts rename to test/loaders/TimedApiBasedShuttleRepositoryLoaderTests.test.ts index 9979a06..3c81887 100644 --- a/test/loaders/TimedApiBasedRepositoryLoaderTests.test.ts +++ b/test/loaders/TimedApiBasedShuttleRepositoryLoaderTests.test.ts @@ -1,10 +1,10 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; -import { TimedApiBasedRepositoryLoader } from "../../src/loaders/TimedApiBasedRepositoryLoader"; +import { TimedApiBasedShuttleRepositoryLoader } from "../../src/loaders/TimedApiBasedShuttleRepositoryLoader"; import { resetGlobalFetchMockJson } from "../testHelpers/fetchMockHelpers"; -import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; describe("TimedApiBasedRepositoryLoader", () => { - let loader: TimedApiBasedRepositoryLoader; + let loader: TimedApiBasedShuttleRepositoryLoader; let spies: any; beforeAll(() => { @@ -15,7 +15,7 @@ describe("TimedApiBasedRepositoryLoader", () => { beforeEach(() => { resetGlobalFetchMockJson(); - loader = new TimedApiBasedRepositoryLoader(new UnoptimizedInMemoryRepository()); + loader = new TimedApiBasedShuttleRepositoryLoader(new UnoptimizedInMemoryShuttleRepository()); spies = { fetchAndUpdateSystemData: jest.spyOn(loader, 'fetchAndUpdateSystemData'), @@ -64,4 +64,4 @@ describe("TimedApiBasedRepositoryLoader", () => { expect(loader['shouldBeRunning']).toBe(false); }); }); -}); \ No newline at end of file +}); diff --git a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts index df528b9..7d9392f 100644 --- a/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts +++ b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts @@ -1,10 +1,11 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { ETANotificationScheduler } from "../../../src/notifications/schedulers/ETANotificationScheduler"; -import { UnoptimizedInMemoryRepository } from "../../../src/repositories/UnoptimizedInMemoryRepository"; -import http2 from "http2"; +import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import { IEta, IShuttle, IStop } from "../../../src/entities/entities"; import { addMockShuttleToRepository, addMockStopToRepository } from "../../testHelpers/repositorySetupHelpers"; import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender"; +import { InMemoryNotificationRepository } from "../../../src/repositories/InMemoryNotificationRepository"; +import { NotificationRepository } from "../../../src/repositories/NotificationRepository"; jest.mock("http2"); jest.mock("../../../src/notifications/senders/AppleNotificationSender"); @@ -15,23 +16,6 @@ function mockNotificationSenderMethods(shouldSimulateNotificationSend: boolean) MockAppleNotificationSender.prototype.sendNotificationImmediately = jest.fn(async () => shouldSimulateNotificationSend); } -/** - * Wait for a condition to become true until the timeout - * is hit. - * @param condition - * @param timeoutMilliseconds - * @param intervalMilliseconds - */ -async function waitForCondition(condition: () => boolean, timeoutMilliseconds = 5000, intervalMilliseconds = 500) { - const startTime = Date.now(); - while (!condition()) { - if (Date.now() - startTime > timeoutMilliseconds) { - throw new Error("Timeout waiting for condition"); - } - await new Promise((resolve) => setTimeout(resolve, intervalMilliseconds)); - } -} - /** * Wait for a specified number of milliseconds. * @param ms @@ -42,16 +26,23 @@ async function waitForMilliseconds(ms: number): Promise { describe("ETANotificationScheduler", () => { - let repository: UnoptimizedInMemoryRepository + let shuttleRepository: UnoptimizedInMemoryShuttleRepository let notificationService: ETANotificationScheduler; + let notificationRepository: NotificationRepository; beforeEach(() => { - repository = new UnoptimizedInMemoryRepository(); + shuttleRepository = new UnoptimizedInMemoryShuttleRepository(); + notificationRepository = new InMemoryNotificationRepository(); mockNotificationSenderMethods(true); const appleNotificationSender = new MockAppleNotificationSender(false); - notificationService = new ETANotificationScheduler(repository, appleNotificationSender); + notificationService = new ETANotificationScheduler( + shuttleRepository, + notificationRepository, + appleNotificationSender + ); + notificationService.startListeningForUpdates(); }); function generateNotificationDataAndEta(shuttle: IShuttle, stop: IStop) { @@ -75,41 +66,26 @@ describe("ETANotificationScheduler", () => { return { eta, notificationData1, notificationData2 }; } - describe("scheduleNotification", () => { - it("schedules the notification", async () => { - // arrange - const notificationData = { - deviceId: "1", - shuttleId: "1", - stopId: "1", - secondsThreshold: 120, - }; - - await notificationService.scheduleNotification(notificationData); - - const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData); - expect(isNotificationScheduled).toEqual(true); - }); - + describe("etaSubscriberCallback", () => { it("sends and clears correct notification after ETA changed", async () => { // Arrange - const shuttle = await addMockShuttleToRepository(repository, "1"); - const stop = await addMockStopToRepository(repository, "1"); + const shuttle = await addMockShuttleToRepository(shuttleRepository, "1"); + const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1, notificationData2 } = generateNotificationDataAndEta(shuttle, stop); // Act - await notificationService.scheduleNotification(notificationData1); - await notificationService.scheduleNotification(notificationData2); - await repository.addOrUpdateEta(eta); + await notificationRepository.addOrUpdateNotification(notificationData1); + await notificationRepository.addOrUpdateNotification(notificationData2); + await shuttleRepository.addOrUpdateEta(eta); // Assert - // Because repository publisher calls subscriber without await - // wait for the change to occur first - await waitForCondition(() => !notificationService.isNotificationScheduled(notificationData1)); + // Wait for the callback to actually be called + await waitForMilliseconds(1000); + + const isFirstNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData1); + const isSecondNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData2); - const isFirstNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); - const isSecondNotificationScheduled = notificationService.isNotificationScheduled(notificationData2); // No longer scheduled after being sent expect(isFirstNotificationScheduled).toBe(false); expect(isSecondNotificationScheduled).toBe(false); @@ -117,97 +93,53 @@ describe("ETANotificationScheduler", () => { it("doesn't send notification if seconds threshold not exceeded", async () => { // Arrange - const shuttle = await addMockShuttleToRepository(repository, "1"); - const stop = await addMockStopToRepository(repository, "1"); + const shuttle = await addMockShuttleToRepository(shuttleRepository, "1"); + const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop); notificationData1.secondsThreshold = eta.secondsRemaining - 10; // Act - await notificationService.scheduleNotification(notificationData1); - await repository.addOrUpdateEta(eta); + await notificationRepository.addOrUpdateNotification(notificationData1); + await shuttleRepository.addOrUpdateEta(eta); // Assert await waitForMilliseconds(500); - const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); + const isNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData1); expect(isNotificationScheduled).toBe(true); }); it("leaves notification in array if delivery unsuccessful", async () => { // Arrange - const shuttle = await addMockShuttleToRepository(repository, "1"); - const stop = await addMockStopToRepository(repository, "1"); + const shuttle = await addMockShuttleToRepository(shuttleRepository, "1"); + const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop) + // replace the old notification scheduler with a new one + // detach the old callback method from the shuttle repo + notificationService.stopListeningForUpdates(); + + // replace the notification repository with a fresh one too + const notificationRepository = new InMemoryNotificationRepository(); + mockNotificationSenderMethods(false); + const updatedNotificationSender = new MockAppleNotificationSender(false); notificationService = new ETANotificationScheduler( - repository, - new MockAppleNotificationSender(), - ) + shuttleRepository, + notificationRepository, + updatedNotificationSender + ); + notificationService.startListeningForUpdates(); // Act - await notificationService.scheduleNotification(notificationData1); - await repository.addOrUpdateEta(eta); + await notificationRepository.addOrUpdateNotification(notificationData1); + await shuttleRepository.addOrUpdateEta(eta); // Assert // The notification should stay scheduled to be retried once // the ETA updates again await waitForMilliseconds(500); - const isNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); + const isNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData1); expect(isNotificationScheduled).toBe(true); }); }); - - - describe("cancelNotification", () => { - it("stops notification from sending to given shuttle/stop ID", async () => { - // Arrange - const shuttle = await addMockShuttleToRepository(repository, "1"); - const stop = await addMockStopToRepository(repository, "1"); - const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop); - - await notificationService.scheduleNotification(notificationData1); - - // Act - await notificationService.cancelNotificationIfExists(notificationData1); - await repository.addOrUpdateEta(eta); - - // Assert - await waitForMilliseconds(500); - expect(http2.connect as jest.Mock).toHaveBeenCalledTimes(0); - }); - }); - - describe("getAllScheduledNotificationsForDevice", () => { - it("returns scheduled notifications for the device ID", async () => { - // Arrange - const shuttle1 = await addMockShuttleToRepository(repository, "1"); - const stop = await addMockStopToRepository(repository, "1"); - const { notificationData1 } = generateNotificationDataAndEta(shuttle1, stop); - await notificationService.scheduleNotification(notificationData1); - - const shuttle2 = { - ...shuttle1, - id: "2", - } - await repository.addOrUpdateShuttle(shuttle2); - - const notificationData2 = { - ...notificationData1, - shuttleId: shuttle2.id, - } - await notificationService.scheduleNotification(notificationData2); - - // Act - const notifications = await notificationService.getAllScheduledNotificationsForDevice(notificationData1.deviceId); - - // Assert - expect(notifications.length).toBe(2); - }); - - it("returns an empty array if there are no notifications", async () => { - // Act - const notifications = await notificationService.getAllScheduledNotificationsForDevice("1"); - expect(notifications.length).toBe(0); - }); - }); }); diff --git a/test/repositories/InMemoryNotificationRepositoryTests.test.ts b/test/repositories/InMemoryNotificationRepositoryTests.test.ts new file mode 100644 index 0000000..3419d96 --- /dev/null +++ b/test/repositories/InMemoryNotificationRepositoryTests.test.ts @@ -0,0 +1,144 @@ +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { InMemoryNotificationRepository } from "../../src/repositories/InMemoryNotificationRepository"; +import { NotificationEvent } from "../../src/repositories/NotificationRepository"; + +describe("InMemoryNotificationRepository", () => { + let repo: InMemoryNotificationRepository; + + beforeEach(() => { + repo = new InMemoryNotificationRepository(); + }) + + const notification = { + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1", + secondsThreshold: 180 + }; + + describe("getAllNotificationsForShuttleAndStopId", () => { + it("gets notifications correctly", async () => { + await repo.addOrUpdateNotification(notification); + + const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(notification); + }); + + it("returns empty array if no notifications", async () => { + const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); + expect(result).toEqual([]); + }); + }); + + describe("getSecondsThresholdForNotificationIfExists", () => { + it("gets the seconds threshold if exists", async () => { + await repo.addOrUpdateNotification(notification); + + const result = await repo.getSecondsThresholdForNotificationIfExists({ + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1" + }); + expect(result).toBe(180); + }); + + it("returns null if there is no seconds threshold", async () => { + const result = await repo.getSecondsThresholdForNotificationIfExists({ + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1" + }); + expect(result).toBeNull(); + }); + }); + + describe("addOrUpdateNotification", () => { + // Add/get flow is covered in getAllNotificationsForShuttleAndStopId + + it("updates the seconds threshold if the notification exists already", async () => { + await repo.addOrUpdateNotification(notification); + await repo.addOrUpdateNotification({...notification, secondsThreshold: 300}); + + const result = await repo.getSecondsThresholdForNotificationIfExists({ + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1" + }); + expect(result).toBe(300); + }); + }); + + describe("deleteNotificationIfExists", () => { + it("deletes the notification", async () => { + await repo.addOrUpdateNotification(notification); + await repo.deleteNotificationIfExists(notification); + + const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); + expect(result).toHaveLength(0); + }); + + + it("does nothing if there's no notification", async () => { + await expect(repo.deleteNotificationIfExists({ + deviceId: "device1", + shuttleId: "shuttle1", + stopId: "stop1" + })).resolves.not.toThrow(); + }); + }); + + describe("subscribeToNotificationChanges", () => { + it("calls subscribers when something is added", async () => { + const mockCallback = jest.fn(); + repo.subscribeToNotificationChanges(mockCallback); + + await repo.addOrUpdateNotification(notification); + + const expectedEvent: NotificationEvent = { + event: 'addOrUpdate', + notification, + } + expect(mockCallback).toHaveBeenCalledTimes(1); + expect(mockCallback).toHaveBeenCalledWith(expectedEvent); + }); + + it("calls subscribers when something is updated", async () => { + const mockCallback = jest.fn(); + repo.subscribeToNotificationChanges(mockCallback); + + await repo.addOrUpdateNotification(notification); + + const updatedNotification = { + ...notification, + secondsThreshold: notification.secondsThreshold + 60, + }; + + await repo.addOrUpdateNotification(updatedNotification); + + const expectedEvent: NotificationEvent = { + event: 'addOrUpdate', + notification, + } + expect(mockCallback).toHaveBeenCalledTimes(2); + expect(mockCallback).toHaveBeenCalledWith(expectedEvent); + }); + + it("calls subscribers when something is deleted", async () => { + await repo.addOrUpdateNotification(notification); + + const mockCallback = jest.fn(); + repo.subscribeToNotificationChanges(mockCallback); + + await repo.deleteNotificationIfExists(notification); + + expect(mockCallback).toHaveBeenCalledTimes(1); + + const expectedEvent: NotificationEvent = { + event: 'delete', + notification, + }; + expect(mockCallback).toHaveBeenCalledWith(expectedEvent); + }); + }) +}); diff --git a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts b/test/repositories/UnoptimizedInMemoryShuttleRepositoryTests.test.ts similarity index 98% rename from test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts rename to test/repositories/UnoptimizedInMemoryShuttleRepositoryTests.test.ts index 35956b3..003f71f 100644 --- a/test/repositories/UnoptimizedInMemoryRepositoryTests.test.ts +++ b/test/repositories/UnoptimizedInMemoryShuttleRepositoryTests.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, jest, test } from "@jest/globals"; -import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import { generateMockEtas, generateMockOrderedStops, @@ -11,14 +11,14 @@ import { // For repositories created in the future, reuse core testing // logic from here and differentiate setup (e.g. creating mocks) -// Do this by creating a function which takes a GetterRepository -// or GetterSetterRepository instance +// Do this by creating a function which takes a ShuttleGetterRepository +// or ShuttleGetterSetterRepository instance describe("UnoptimizedInMemoryRepository", () => { - let repository: UnoptimizedInMemoryRepository; + let repository: UnoptimizedInMemoryShuttleRepository; beforeEach(() => { - repository = new UnoptimizedInMemoryRepository(); + repository = new UnoptimizedInMemoryShuttleRepository(); }); describe("getSystems", () => { diff --git a/test/resolvers/EtaResolverTests.test.ts b/test/resolvers/EtaResolverTests.test.ts index 98c2e52..92a5600 100644 --- a/test/resolvers/EtaResolverTests.test.ts +++ b/test/resolvers/EtaResolverTests.test.ts @@ -18,10 +18,10 @@ describe("EtaResolvers", () => { let expectedEta: IEta; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); - mockShuttle = await addMockShuttleToRepository(context.repository, mockSystem.id); - mockStop = await addMockStopToRepository(context.repository, mockSystem.id); - expectedEta = await addMockEtaToRepository(context.repository, mockStop.id, mockShuttle.id); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); + mockShuttle = await addMockShuttleToRepository(context.shuttleRepository, mockSystem.id); + mockStop = await addMockStopToRepository(context.shuttleRepository, mockSystem.id); + expectedEta = await addMockEtaToRepository(context.shuttleRepository, mockStop.id, mockShuttle.id); }); async function getResponseForEtaQuery(query: string) { @@ -32,9 +32,8 @@ describe("EtaResolvers", () => { shuttleId: mockShuttle.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); return response; } diff --git a/test/resolvers/MutationResolverTests.test.ts b/test/resolvers/MutationResolverTests.test.ts index 73eaae5..055195e 100644 --- a/test/resolvers/MutationResolverTests.test.ts +++ b/test/resolvers/MutationResolverTests.test.ts @@ -38,20 +38,20 @@ describe("MutationResolvers", () => { } ` - function assertFailedResponse(response: any, notificationInput: NotificationInput) { + async function assertFailedResponse(response: any, notificationInput: NotificationInput) { assert(response.body.kind === "single"); expect(response.body.singleResult.errors).toBeUndefined(); const notificationResponse = response.body.singleResult.data?.scheduleNotification as any; expect(notificationResponse.success).toBe(false); - expect(context.notificationService.isNotificationScheduled(notificationInput)).toBe(false); + expect(await context.notificationRepository.isNotificationScheduled(notificationInput)).toBe(false); } it("adds a notification to the notification service", async () => { - const system = await addMockSystemToRepository(context.repository); - const shuttle = await addMockShuttleToRepository(context.repository, system.id); - const stop = await addMockStopToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, system.id); + const stop = await addMockStopToRepository(context.shuttleRepository, system.id); const notificationInput = { deviceId: "1", @@ -72,13 +72,13 @@ describe("MutationResolvers", () => { expect(notificationResponse?.success).toBe(true); expect(notificationResponse?.data).toEqual(expectedNotificationData); - expect(context.notificationService.getSecondsThresholdForScheduledNotification(expectedNotificationData)).toBe(240); + expect(await context.notificationRepository.getSecondsThresholdForNotificationIfExists(expectedNotificationData)).toBe(240); }); it("adds a notification with the default seconds threshold if none is provided", async () => { - const system = await addMockSystemToRepository(context.repository); - const shuttle = await addMockShuttleToRepository(context.repository, system.id); - const stop = await addMockStopToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, system.id); + const stop = await addMockStopToRepository(context.shuttleRepository, system.id); const notificationInput = { deviceId: "1", @@ -93,12 +93,12 @@ describe("MutationResolvers", () => { const notificationResponse = response.body.singleResult.data?.scheduleNotification as any; expect(notificationResponse?.success).toBe(true); - expect(context.notificationService.getSecondsThresholdForScheduledNotification(notificationInput)).toBe(180); + expect(await context.notificationRepository.getSecondsThresholdForNotificationIfExists(notificationInput)).toBe(180); }); it("fails if the shuttle ID doesn't exist", async () => { - const system = await addMockSystemToRepository(context.repository); - const stop = await addMockStopToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const stop = await addMockStopToRepository(context.shuttleRepository, system.id); const notificationInput = { deviceId: "1", @@ -106,12 +106,12 @@ describe("MutationResolvers", () => { stopId: stop.id, } const response = await getServerResponse(query, notificationInput); - assertFailedResponse(response, notificationInput); + await assertFailedResponse(response, notificationInput); }); it("fails if the stop ID doesn't exist", async () => { - const system = await addMockSystemToRepository(context.repository); - const shuttle = await addMockShuttleToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, system.id); const notificationInput = { deviceId: "1", @@ -120,7 +120,7 @@ describe("MutationResolvers", () => { } const response = await getServerResponse(query, notificationInput); - assertFailedResponse(response, notificationInput); + await assertFailedResponse(response, notificationInput); }); }); @@ -140,9 +140,9 @@ describe("MutationResolvers", () => { ` it("removes the notification from the notification service", async () => { - const system = await addMockSystemToRepository(context.repository); - const shuttle = await addMockShuttleToRepository(context.repository, system.id); - const stop = await addMockStopToRepository(context.repository, system.id); + const system = await addMockSystemToRepository(context.shuttleRepository); + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, system.id); + const stop = await addMockStopToRepository(context.shuttleRepository, system.id); const notificationInput: any = { deviceId: "1", @@ -150,7 +150,7 @@ describe("MutationResolvers", () => { stopId: stop.id, secondsThreshold: 180, } - await context.notificationService.scheduleNotification(notificationInput); + await context.notificationRepository.addOrUpdateNotification(notificationInput); const notificationLookup = { ...notificationInput @@ -166,7 +166,7 @@ describe("MutationResolvers", () => { expect(notificationResponse.success).toBe(true); expect(notificationResponse.data).toEqual(notificationLookup); - expect(context.notificationService.isNotificationScheduled(notificationLookup)).toBe(false); + expect(await context.notificationRepository.isNotificationScheduled(notificationLookup)).toBe(false); }); it("fails if the notification doesn't exist", async () => { diff --git a/test/resolvers/OrderedStopResolverTests.test.ts b/test/resolvers/OrderedStopResolverTests.test.ts index 0d047ac..4813e2d 100644 --- a/test/resolvers/OrderedStopResolverTests.test.ts +++ b/test/resolvers/OrderedStopResolverTests.test.ts @@ -14,13 +14,13 @@ describe("OrderedStopResolvers", () => { let mockStops: IStop[]; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); - mockRoute = await addMockRouteToRepository(context.repository, mockSystem.id); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); + mockRoute = await addMockRouteToRepository(context.shuttleRepository, mockSystem.id); mockStops = generateMockStops(); await Promise.all(mockStops.map(async (mockStop) => { mockStop.systemId = mockSystem.id; - await context.repository.addOrUpdateStop(mockStop); + await context.shuttleRepository.addOrUpdateStop(mockStop); })); }); @@ -38,8 +38,8 @@ describe("OrderedStopResolvers", () => { // Link the stops together orderedStops[0].nextStop = orderedStops[1]; orderedStops[1].previousStop = orderedStops[0]; - await context.repository.addOrUpdateOrderedStop(orderedStops[0]); - await context.repository.addOrUpdateOrderedStop(orderedStops[1]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[0]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[1]); return orderedStops; } @@ -68,9 +68,8 @@ describe("OrderedStopResolvers", () => { stopId, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -94,7 +93,7 @@ describe("OrderedStopResolvers", () => { it("returns null if there is no next stop in the repository", async () => { const orderedStops = await setUpOrderedStopsInRepository(); orderedStops[0].nextStop = undefined; - await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[0]); const response = await getResponseForNextStopQuery(orderedStops[0].stopId); @@ -105,7 +104,7 @@ describe("OrderedStopResolvers", () => { it("returns null if the next stop object no longer exists", async () => { const orderedStops = await setUpOrderedStopsInRepository(); - await context.repository.removeStopIfExists(orderedStops[1].stopId); + await context.shuttleRepository.removeStopIfExists(orderedStops[1].stopId); const response = await getResponseForNextStopQuery(orderedStops[0].stopId); @@ -140,9 +139,8 @@ describe("OrderedStopResolvers", () => { stopId, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -165,7 +163,7 @@ describe("OrderedStopResolvers", () => { it("returns null if there is no previous stop in the repository", async () => { const orderedStops = await setUpOrderedStopsInRepository(); orderedStops[1].previousStop = undefined; - await context.repository.addOrUpdateOrderedStop(orderedStops[1]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[1]); const response = await getResponseForPreviousStopQuery(orderedStops[1].stopId); @@ -176,7 +174,7 @@ describe("OrderedStopResolvers", () => { it("returns null if the current stop no longer exists", async () => { const orderedStops = await setUpOrderedStopsInRepository(); - await context.repository.removeStopIfExists(orderedStops[0].stopId); + await context.shuttleRepository.removeStopIfExists(orderedStops[0].stopId); const response = await getResponseForPreviousStopQuery(orderedStops[1].stopId); @@ -214,9 +212,8 @@ describe("OrderedStopResolvers", () => { stopId, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -226,7 +223,7 @@ describe("OrderedStopResolvers", () => { orderedStops[0].stopId = mockStops[0].id; // Add one stop only - await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[0]); const response = await getResponseForRouteQuery(orderedStops[1].stopId); @@ -265,16 +262,15 @@ describe("OrderedStopResolvers", () => { stopId, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } it("returns the associated stop if it exists", async () => { const orderedStops = await setUpOrderedStopsInRepository(); orderedStops[0].stopId = mockStops[0].id; - await context.repository.addOrUpdateOrderedStop(orderedStops[0]); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStops[0]); const response = await getResponseForStopQuery(orderedStops[0].stopId); diff --git a/test/resolvers/QueryResolverTests.test.ts b/test/resolvers/QueryResolverTests.test.ts index 00007a7..52d6605 100644 --- a/test/resolvers/QueryResolverTests.test.ts +++ b/test/resolvers/QueryResolverTests.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it } from "@jest/globals"; import { generateMockSystems } from "../testHelpers/mockDataGenerators"; import { setupTestServerContext, setupTestServerHolder } from "../testHelpers/apolloTestServerHelpers"; import assert = require("node:assert"); -import { NotificationSchedulingArguments } from "../../src/notifications/schedulers/ETANotificationScheduler"; import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers"; +import { ScheduledNotification } from "../../src/repositories/NotificationRepository"; // See Apollo documentation for integration test guide // https://www.apollographql.com/docs/apollo-server/testing/testing @@ -15,7 +15,7 @@ describe("QueryResolvers", () => { async function addMockSystems() { const systems = generateMockSystems(); await Promise.all(systems.map(async (system) => { - await context.repository.addOrUpdateSystem(system); + await context.shuttleRepository.addOrUpdateSystem(system); })); return systems; } @@ -36,9 +36,8 @@ describe("QueryResolvers", () => { const response = await holder.testServer.executeOperation({ query, }, { - contextValue: { - repository: context.repository, - }, + contextValue: context + }); assert(response.body.kind === "single"); @@ -68,9 +67,8 @@ describe("QueryResolvers", () => { id: systemToGet.id, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); assert(response.body.kind === "single"); @@ -85,9 +83,8 @@ describe("QueryResolvers", () => { id: "nonexistent-id", } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); assert(response.body.kind === "single"); @@ -106,16 +103,16 @@ describe("QueryResolvers", () => { it("returns correct data if the notification is scheduled", async () => { // Arrange - const shuttle = await addMockShuttleToRepository(context.repository, "1"); - const stop = await addMockStopToRepository(context.repository, "1") + const shuttle = await addMockShuttleToRepository(context.shuttleRepository, "1"); + const stop = await addMockStopToRepository(context.shuttleRepository, "1") - const notification: NotificationSchedulingArguments = { + const notification: ScheduledNotification = { shuttleId: shuttle.id, stopId: stop.id, deviceId: "1", secondsThreshold: 240, }; - await context.notificationService.scheduleNotification(notification); + await context.notificationRepository.addOrUpdateNotification(notification); const notificationLookup: any = { ...notification, diff --git a/test/resolvers/RouteResolverTests.test.ts b/test/resolvers/RouteResolverTests.test.ts index 0279a6e..909c1d8 100644 --- a/test/resolvers/RouteResolverTests.test.ts +++ b/test/resolvers/RouteResolverTests.test.ts @@ -18,11 +18,11 @@ describe("RouteResolvers", () => { let mockStop: IStop; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); const systemId = mockSystem.id; - mockRoute = await addMockRouteToRepository(context.repository, systemId); - mockStop = await addMockStopToRepository(context.repository, systemId); + mockRoute = await addMockRouteToRepository(context.shuttleRepository, systemId); + mockStop = await addMockStopToRepository(context.shuttleRepository, systemId); }); @@ -48,9 +48,8 @@ describe("RouteResolvers", () => { routeId: mockRoute.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -59,7 +58,7 @@ describe("RouteResolvers", () => { const expectedShuttle = expectedShuttles[0]; expectedShuttle.systemId = mockSystem.id; expectedShuttle.routeId = mockRoute.id; - await context.repository.addOrUpdateShuttle(expectedShuttle); + await context.shuttleRepository.addOrUpdateShuttle(expectedShuttle); const response = await getResponseForShuttlesQuery(); @@ -104,9 +103,8 @@ describe("RouteResolvers", () => { stopId: mockStop.id, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } @@ -115,7 +113,7 @@ describe("RouteResolvers", () => { const expectedOrderedStop = orderedStops[0]; expectedOrderedStop.stopId = mockStop.id; expectedOrderedStop.routeId = mockRoute.id; - await context.repository.addOrUpdateOrderedStop(expectedOrderedStop); + await context.shuttleRepository.addOrUpdateOrderedStop(expectedOrderedStop); const response = await getResponseForOrderedStopQuery(); @@ -132,9 +130,9 @@ describe("RouteResolvers", () => { const expectedOrderedStop = orderedStops[0]; expectedOrderedStop.stopId = mockStop.id; expectedOrderedStop.routeId = mockRoute.id; - await context.repository.addOrUpdateOrderedStop(expectedOrderedStop); + await context.shuttleRepository.addOrUpdateOrderedStop(expectedOrderedStop); - await context.repository.removeStopIfExists(mockStop.id); + await context.shuttleRepository.removeStopIfExists(mockStop.id); const response = await getResponseForOrderedStopQuery(); diff --git a/test/resolvers/ShuttleResolverTests.test.ts b/test/resolvers/ShuttleResolverTests.test.ts index b2b3e81..800f34c 100644 --- a/test/resolvers/ShuttleResolverTests.test.ts +++ b/test/resolvers/ShuttleResolverTests.test.ts @@ -14,8 +14,8 @@ describe("ShuttleResolvers", () => { let mockShuttle: IShuttle; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); - mockShuttle = await addMockShuttleToRepository(context.repository, + mockSystem = await addMockSystemToRepository(context.shuttleRepository); + mockShuttle = await addMockShuttleToRepository(context.shuttleRepository, mockSystem.id); }); @@ -24,7 +24,7 @@ describe("ShuttleResolvers", () => { const etas = generateMockEtas(); await Promise.all(etas.map(async (eta) => { eta.shuttleId = shuttleId; - await context.repository.addOrUpdateEta(eta); + await context.shuttleRepository.addOrUpdateEta(eta); })); return etas; } @@ -56,9 +56,8 @@ describe("ShuttleResolvers", () => { stopId: mockEta.stopId, }, }, { - contextValue: { - repository: context.repository, - }, + contextValue: context + }); // Assert @@ -77,9 +76,8 @@ describe("ShuttleResolvers", () => { stopId: "nonexistent-stop", } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); // Assert @@ -114,9 +112,8 @@ describe("ShuttleResolvers", () => { shuttleId: mockShuttle.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); assert(response.body.kind === "single"); @@ -133,9 +130,8 @@ describe("ShuttleResolvers", () => { shuttleId: mockShuttle.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); assert(response.body.kind === "single"); @@ -172,15 +168,14 @@ describe("ShuttleResolvers", () => { shuttleId: mockShuttle.id, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } it("returns the route if it exists", async () => { const mockRoute = generateMockRoutes()[0]; - await context.repository.addOrUpdateRoute(mockRoute); + await context.shuttleRepository.addOrUpdateRoute(mockRoute); const response = await getResponseForQuery(); diff --git a/test/resolvers/StopResolverTests.test.ts b/test/resolvers/StopResolverTests.test.ts index fdda62b..d82ca78 100644 --- a/test/resolvers/StopResolverTests.test.ts +++ b/test/resolvers/StopResolverTests.test.ts @@ -13,8 +13,8 @@ describe("StopResolvers", () => { let mockSystem: ISystem; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); - mockStop = await addMockStopToRepository(context.repository, mockSystem.id); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); + mockStop = await addMockStopToRepository(context.shuttleRepository, mockSystem.id); }) async function getResponseForQuery(query: string) { @@ -25,9 +25,7 @@ describe("StopResolvers", () => { stopId: mockStop.id, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context, }); } @@ -51,7 +49,7 @@ describe("StopResolvers", () => { mockOrderedStops = mockOrderedStops.filter((orderedStop) => orderedStop.stopId === mockOrderedStops[0].stopId); await Promise.all(mockOrderedStops.map(async orderedStop => { orderedStop.stopId = mockStop.id; - await context.repository.addOrUpdateOrderedStop(orderedStop); + await context.shuttleRepository.addOrUpdateOrderedStop(orderedStop); })); const response = await getResponseForQuery(query); @@ -88,7 +86,7 @@ describe("StopResolvers", () => { mockEtas = mockEtas.filter((eta) => eta.stopId === mockEtas[0].stopId); await Promise.all(mockEtas.map(async eta => { eta.stopId = mockStop.id; - await context.repository.addOrUpdateEta(eta); + await context.shuttleRepository.addOrUpdateEta(eta); })); const response = await getResponseForQuery(query); diff --git a/test/resolvers/SystemResolverTests.test.ts b/test/resolvers/SystemResolverTests.test.ts index b7f3f5d..1eb817c 100644 --- a/test/resolvers/SystemResolverTests.test.ts +++ b/test/resolvers/SystemResolverTests.test.ts @@ -17,7 +17,7 @@ describe("SystemResolvers", () => { let mockSystem: ISystem; beforeEach(async () => { - mockSystem = await addMockSystemToRepository(context.repository); + mockSystem = await addMockSystemToRepository(context.shuttleRepository); }); // TODO: Consolidate these into one single method taking an object @@ -29,7 +29,7 @@ describe("SystemResolvers", () => { }, }, { contextValue: { - repository: context.repository + shuttleRepository: context.shuttleRepository }, }); } @@ -50,7 +50,7 @@ describe("SystemResolvers", () => { const expectedRoutes = generateMockRoutes(); await Promise.all(expectedRoutes.map(async (route) => { route.systemId = mockSystem.id; - await context.repository.addOrUpdateRoute(route); + await context.shuttleRepository.addOrUpdateRoute(route); })); const response = await getResponseFromQueryNeedingSystemId(query); @@ -78,7 +78,7 @@ describe("SystemResolvers", () => { const expectedStops = generateMockStops(); await Promise.all(expectedStops.map(async (stop) => { stop.systemId = mockSystem.id; - await context.repository.addOrUpdateStop(stop); + await context.shuttleRepository.addOrUpdateStop(stop); })); const response = await getResponseFromQueryNeedingSystemId(query); @@ -111,13 +111,13 @@ describe("SystemResolvers", () => { }, }, { contextValue: { - repository: context.repository, + shuttleRepository: context.shuttleRepository, } }); } it("gets the stop with the correct id", async () => { - const mockStop = await addMockStopToRepository(context.repository, mockSystem.id); + const mockStop = await addMockStopToRepository(context.shuttleRepository, mockSystem.id); const response = await getResponseForStopQuery(mockStop.id); @@ -133,9 +133,9 @@ describe("SystemResolvers", () => { ...mockSystem, id: "2", } - await context.repository.addOrUpdateSystem(updatedSystem); + await context.shuttleRepository.addOrUpdateSystem(updatedSystem); - const mockStop = await addMockStopToRepository(context.repository, updatedSystem.id); + const mockStop = await addMockStopToRepository(context.shuttleRepository, updatedSystem.id); const response = await getResponseForStopQuery(mockStop.id); @@ -177,14 +177,12 @@ describe("SystemResolvers", () => { routeId, }, }, { - contextValue: { - repository: context.repository, - } + contextValue: context }); } it("gets the route with the correct id", async () => { - const mockRoute = await addMockRouteToRepository(context.repository, mockSystem.id); + const mockRoute = await addMockRouteToRepository(context.shuttleRepository, mockSystem.id); const response = await getResponseForRouteQuery(mockRoute.id); @@ -201,9 +199,9 @@ describe("SystemResolvers", () => { ...mockSystem, id: "2", } - await context.repository.addOrUpdateSystem(updatedSystem); + await context.shuttleRepository.addOrUpdateSystem(updatedSystem); - const mockRoute = await addMockRouteToRepository(context.repository, updatedSystem.id); + const mockRoute = await addMockRouteToRepository(context.shuttleRepository, updatedSystem.id); const response = await getResponseForRouteQuery(mockRoute.id); @@ -245,14 +243,13 @@ describe("SystemResolvers", () => { shuttleId: shuttleId, } }, { - contextValue: { - repository: context.repository, - } + contextValue: context + }); } it("gets the shuttle with the correct id", async () => { - const mockShuttle = await addMockShuttleToRepository(context.repository, mockSystem.id); + const mockShuttle = await addMockShuttleToRepository(context.shuttleRepository, mockSystem.id); const response = await getResponseForShuttleQuery(mockShuttle.id); @@ -268,9 +265,9 @@ describe("SystemResolvers", () => { ...mockSystem, id: "2", } - await context.repository.addOrUpdateSystem(updatedSystem); + await context.shuttleRepository.addOrUpdateSystem(updatedSystem); - const mockShuttle = await addMockShuttleToRepository(context.repository, updatedSystem.id); + const mockShuttle = await addMockShuttleToRepository(context.shuttleRepository, updatedSystem.id); const response = await getResponseForShuttleQuery(mockShuttle.id); @@ -308,7 +305,7 @@ describe("SystemResolvers", () => { const expectedShuttles = generateMockShuttles(); await Promise.all(expectedShuttles.map(async (shuttle) => { shuttle.systemId = mockSystem.id; - await context.repository.addOrUpdateShuttle(shuttle); + await context.shuttleRepository.addOrUpdateShuttle(shuttle); })); const response = await getResponseFromQueryNeedingSystemId(query); diff --git a/test/testHelpers/apolloTestServerHelpers.ts b/test/testHelpers/apolloTestServerHelpers.ts index f9fa2d9..a7512ec 100644 --- a/test/testHelpers/apolloTestServerHelpers.ts +++ b/test/testHelpers/apolloTestServerHelpers.ts @@ -1,10 +1,11 @@ import { readFileSync } from "fs"; import { ApolloServer } from "@apollo/server"; import { MergedResolvers } from "../../src/MergedResolvers"; -import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { UnoptimizedInMemoryShuttleRepository } from "../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import { beforeEach } from "@jest/globals"; import { ServerContext } from "../../src/ServerContext"; import { ETANotificationScheduler } from "../../src/notifications/schedulers/ETANotificationScheduler"; +import { InMemoryNotificationRepository } from "../../src/repositories/InMemoryNotificationRepository"; function setUpTestServer() { @@ -25,8 +26,8 @@ export function setupTestServerContext() { const context: { [key: string] : any } = {}; beforeEach(() => { - context.repository = new UnoptimizedInMemoryRepository(); - context.notificationService = new ETANotificationScheduler(context.repository); + context.shuttleRepository = new UnoptimizedInMemoryShuttleRepository(); + context.notificationRepository = new InMemoryNotificationRepository(); }); return context as ServerContext; diff --git a/test/testHelpers/repositorySetupHelpers.ts b/test/testHelpers/repositorySetupHelpers.ts index 4c95363..58caea3 100644 --- a/test/testHelpers/repositorySetupHelpers.ts +++ b/test/testHelpers/repositorySetupHelpers.ts @@ -5,9 +5,9 @@ import { generateMockStops, generateMockSystems } from "./mockDataGenerators"; -import { GetterSetterRepository } from "../../src/repositories/GetterSetterRepository"; +import { ShuttleGetterSetterRepository } from "../../src/repositories/ShuttleGetterSetterRepository"; -export async function addMockSystemToRepository(repository: GetterSetterRepository) { +export async function addMockSystemToRepository(repository: ShuttleGetterSetterRepository) { const mockSystems = generateMockSystems(); const mockSystem = mockSystems[0]; mockSystem.id = "1"; @@ -16,7 +16,7 @@ export async function addMockSystemToRepository(repository: GetterSetterReposito return mockSystem; } -export async function addMockRouteToRepository(repository: GetterSetterRepository, systemId: string) { +export async function addMockRouteToRepository(repository: ShuttleGetterSetterRepository, systemId: string) { const mockRoutes = generateMockRoutes(); const mockRoute = mockRoutes[0]; mockRoute.systemId = systemId; @@ -25,7 +25,7 @@ export async function addMockRouteToRepository(repository: GetterSetterRepositor return mockRoute; } -export async function addMockStopToRepository(repository: GetterSetterRepository, systemId: string) { +export async function addMockStopToRepository(repository: ShuttleGetterSetterRepository, systemId: string) { const mockStops = generateMockStops(); const mockStop = mockStops[0]; mockStop.systemId = systemId; @@ -34,7 +34,7 @@ export async function addMockStopToRepository(repository: GetterSetterRepository return mockStop; } -export async function addMockShuttleToRepository(repository: GetterSetterRepository, systemId: string) { +export async function addMockShuttleToRepository(repository: ShuttleGetterSetterRepository, systemId: string) { const mockShuttles = generateMockShuttles(); const mockShuttle = mockShuttles[0]; mockShuttle.systemId = systemId; @@ -42,7 +42,7 @@ export async function addMockShuttleToRepository(repository: GetterSetterReposit return mockShuttle; } -export async function addMockEtaToRepository(repository: GetterSetterRepository, stopId: string, shuttleId: string) { +export async function addMockEtaToRepository(repository: ShuttleGetterSetterRepository, stopId: string, shuttleId: string) { const etas = generateMockEtas(); const expectedEta = etas[0]; expectedEta.stopId = stopId;