mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-17 07:50:31 +00:00
Merge pull request #35 from brendan-ch/feat/in-memory-notification-repository
feat/in-memory-notification-repository
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
135
package-lock.json
generated
135
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
28
redis.conf
Normal file
28
redis.conf
Normal file
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
42
src/index.ts
42
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,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface RepositoryLoader {
|
||||
export interface ShuttleRepositoryLoader {
|
||||
fetchAndUpdateSystemData(): Promise<void>;
|
||||
fetchAndUpdateRouteDataForExistingSystemsInRepository(): Promise<void>;
|
||||
fetchAndUpdateRouteDataForSystemId(systemId: string): Promise<void>;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}));
|
||||
@@ -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<boolean> {
|
||||
private async sendEtaNotificationImmediately(notificationData: ScheduledNotification): Promise<boolean> {
|
||||
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<string>();
|
||||
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<NotificationLookupArguments[]> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
144
src/repositories/InMemoryNotificationRepository.ts
Normal file
144
src/repositories/InMemoryNotificationRepository.ts
Normal file
@@ -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<boolean> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/repositories/NotificationRepository.ts
Normal file
33
src/repositories/NotificationRepository.ts
Normal file
@@ -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<ScheduledNotification[]>;
|
||||
getSecondsThresholdForNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise<number | null>;
|
||||
isNotificationScheduled(lookupArguments: NotificationLookupArguments): Promise<boolean>;
|
||||
addOrUpdateNotification(notification: ScheduledNotification): Promise<void>;
|
||||
deleteNotificationIfExists(lookupArguments: NotificationLookupArguments): Promise<void>;
|
||||
|
||||
subscribeToNotificationChanges(listener: Listener): void;
|
||||
unsubscribeFromNotificationChanges(listener: Listener): void;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IEta, IOrderedStop, IRoute, IShuttle, IStop, ISystem } from "../entities/entities";
|
||||
|
||||
export interface GetterRepository {
|
||||
export interface ShuttleGetterRepository {
|
||||
getSystems(): Promise<ISystem[]>;
|
||||
getSystemById(systemId: string): Promise<ISystem | null>;
|
||||
|
||||
@@ -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<void>;
|
||||
addOrUpdateRoute(route: IRoute): Promise<void>;
|
||||
@@ -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[] = [];
|
||||
@@ -4,10 +4,10 @@ import { ServerContext } from "../ServerContext";
|
||||
export const EtaResolvers: Resolvers<ServerContext> = {
|
||||
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);
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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<ServerContext> = {
|
||||
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<ServerContext> = {
|
||||
}
|
||||
}
|
||||
|
||||
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<ServerContext> = {
|
||||
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",
|
||||
|
||||
@@ -7,13 +7,13 @@ export const OrderedStopResolvers: Resolvers<ServerContext> = {
|
||||
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<ServerContext> = {
|
||||
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,10 +42,10 @@ export const OrderedStopResolvers: Resolvers<ServerContext> = {
|
||||
}
|
||||
},
|
||||
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);
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import { Resolvers } from "../generated/graphql";
|
||||
export const QueryResolvers: Resolvers<ServerContext> = {
|
||||
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<ServerContext> = {
|
||||
},
|
||||
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);
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ServerContext } from "../ServerContext";
|
||||
export const RouteResolvers: Resolvers<ServerContext> = {
|
||||
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<ServerContext> = {
|
||||
},
|
||||
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 {
|
||||
|
||||
@@ -5,7 +5,7 @@ export const ShuttleResolvers: Resolvers<ServerContext> = {
|
||||
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<ServerContext> = {
|
||||
};
|
||||
},
|
||||
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<ServerContext> = {
|
||||
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 {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { ServerContext } from "../ServerContext";
|
||||
export const StopResolvers: Resolvers<ServerContext> = {
|
||||
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);
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -4,14 +4,14 @@ import { ServerContext } from "../ServerContext";
|
||||
export const SystemResolvers: Resolvers<ServerContext> = {
|
||||
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<ServerContext> = {
|
||||
},
|
||||
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<ServerContext> = {
|
||||
},
|
||||
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<ServerContext> = {
|
||||
return shuttle;
|
||||
},
|
||||
shuttles: async (parent, args, contextValue, info) => {
|
||||
return await contextValue.repository.getShuttlesBySystemId(parent.id);
|
||||
return await contextValue.shuttleRepository.getShuttlesBySystemId(parent.id);
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
@@ -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<void> {
|
||||
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
144
test/repositories/InMemoryNotificationRepositoryTests.test.ts
Normal file
144
test/repositories/InMemoryNotificationRepositoryTests.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
})
|
||||
});
|
||||
@@ -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", () => {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user