From 619ef9a27ff5b08cc48d433664223e27a4ca9568 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 09:20:10 -0700 Subject: [PATCH 01/15] update naming of classes and tests --- src/ServerContext.ts | 4 ++-- src/index.ts | 8 ++++---- .../schedulers/ETANotificationScheduler.ts} | 10 +++++----- .../senders/AppleNotificationSender.ts | 0 .../fetchSystemDataSuccessfulResponse.ts | 6 +++--- .../ETANotificationSchedulerTests.ts} | 18 +++++++++--------- test/resolvers/QueryResolverTests.test.ts | 2 +- test/testHelpers/apolloTestServerHelpers.ts | 4 ++-- 8 files changed, 26 insertions(+), 26 deletions(-) rename src/{services/NotificationService.ts => notifications/schedulers/ETANotificationScheduler.ts} (96%) create mode 100644 src/notifications/senders/AppleNotificationSender.ts rename test/{services/NotificationServiceTests.test.ts => notifications/schedulers/ETANotificationSchedulerTests.ts} (93%) diff --git a/src/ServerContext.ts b/src/ServerContext.ts index 6579453..8dade11 100644 --- a/src/ServerContext.ts +++ b/src/ServerContext.ts @@ -1,7 +1,7 @@ -import { NotificationService } from "./services/NotificationService"; +import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; import { GetterSetterRepository } from "./repositories/GetterSetterRepository"; export interface ServerContext { repository: GetterSetterRepository; - notificationService: NotificationService; + notificationService: ETANotificationScheduler; } diff --git a/src/index.ts b/src/index.ts index 3cf1915..fb42543 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { MergedResolvers } from "./MergedResolvers"; import { ServerContext } from "./ServerContext"; import { UnoptimizedInMemoryRepository } from "./repositories/UnoptimizedInMemoryRepository"; import { TimedApiBasedRepositoryLoader } from "./loaders/TimedApiBasedRepositoryLoader"; -import { NotificationService } from "./services/NotificationService"; +import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; import { configDotenv } from "dotenv"; import { loadTestData } from "./loaders/loadTestData"; @@ -21,16 +21,16 @@ async function main() { }); const repository = new UnoptimizedInMemoryRepository(); - let notificationService: NotificationService; + let notificationService: ETANotificationScheduler; if (process.argv.length > 2 && process.argv[2] == "integration-testing") { await loadTestData(repository); - notificationService = new NotificationService(repository, false); + notificationService = new ETANotificationScheduler(repository, false); } else { const repositoryDataUpdater = new TimedApiBasedRepositoryLoader( repository ); await repositoryDataUpdater.start(); - notificationService = new NotificationService(repository); + notificationService = new ETANotificationScheduler(repository); } notificationService.reloadAPNsTokenIfTimePassed(); diff --git a/src/services/NotificationService.ts b/src/notifications/schedulers/ETANotificationScheduler.ts similarity index 96% rename from src/services/NotificationService.ts rename to src/notifications/schedulers/ETANotificationScheduler.ts index bb15e46..16251da 100644 --- a/src/services/NotificationService.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -1,8 +1,8 @@ -import { GetterRepository } from "../repositories/GetterRepository"; +import { GetterRepository } from "../../repositories/GetterRepository"; import jwt from "jsonwebtoken"; import fs from "fs"; -import { TupleKey } from "../types/TupleKey"; -import { IEta } from "../entities/entities"; +import { TupleKey } from "../../types/TupleKey"; +import { IEta } from "../../entities/entities"; import http2 from "http2"; export interface ScheduledNotificationData { @@ -17,7 +17,7 @@ interface APNsUrl { host: string; } -export class NotificationService { +export class ETANotificationScheduler { public readonly secondsThresholdForNotificationToFire = 180; private apnsToken: string | undefined = undefined; @@ -113,7 +113,7 @@ export class NotificationService { throw new Error("APNS_BUNDLE_ID environment variable is not set correctly"); } - const { path, host } = NotificationService.getAPNsFullUrlToUse(deviceId); + const { path, host } = ETANotificationScheduler.getAPNsFullUrlToUse(deviceId); const headers = { ':method': 'POST', diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/jsonSnapshots/fetchSystemData/fetchSystemDataSuccessfulResponse.ts b/test/jsonSnapshots/fetchSystemData/fetchSystemDataSuccessfulResponse.ts index 67b77e5..449c5c6 100644 --- a/test/jsonSnapshots/fetchSystemData/fetchSystemDataSuccessfulResponse.ts +++ b/test/jsonSnapshots/fetchSystemData/fetchSystemDataSuccessfulResponse.ts @@ -2094,9 +2094,9 @@ export const fetchSystemDataSuccessfulResponse = { "logo": 1, "goRoutePlannerEnabled": "0", "goColor": null, - "goSupportEmail": "transit.services@pepperdine.edu", + "goSupportEmail": "transit.schedulers@pepperdine.edu", "goAuthenticationType": "0", - "email": "transit.services@pepperdine.edu" + "email": "transit.schedulers@pepperdine.edu" }, { "fullname": "Perimeter Summit Shuttle", @@ -3347,4 +3347,4 @@ export const fetchSystemDataSuccessfulResponse = { "fromCache": 1, "myip": "206.211.154.147" } -}; \ No newline at end of file +}; diff --git a/test/services/NotificationServiceTests.test.ts b/test/notifications/schedulers/ETANotificationSchedulerTests.ts similarity index 93% rename from test/services/NotificationServiceTests.test.ts rename to test/notifications/schedulers/ETANotificationSchedulerTests.ts index 8517727..3fb738b 100644 --- a/test/services/NotificationServiceTests.test.ts +++ b/test/notifications/schedulers/ETANotificationSchedulerTests.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; -import { NotificationService } from "../../src/services/NotificationService"; -import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; +import { ETANotificationScheduler } from "../../../src/notifications/schedulers/ETANotificationScheduler"; +import { UnoptimizedInMemoryRepository } from "../../../src/repositories/UnoptimizedInMemoryRepository"; import http2 from "http2"; -import { IEta, IShuttle, IStop } from "../../src/entities/entities"; -import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers"; +import { IEta, IShuttle, IStop } from "../../../src/entities/entities"; +import { addMockShuttleToRepository, addMockStopToRepository } from "../../testHelpers/repositorySetupHelpers"; import EventEmitter = require("node:events"); jest.mock("http2"); @@ -55,13 +55,13 @@ function mockHttp2Connect(status: number) { (http2.connect as jest.Mock) = jest.fn(() => new MockClient()); } -describe("NotificationService", () => { +describe("ETANotificationScheduler", () => { let repository: UnoptimizedInMemoryRepository - let notificationService: NotificationService; + let notificationService: ETANotificationScheduler; beforeEach(() => { repository = new UnoptimizedInMemoryRepository(); - notificationService = new NotificationService(repository); + notificationService = new ETANotificationScheduler(repository); // Ensure that tests don't hit the server process.env = { @@ -192,7 +192,7 @@ describe("NotificationService", () => { it('should return the production URL when APNS_IS_PRODUCTION is set to "1"', () => { process.env.APNS_IS_PRODUCTION = "1"; const deviceId = 'testDeviceId'; - const result = NotificationService.getAPNsFullUrlToUse(deviceId); + const result = ETANotificationScheduler.getAPNsFullUrlToUse(deviceId); const { fullUrl, host, path } = result; expect(fullUrl).toBe(`https://api.push.apple.com/3/device/${deviceId}`); @@ -203,7 +203,7 @@ describe("NotificationService", () => { it('should return the sandbox URL when APNS_IS_PRODUCTION is set to something other than 1', () => { process.env.APNS_IS_PRODUCTION = "0"; const deviceId = 'testDeviceId'; - const result = NotificationService.getAPNsFullUrlToUse(deviceId); + const result = ETANotificationScheduler.getAPNsFullUrlToUse(deviceId); const { fullUrl, host, path } = result; expect(fullUrl).toBe(`https://api.development.push.apple.com/3/device/${deviceId}`); diff --git a/test/resolvers/QueryResolverTests.test.ts b/test/resolvers/QueryResolverTests.test.ts index bb53d5b..8b0b60e 100644 --- a/test/resolvers/QueryResolverTests.test.ts +++ b/test/resolvers/QueryResolverTests.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "@jest/globals"; import { generateMockSystems } from "../testHelpers/mockDataGenerators"; import { setupTestServerContext, setupTestServerHolder } from "../testHelpers/apolloTestServerHelpers"; import assert = require("node:assert"); -import { ScheduledNotificationData } from "../../src/services/NotificationService"; +import { ScheduledNotificationData } from "../../src/notifications/schedulers/ETANotificationScheduler"; import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers"; // See Apollo documentation for integration test guide diff --git a/test/testHelpers/apolloTestServerHelpers.ts b/test/testHelpers/apolloTestServerHelpers.ts index a8af9d4..f9fa2d9 100644 --- a/test/testHelpers/apolloTestServerHelpers.ts +++ b/test/testHelpers/apolloTestServerHelpers.ts @@ -4,7 +4,7 @@ import { MergedResolvers } from "../../src/MergedResolvers"; import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository"; import { beforeEach } from "@jest/globals"; import { ServerContext } from "../../src/ServerContext"; -import { NotificationService } from "../../src/services/NotificationService"; +import { ETANotificationScheduler } from "../../src/notifications/schedulers/ETANotificationScheduler"; function setUpTestServer() { @@ -26,7 +26,7 @@ export function setupTestServerContext() { beforeEach(() => { context.repository = new UnoptimizedInMemoryRepository(); - context.notificationService = new NotificationService(context.repository); + context.notificationService = new ETANotificationScheduler(context.repository); }); return context as ServerContext; From a58780a37d5b2ab30ca0f97d4be88e17cdd70bda Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 09:30:22 -0700 Subject: [PATCH 02/15] add apns token methods --- .../senders/AppleNotificationSender.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts index e69de29..c4a551d 100644 --- a/src/notifications/senders/AppleNotificationSender.ts +++ b/src/notifications/senders/AppleNotificationSender.ts @@ -0,0 +1,73 @@ +import jwt from "jsonwebtoken"; + +interface APNsUrl { + fullUrl: string; + path: string; + host: string; +} + +class AppleNotificationSender { + private apnsToken: string | undefined = undefined; + private _lastRefreshedTimeMs: number | undefined = undefined; + + private lastReloadedTimeForAPNsIsTooRecent() { + const thirtyMinutesMs = 1800000; + return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; + } + + private reloadAPNsTokenIfTimePassed() { + if (this.lastReloadedTimeForAPNsIsTooRecent()) { + return; + } + + const keyId = process.env.APNS_KEY_ID; + const teamId = process.env.APNS_TEAM_ID; + + const privateKeyBase64 = process.env.APNS_PRIVATE_KEY; + if (!privateKeyBase64) return; + const privateKey = Buffer.from(privateKeyBase64, 'base64').toString('utf-8'); + + const tokenHeader = { + alg: "ES256", + "kid": keyId, + }; + + const nowMs = Date.now(); + const claimsPayload = { + "iss": teamId, + "iat": Math.ceil(nowMs / 1000), // APNs requires number of seconds since Epoch + }; + + this.apnsToken = jwt.sign(claimsPayload, privateKey, { + algorithm: "ES256", + header: tokenHeader + }); + this._lastRefreshedTimeMs = nowMs; + } + + public sendNotificationImmediately(notification: any) { + // TODO Send the notification + } + + public static getAPNsFullUrlToUse(deviceId: string): APNsUrl { + // Construct the fetch request + const devBaseUrl = "https://api.development.push.apple.com" + const prodBaseUrl = "https://api.push.apple.com" + + let hostToUse = devBaseUrl; + if (process.env.APNS_IS_PRODUCTION === "1") { + hostToUse = prodBaseUrl; + } + + const path = "/3/device/" + deviceId; + const fullUrl = hostToUse + path; + + const constructedObject = { + fullUrl, + host: hostToUse, + path, + } + return constructedObject; + } + +} From 83766c90c5f699d24a5c41b1fbac0e6aaf49e7a0 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 09:34:53 -0700 Subject: [PATCH 03/15] add notification sending logic --- .../senders/AppleNotificationSender.ts | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts index c4a551d..002da8f 100644 --- a/src/notifications/senders/AppleNotificationSender.ts +++ b/src/notifications/senders/AppleNotificationSender.ts @@ -1,4 +1,5 @@ import jwt from "jsonwebtoken"; +import http2 from "http2"; interface APNsUrl { fullUrl: string; @@ -6,6 +7,11 @@ interface APNsUrl { host: string; } +interface NotificationAlertArguments { + title: string; + body: string; +} + class AppleNotificationSender { private apnsToken: string | undefined = undefined; private _lastRefreshedTimeMs: number | undefined = undefined; @@ -45,8 +51,56 @@ class AppleNotificationSender { this._lastRefreshedTimeMs = nowMs; } - public sendNotificationImmediately(notification: any) { - // TODO Send the notification + /** + * Send a notification immediately. + * @param deviceId + * @param notificationAlertArguments + * + * @return Boolean promise indicating whether the + * notification was sent successfully. + */ + public async sendNotificationImmediately(deviceId: string, notificationAlertArguments: NotificationAlertArguments) { + const bundleId = process.env.APNS_BUNDLE_ID; + if (typeof bundleId !== "string") { + throw new Error("APNS_BUNDLE_ID environment variable is not set correctly"); + } + + const { path, host } = AppleNotificationSender.getAPNsFullUrlToUse(deviceId); + + const headers = { + ':method': 'POST', + ':path': path, + 'authorization': `bearer ${this.apnsToken}`, + "apns-push-type": "alert", + "apns-expiration": "0", + "apns-priority": "10", + "apns-topic": bundleId, + }; + try { + const client = http2.connect(host); + const req = client.request(headers); + req.setEncoding('utf8'); + + await new Promise((resolve, reject) => { + req.on('response', (headers, flags) => { + if (headers[":status"] !== 200) { + reject(`APNs request failed with status ${headers[":status"]}`); + } + resolve(); + }); + + req.write(JSON.stringify({ + aps: { + alert: notificationAlertArguments, + } + })); + req.end(); + }); + return true; + } catch(e) { + console.error(e); + return false; + } } public static getAPNsFullUrlToUse(deviceId: string): APNsUrl { From 7f1bf005c1f53833a7a4b2131257332195ddec9a Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 09:39:07 -0700 Subject: [PATCH 04/15] integrate notification sender class into ETA notification scheduler --- .../schedulers/ETANotificationScheduler.ts | 116 +----------------- .../senders/AppleNotificationSender.ts | 6 +- 2 files changed, 10 insertions(+), 112 deletions(-) diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 16251da..4e4e732 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -4,6 +4,7 @@ import fs from "fs"; import { TupleKey } from "../../types/TupleKey"; import { IEta } from "../../entities/entities"; import http2 from "http2"; +import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; export interface ScheduledNotificationData { deviceId: string; @@ -11,16 +12,10 @@ export interface ScheduledNotificationData { stopId: string; } -interface APNsUrl { - fullUrl: string; - path: string; - host: string; -} - export class ETANotificationScheduler { public readonly secondsThresholdForNotificationToFire = 180; - private apnsToken: string | undefined = undefined; + private appleNotificationSender = new AppleNotificationSender() private _lastRefreshedTimeMs: number | undefined = undefined; get lastRefreshedTimeMs() { @@ -29,8 +24,6 @@ export class ETANotificationScheduler { constructor(private repository: GetterRepository, private shouldActuallySendNotifications = true) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); - this.reloadAPNsTokenIfTimePassed = this.reloadAPNsTokenIfTimePassed.bind(this); - this.lastReloadedTimeForAPNsIsTooRecent = this.lastReloadedTimeForAPNsIsTooRecent.bind(this); this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold = this.sendEtaNotificationImmediatelyIfSecondsRemainingBelowThreshold.bind(this); @@ -45,48 +38,12 @@ export class ETANotificationScheduler { */ private deviceIdsToDeliverTo: { [key: string]: Set } = {} - public reloadAPNsTokenIfTimePassed() { - if (this.lastReloadedTimeForAPNsIsTooRecent()) { - return; - } - - const keyId = process.env.APNS_KEY_ID; - const teamId = process.env.APNS_TEAM_ID; - - const privateKeyBase64 = process.env.APNS_PRIVATE_KEY; - if (!privateKeyBase64) return; - const privateKey = Buffer.from(privateKeyBase64, 'base64').toString('utf-8'); - - const tokenHeader = { - alg: "ES256", - "kid": keyId, - }; - - const nowMs = Date.now(); - const claimsPayload = { - "iss": teamId, - "iat": Math.ceil(nowMs / 1000), // APNs requires number of seconds since Epoch - }; - - this.apnsToken = jwt.sign(claimsPayload, privateKey, { - algorithm: "ES256", - header: tokenHeader - }); - this._lastRefreshedTimeMs = nowMs; - } - - private lastReloadedTimeForAPNsIsTooRecent() { - const thirtyMinutesMs = 1800000; - return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; - } - private async sendEtaNotificationImmediately(notificationData: ScheduledNotificationData): Promise { if (!this.shouldActuallySendNotifications) { return true; // pretend that the notification sent } const { deviceId, shuttleId, stopId } = notificationData; - this.reloadAPNsTokenIfTimePassed(); const shuttle = await this.repository.getShuttleById(shuttleId); const stop = await this.repository.getStopById(stopId); @@ -107,72 +64,11 @@ export class ETANotificationScheduler { return false; } - // Send the fetch request - const bundleId = process.env.APNS_BUNDLE_ID; - if (typeof bundleId !== "string") { - throw new Error("APNS_BUNDLE_ID environment variable is not set correctly"); + const notificationAlertArguments: NotificationAlertArguments = { + title: "Shuttle is arriving", + body: `Shuttle is approaching ${stop.name} in ${Math.ceil(eta.secondsRemaining / 60)} minutes.`, } - - const { path, host } = ETANotificationScheduler.getAPNsFullUrlToUse(deviceId); - - const headers = { - ':method': 'POST', - ':path': path, - 'authorization': `bearer ${this.apnsToken}`, - "apns-push-type": "alert", - "apns-expiration": "0", - "apns-priority": "10", - "apns-topic": bundleId, - }; - try { - const client = http2.connect(host); - const req = client.request(headers); - req.setEncoding('utf8'); - - await new Promise((resolve, reject) => { - req.on('response', (headers, flags) => { - if (headers[":status"] !== 200) { - reject(`APNs request failed with status ${headers[":status"]}`); - } - resolve(); - }); - - req.write(JSON.stringify({ - aps: { - alert: { - title: "Shuttle is arriving", - body: `Shuttle is approaching ${stop.name} in ${Math.ceil(eta.secondsRemaining / 60)} minutes.` - } - } - })); - req.end(); - }); - return true; - } catch(e) { - console.error(e); - return false; - } - } - - public static getAPNsFullUrlToUse(deviceId: string): APNsUrl { - // Construct the fetch request - const devBaseUrl = "https://api.development.push.apple.com" - const prodBaseUrl = "https://api.push.apple.com" - - let hostToUse = devBaseUrl; - if (process.env.APNS_IS_PRODUCTION === "1") { - hostToUse = prodBaseUrl; - } - - const path = "/3/device/" + deviceId; - const fullUrl = hostToUse + path; - - const constructedObject = { - fullUrl, - host: hostToUse, - path, - } - return constructedObject; + return this.appleNotificationSender.sendNotificationImmediately(deviceId, notificationAlertArguments); } private async etaSubscriberCallback(eta: IEta) { diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts index 002da8f..7bfa0d9 100644 --- a/src/notifications/senders/AppleNotificationSender.ts +++ b/src/notifications/senders/AppleNotificationSender.ts @@ -7,12 +7,12 @@ interface APNsUrl { host: string; } -interface NotificationAlertArguments { +export interface NotificationAlertArguments { title: string; body: string; } -class AppleNotificationSender { +export class AppleNotificationSender { private apnsToken: string | undefined = undefined; private _lastRefreshedTimeMs: number | undefined = undefined; @@ -60,6 +60,8 @@ class AppleNotificationSender { * notification was sent successfully. */ public async sendNotificationImmediately(deviceId: string, notificationAlertArguments: NotificationAlertArguments) { + this.reloadAPNsTokenIfTimePassed(); + const bundleId = process.env.APNS_BUNDLE_ID; if (typeof bundleId !== "string") { throw new Error("APNS_BUNDLE_ID environment variable is not set correctly"); From 9869632a88cc868e64adae1b901234b897a0b87b Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 09:42:41 -0700 Subject: [PATCH 05/15] change reloadAPNsTokenIfTimePassed method to public for testing --- src/notifications/senders/AppleNotificationSender.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts index 7bfa0d9..4a171c5 100644 --- a/src/notifications/senders/AppleNotificationSender.ts +++ b/src/notifications/senders/AppleNotificationSender.ts @@ -21,7 +21,7 @@ export class AppleNotificationSender { return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; } - private reloadAPNsTokenIfTimePassed() { + public reloadAPNsTokenIfTimePassed() { if (this.lastReloadedTimeForAPNsIsTooRecent()) { return; } From 23dc4c3f59d08895171a81bc5d3383efbbbf37e5 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 09:44:50 -0700 Subject: [PATCH 06/15] add getter for the last refreshed token time --- src/notifications/senders/AppleNotificationSender.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts index 4a171c5..7d358b1 100644 --- a/src/notifications/senders/AppleNotificationSender.ts +++ b/src/notifications/senders/AppleNotificationSender.ts @@ -16,6 +16,10 @@ export class AppleNotificationSender { private apnsToken: string | undefined = undefined; private _lastRefreshedTimeMs: number | undefined = undefined; + get lastRefreshedTimeMs(): number | undefined { + return this._lastRefreshedTimeMs; + } + private lastReloadedTimeForAPNsIsTooRecent() { const thirtyMinutesMs = 1800000; return this._lastRefreshedTimeMs && Date.now() - this._lastRefreshedTimeMs < thirtyMinutesMs; From 26cdab990789835d59e1347229987a623036779c Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 09:48:35 -0700 Subject: [PATCH 07/15] copy over test for APNs token reload --- .../AppleNotificationSenderTests.test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 test/notifications/senders/AppleNotificationSenderTests.test.ts diff --git a/test/notifications/senders/AppleNotificationSenderTests.test.ts b/test/notifications/senders/AppleNotificationSenderTests.test.ts new file mode 100644 index 0000000..c4fddb5 --- /dev/null +++ b/test/notifications/senders/AppleNotificationSenderTests.test.ts @@ -0,0 +1,66 @@ +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import http2 from "http2"; +import { EventEmitter } from "node:events"; +import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender"; + +jest.mock("http2"); + +const sampleKeyBase64 = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR1RBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJIa3dkd0lCQVFRZ3NybVNBWklhZ09mQ1A4c0IKV2kyQ0JYRzFPbzd2MWJpc3BJWkN3SXI0UkRlZ0NnWUlLb1pJemowREFRZWhSQU5DQUFUWkh4VjJ3UUpMTUJxKwp5YSt5ZkdpM2cyWlV2NmhyZmUrajA4eXRla1BIalhTMHF6Sm9WRUx6S0hhNkVMOVlBb1pEWEJ0QjZoK2ZHaFhlClNPY09OYmFmCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"; + +function mockHttp2Connect(status: number) { + class MockClient extends EventEmitter { + request = jest.fn((headers: any) => { + const mockRequest: any = new EventEmitter(); + mockRequest.setEncoding = jest.fn(); + mockRequest.write = jest.fn(); + mockRequest.end = jest.fn(() => { + setTimeout(() => { + mockRequest.emit('response', { ':status': status }); + }, 10); + }); + return mockRequest; + }); + + close() {}; + } + + (http2.connect as jest.Mock) = jest.fn(() => new MockClient()); +} + +describe("AppleNotificationSender", () => { + let notificationSender: AppleNotificationSender; + + beforeEach(() => { + notificationSender = new AppleNotificationSender(); + + // Ensure that tests don't hit the server + process.env = { + ...process.env, + APNS_KEY_ID: "1", + APNS_TEAM_ID: "1", + APNS_BUNDLE_ID: "dev.bchen.ProjectInter", + APNS_PRIVATE_KEY: sampleKeyBase64, + }; + }); + + beforeEach(() => { + mockHttp2Connect(200); + }); + + describe("reloadAPNsTokenIfTimePassed", () => { + it("reloads the token if token hasn't been generated yet", async () => { + notificationSender.reloadAPNsTokenIfTimePassed(); + expect(notificationSender.lastRefreshedTimeMs).toBeDefined(); + }); + + it("doesn't reload the token if last refreshed time is recent", async () => { + notificationSender.reloadAPNsTokenIfTimePassed(); + const lastRefreshedTimeMs = notificationSender.lastRefreshedTimeMs; + + notificationSender.reloadAPNsTokenIfTimePassed(); + // Expect no change to have occurred + expect(lastRefreshedTimeMs).toEqual(notificationSender.lastRefreshedTimeMs); + }); + }); + +}); From a6ceeb4dfbf149d436fcdc7a04198ad27b78f056 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 09:59:29 -0700 Subject: [PATCH 08/15] add optional dependency inject for apple notification sender --- .../schedulers/ETANotificationScheduler.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index 4e4e732..ac63f5c 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -15,14 +15,10 @@ export interface ScheduledNotificationData { export class ETANotificationScheduler { public readonly secondsThresholdForNotificationToFire = 180; - private appleNotificationSender = new AppleNotificationSender() - - private _lastRefreshedTimeMs: number | undefined = undefined; - get lastRefreshedTimeMs() { - return this._lastRefreshedTimeMs; - } - - constructor(private repository: GetterRepository, private shouldActuallySendNotifications = true) { + constructor(private repository: GetterRepository, + private shouldActuallySendNotifications = true, + private appleNotificationSender = new AppleNotificationSender() + ) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); this.sendEtaNotificationImmediately = this.sendEtaNotificationImmediately.bind(this); this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); From 6251f0e24764dd8a28f791784aa77f419a787e54 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 10:02:30 -0700 Subject: [PATCH 09/15] move shouldSendNotifications property to the AppleNotificationSender --- src/index.ts | 5 +++-- src/notifications/schedulers/ETANotificationScheduler.ts | 8 -------- src/notifications/senders/AppleNotificationSender.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index fb42543..2f0f6bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ import { TimedApiBasedRepositoryLoader } from "./loaders/TimedApiBasedRepository import { ETANotificationScheduler } from "./notifications/schedulers/ETANotificationScheduler"; import { configDotenv } from "dotenv"; import { loadTestData } from "./loaders/loadTestData"; +import { AppleNotificationSender } from "./notifications/senders/AppleNotificationSender"; configDotenv(); @@ -24,7 +25,8 @@ async function main() { let notificationService: ETANotificationScheduler; if (process.argv.length > 2 && process.argv[2] == "integration-testing") { await loadTestData(repository); - notificationService = new ETANotificationScheduler(repository, false); + const appleNotificationSender = new AppleNotificationSender(false); + notificationService = new ETANotificationScheduler(repository, appleNotificationSender); } else { const repositoryDataUpdater = new TimedApiBasedRepositoryLoader( repository @@ -32,7 +34,6 @@ async function main() { await repositoryDataUpdater.start(); notificationService = new ETANotificationScheduler(repository); } - notificationService.reloadAPNsTokenIfTimePassed(); const { url } = await startStandaloneServer(server, { listen: { diff --git a/src/notifications/schedulers/ETANotificationScheduler.ts b/src/notifications/schedulers/ETANotificationScheduler.ts index ac63f5c..4ccf110 100644 --- a/src/notifications/schedulers/ETANotificationScheduler.ts +++ b/src/notifications/schedulers/ETANotificationScheduler.ts @@ -1,9 +1,6 @@ import { GetterRepository } from "../../repositories/GetterRepository"; -import jwt from "jsonwebtoken"; -import fs from "fs"; import { TupleKey } from "../../types/TupleKey"; import { IEta } from "../../entities/entities"; -import http2 from "http2"; import { AppleNotificationSender, NotificationAlertArguments } from "../senders/AppleNotificationSender"; export interface ScheduledNotificationData { @@ -16,7 +13,6 @@ export class ETANotificationScheduler { public readonly secondsThresholdForNotificationToFire = 180; constructor(private repository: GetterRepository, - private shouldActuallySendNotifications = true, private appleNotificationSender = new AppleNotificationSender() ) { this.etaSubscriberCallback = this.etaSubscriberCallback.bind(this); @@ -35,10 +31,6 @@ export class ETANotificationScheduler { private deviceIdsToDeliverTo: { [key: string]: Set } = {} private async sendEtaNotificationImmediately(notificationData: ScheduledNotificationData): Promise { - if (!this.shouldActuallySendNotifications) { - return true; // pretend that the notification sent - } - const { deviceId, shuttleId, stopId } = notificationData; const shuttle = await this.repository.getShuttleById(shuttleId); diff --git a/src/notifications/senders/AppleNotificationSender.ts b/src/notifications/senders/AppleNotificationSender.ts index 7d358b1..80c6f2a 100644 --- a/src/notifications/senders/AppleNotificationSender.ts +++ b/src/notifications/senders/AppleNotificationSender.ts @@ -16,6 +16,9 @@ export class AppleNotificationSender { private apnsToken: string | undefined = undefined; private _lastRefreshedTimeMs: number | undefined = undefined; + constructor(private shouldActuallySendNotifications = true) { + } + get lastRefreshedTimeMs(): number | undefined { return this._lastRefreshedTimeMs; } @@ -64,6 +67,11 @@ export class AppleNotificationSender { * notification was sent successfully. */ public async sendNotificationImmediately(deviceId: string, notificationAlertArguments: NotificationAlertArguments) { + if (!this.shouldActuallySendNotifications) { + // pretend that the notification sent + return true; + } + this.reloadAPNsTokenIfTimePassed(); const bundleId = process.env.APNS_BUNDLE_ID; From 5670efb042ced243e8145e23b10bc83be1e185c0 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 10:05:10 -0700 Subject: [PATCH 10/15] move getAPNsFullUrlToUse method to the notification sender tests --- .../AppleNotificationSenderTests.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/notifications/senders/AppleNotificationSenderTests.test.ts b/test/notifications/senders/AppleNotificationSenderTests.test.ts index c4fddb5..919f535 100644 --- a/test/notifications/senders/AppleNotificationSenderTests.test.ts +++ b/test/notifications/senders/AppleNotificationSenderTests.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import http2 from "http2"; import { EventEmitter } from "node:events"; import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender"; +import { ETANotificationScheduler } from "../../../src/notifications/schedulers/ETANotificationScheduler"; jest.mock("http2"); @@ -63,4 +64,31 @@ describe("AppleNotificationSender", () => { }); }); + describe('getAPNsFullUrlToUse', () => { + it('should return the production URL when APNS_IS_PRODUCTION is set to "1"', () => { + process.env.APNS_IS_PRODUCTION = "1"; + const deviceId = 'testDeviceId'; + const result = AppleNotificationSender.getAPNsFullUrlToUse(deviceId); + + const { fullUrl, host, path } = result; + expect(fullUrl).toBe(`https://api.push.apple.com/3/device/${deviceId}`); + expect(host).toBe("https://api.push.apple.com"); + expect(path).toBe(`/3/device/${deviceId}`); + }); + + it('should return the sandbox URL when APNS_IS_PRODUCTION is set to something other than 1', () => { + process.env.APNS_IS_PRODUCTION = "0"; + const deviceId = 'testDeviceId'; + const result = AppleNotificationSender.getAPNsFullUrlToUse(deviceId); + + const { fullUrl, host, path } = result; + expect(fullUrl).toBe(`https://api.development.push.apple.com/3/device/${deviceId}`); + expect(host).toBe("https://api.development.push.apple.com"); + expect(path).toBe(`/3/device/${deviceId}`); + }); + }); + + describe("sendNotificationImmediately", () => { + + }); }); From e433662f19c64b0083cdf5b6580e77c96dcc88f5 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 10:09:37 -0700 Subject: [PATCH 11/15] add test cases for sendNotificationImmediately --- .../AppleNotificationSenderTests.test.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/notifications/senders/AppleNotificationSenderTests.test.ts b/test/notifications/senders/AppleNotificationSenderTests.test.ts index 919f535..9804e20 100644 --- a/test/notifications/senders/AppleNotificationSenderTests.test.ts +++ b/test/notifications/senders/AppleNotificationSenderTests.test.ts @@ -1,7 +1,10 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import http2 from "http2"; import { EventEmitter } from "node:events"; -import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender"; +import { + AppleNotificationSender, + NotificationAlertArguments +} from "../../../src/notifications/senders/AppleNotificationSender"; import { ETANotificationScheduler } from "../../../src/notifications/schedulers/ETANotificationScheduler"; jest.mock("http2"); @@ -89,6 +92,26 @@ describe("AppleNotificationSender", () => { }); describe("sendNotificationImmediately", () => { + it('makes the connection to the http server if the notification should be sent', async () => { + }); + + it('throws an error if the bundle ID is not set correctly', async () => { + + }); + + it('does not send notification if shouldActuallySendNotifications is false', async () => { + notificationSender = new AppleNotificationSender(false); + + const notificationArguments: NotificationAlertArguments = { + title: 'Test notification', + body: 'This notification should not send', + } + + const result = await notificationSender.sendNotificationImmediately('1', notificationArguments); + + expect(http2.connect).not.toHaveBeenCalled(); + expect(result).toBe(true); + }); }); }); From ac65e23984bf63864020ff46e9b84f5a1d597aa1 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 10:11:17 -0700 Subject: [PATCH 12/15] add test for making a connection to the server --- .../senders/AppleNotificationSenderTests.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/notifications/senders/AppleNotificationSenderTests.test.ts b/test/notifications/senders/AppleNotificationSenderTests.test.ts index 9804e20..2fd2805 100644 --- a/test/notifications/senders/AppleNotificationSenderTests.test.ts +++ b/test/notifications/senders/AppleNotificationSenderTests.test.ts @@ -93,13 +93,25 @@ describe("AppleNotificationSender", () => { describe("sendNotificationImmediately", () => { it('makes the connection to the http server if the notification should be sent', async () => { + const notificationArguments: NotificationAlertArguments = { + title: 'Test notification', + body: 'This notification will send', + } + const result = await notificationSender.sendNotificationImmediately('1', notificationArguments); + + expect(http2.connect).toHaveBeenCalled(); + expect(result).toBe(true); }); it('throws an error if the bundle ID is not set correctly', async () => { }); + it('returns false if there is an error sending the notification', async () => { + + }); + it('does not send notification if shouldActuallySendNotifications is false', async () => { notificationSender = new AppleNotificationSender(false); From 20282c3cf91e3c3587cecba52b7a82da6b09724b Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 10:13:58 -0700 Subject: [PATCH 13/15] add test for APNs bundle ID --- .../senders/AppleNotificationSenderTests.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/notifications/senders/AppleNotificationSenderTests.test.ts b/test/notifications/senders/AppleNotificationSenderTests.test.ts index 2fd2805..aed404d 100644 --- a/test/notifications/senders/AppleNotificationSenderTests.test.ts +++ b/test/notifications/senders/AppleNotificationSenderTests.test.ts @@ -105,7 +105,18 @@ describe("AppleNotificationSender", () => { }); it('throws an error if the bundle ID is not set correctly', async () => { + process.env = { + ...process.env, + APNS_BUNDLE_ID: undefined, + } + const notificationArguments: NotificationAlertArguments = { + title: 'Test notification', + body: 'This notification will not send', + } + await expect(async () => { + await notificationSender.sendNotificationImmediately('1', notificationArguments); + }).rejects.toThrow(); }); it('returns false if there is an error sending the notification', async () => { From 2ccdba3d117699e41db55f92247981dc1d9b8af6 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 10:14:43 -0700 Subject: [PATCH 14/15] add test for failing error code --- .../senders/AppleNotificationSenderTests.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/notifications/senders/AppleNotificationSenderTests.test.ts b/test/notifications/senders/AppleNotificationSenderTests.test.ts index aed404d..9cbc0cb 100644 --- a/test/notifications/senders/AppleNotificationSenderTests.test.ts +++ b/test/notifications/senders/AppleNotificationSenderTests.test.ts @@ -120,7 +120,17 @@ describe("AppleNotificationSender", () => { }); it('returns false if there is an error sending the notification', async () => { + mockHttp2Connect(403); + const notificationArguments: NotificationAlertArguments = { + title: 'Test notification', + body: 'This notification will not send', + } + + const result = await notificationSender.sendNotificationImmediately('1', notificationArguments); + + expect(http2.connect).toHaveBeenCalled(); + expect(result).toBe(false); }); it('does not send notification if shouldActuallySendNotifications is false', async () => { From 7e3305247dabe4e3f395d5cfc6296c5e690d9236 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Mon, 24 Mar 2025 10:20:59 -0700 Subject: [PATCH 15/15] update notification scheduler tests with mock notification sender --- ... => ETANotificationSchedulerTests.test.ts} | 93 ++++--------------- 1 file changed, 18 insertions(+), 75 deletions(-) rename test/notifications/schedulers/{ETANotificationSchedulerTests.ts => ETANotificationSchedulerTests.test.ts} (69%) diff --git a/test/notifications/schedulers/ETANotificationSchedulerTests.ts b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts similarity index 69% rename from test/notifications/schedulers/ETANotificationSchedulerTests.ts rename to test/notifications/schedulers/ETANotificationSchedulerTests.test.ts index 3fb738b..60262c8 100644 --- a/test/notifications/schedulers/ETANotificationSchedulerTests.ts +++ b/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts @@ -4,11 +4,16 @@ import { UnoptimizedInMemoryRepository } from "../../../src/repositories/Unoptim import http2 from "http2"; import { IEta, IShuttle, IStop } from "../../../src/entities/entities"; import { addMockShuttleToRepository, addMockStopToRepository } from "../../testHelpers/repositorySetupHelpers"; -import EventEmitter = require("node:events"); +import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender"; jest.mock("http2"); +jest.mock("../../../src/notifications/senders/AppleNotificationSender"); -const sampleKeyBase64 = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR1RBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJIa3dkd0lCQVFRZ3NybVNBWklhZ09mQ1A4c0IKV2kyQ0JYRzFPbzd2MWJpc3BJWkN3SXI0UkRlZ0NnWUlLb1pJemowREFRZWhSQU5DQUFUWkh4VjJ3UUpMTUJxKwp5YSt5ZkdpM2cyWlV2NmhyZmUrajA4eXRla1BIalhTMHF6Sm9WRUx6S0hhNkVMOVlBb1pEWEJ0QjZoK2ZHaFhlClNPY09OYmFmCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"; +const MockAppleNotificationSender = AppleNotificationSender as jest.MockedClass; + +function mockNotificationSenderMethods(shouldSimulateNotificationSend: boolean) { + MockAppleNotificationSender.prototype.sendNotificationImmediately = jest.fn(async () => shouldSimulateNotificationSend); +} /** * Wait for a condition to become true until the timeout @@ -35,25 +40,6 @@ async function waitForMilliseconds(ms: number): Promise { await new Promise((resolve) => setTimeout(resolve, ms)); } -function mockHttp2Connect(status: number) { - class MockClient extends EventEmitter { - request = jest.fn((headers: any) => { - const mockRequest: any = new EventEmitter(); - mockRequest.setEncoding = jest.fn(); - mockRequest.write = jest.fn(); - mockRequest.end = jest.fn(() => { - setTimeout(() => { - mockRequest.emit('response', { ':status': status }); - }, 10); - }); - return mockRequest; - }); - - close() {}; - } - - (http2.connect as jest.Mock) = jest.fn(() => new MockClient()); -} describe("ETANotificationScheduler", () => { let repository: UnoptimizedInMemoryRepository @@ -61,38 +47,13 @@ describe("ETANotificationScheduler", () => { beforeEach(() => { repository = new UnoptimizedInMemoryRepository(); - notificationService = new ETANotificationScheduler(repository); - // Ensure that tests don't hit the server - process.env = { - ...process.env, - APNS_KEY_ID: "1", - APNS_TEAM_ID: "1", - APNS_BUNDLE_ID: "dev.bchen.ProjectInter", - APNS_PRIVATE_KEY: sampleKeyBase64, - }; + mockNotificationSenderMethods(true); + + const appleNotificationSender = new MockAppleNotificationSender(false); + notificationService = new ETANotificationScheduler(repository, appleNotificationSender); }); - beforeEach(() => { - mockHttp2Connect(200); - }); - - describe("reloadAPNsTokenIfTimePassed", () => { - it("reloads the token if token hasn't been generated yet", async () => { - notificationService.reloadAPNsTokenIfTimePassed(); - expect(notificationService.lastRefreshedTimeMs).toBeDefined(); - }); - - it("doesn't reload the token if last refreshed time is recent", async () => { - notificationService.reloadAPNsTokenIfTimePassed(); - const lastRefreshedTimeMs = notificationService.lastRefreshedTimeMs; - - notificationService.reloadAPNsTokenIfTimePassed(); - // Expect no change to have occurred - expect(lastRefreshedTimeMs).toEqual(notificationService.lastRefreshedTimeMs); - }); - }) - function generateNotificationDataAndEta(shuttle: IShuttle, stop: IStop) { const eta: IEta = { shuttleId: shuttle.id, @@ -173,7 +134,12 @@ describe("ETANotificationScheduler", () => { const shuttle = await addMockShuttleToRepository(repository, "1"); const stop = await addMockStopToRepository(repository, "1"); const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop) - mockHttp2Connect(403); + + mockNotificationSenderMethods(false); + notificationService = new ETANotificationScheduler( + repository, + new MockAppleNotificationSender(), + ) // Act await notificationService.scheduleNotification(notificationData1); @@ -188,29 +154,6 @@ describe("ETANotificationScheduler", () => { }); }); - describe('getAPNsFullUrlToUse', () => { - it('should return the production URL when APNS_IS_PRODUCTION is set to "1"', () => { - process.env.APNS_IS_PRODUCTION = "1"; - const deviceId = 'testDeviceId'; - const result = ETANotificationScheduler.getAPNsFullUrlToUse(deviceId); - - const { fullUrl, host, path } = result; - expect(fullUrl).toBe(`https://api.push.apple.com/3/device/${deviceId}`); - expect(host).toBe("https://api.push.apple.com"); - expect(path).toBe(`/3/device/${deviceId}`); - }); - - it('should return the sandbox URL when APNS_IS_PRODUCTION is set to something other than 1', () => { - process.env.APNS_IS_PRODUCTION = "0"; - const deviceId = 'testDeviceId'; - const result = ETANotificationScheduler.getAPNsFullUrlToUse(deviceId); - - const { fullUrl, host, path } = result; - expect(fullUrl).toBe(`https://api.development.push.apple.com/3/device/${deviceId}`); - expect(host).toBe("https://api.development.push.apple.com"); - expect(path).toBe(`/3/device/${deviceId}`); - }); - }); describe("cancelNotification", () => { it("stops notification from sending to given shuttle/stop ID", async () => { @@ -236,7 +179,7 @@ describe("ETANotificationScheduler", () => { // Arrange const shuttle1 = await addMockShuttleToRepository(repository, "1"); const stop = await addMockStopToRepository(repository, "1"); - const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle1, stop); + const { notificationData1 } = generateNotificationDataAndEta(shuttle1, stop); await notificationService.scheduleNotification(notificationData1); const shuttle2 = {