import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { ETANotificationScheduler } from "../../../src/notifications/schedulers/ETANotificationScheduler"; import { UnoptimizedInMemoryShuttleRepository } from "../../../src/repositories/UnoptimizedInMemoryShuttleRepository"; import http2 from "http2"; import { IEta, IShuttle, IStop } from "../../../src/entities/entities"; import { addMockShuttleToRepository, addMockStopToRepository } from "../../testHelpers/repositorySetupHelpers"; import { AppleNotificationSender } from "../../../src/notifications/senders/AppleNotificationSender"; jest.mock("http2"); jest.mock("../../../src/notifications/senders/AppleNotificationSender"); 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 * 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 */ async function waitForMilliseconds(ms: number): Promise { await new Promise((resolve) => setTimeout(resolve, ms)); } describe("ETANotificationScheduler", () => { let repository: UnoptimizedInMemoryShuttleRepository let notificationService: ETANotificationScheduler; beforeEach(() => { repository = new UnoptimizedInMemoryShuttleRepository(); mockNotificationSenderMethods(true); const appleNotificationSender = new MockAppleNotificationSender(false); notificationService = new ETANotificationScheduler(repository, appleNotificationSender); }); function generateNotificationDataAndEta(shuttle: IShuttle, stop: IStop) { const eta: IEta = { shuttleId: shuttle.id, stopId: stop.id, secondsRemaining: 120, }; const notificationData1 = { deviceId: "1", shuttleId: eta.shuttleId, stopId: eta.stopId, secondsThreshold: 240, } const notificationData2 = { ...notificationData1, deviceId: "2", secondsThreshold: 180, } 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); }); it("sends and clears correct notification after ETA changed", async () => { // Arrange const shuttle = await addMockShuttleToRepository(repository, "1"); const stop = await addMockStopToRepository(repository, "1"); const { eta, notificationData1, notificationData2 } = generateNotificationDataAndEta(shuttle, stop); // Act await notificationService.scheduleNotification(notificationData1); await notificationService.scheduleNotification(notificationData2); await repository.addOrUpdateEta(eta); // Assert // Because repository publisher calls subscriber without await // wait for the change to occur first await waitForCondition(() => !notificationService.isNotificationScheduled(notificationData1)); const isFirstNotificationScheduled = notificationService.isNotificationScheduled(notificationData1); const isSecondNotificationScheduled = notificationService.isNotificationScheduled(notificationData2); // No longer scheduled after being sent expect(isFirstNotificationScheduled).toBe(false); expect(isSecondNotificationScheduled).toBe(false); }); 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 { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop); notificationData1.secondsThreshold = eta.secondsRemaining - 10; // Act await notificationService.scheduleNotification(notificationData1); await repository.addOrUpdateEta(eta); // Assert await waitForMilliseconds(500); const isNotificationScheduled = notificationService.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 { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop) mockNotificationSenderMethods(false); notificationService = new ETANotificationScheduler( repository, new MockAppleNotificationSender(), ) // Act await notificationService.scheduleNotification(notificationData1); await repository.addOrUpdateEta(eta); // Assert // The notification should stay scheduled to be retried once // the ETA updates again await waitForMilliseconds(500); const isNotificationScheduled = notificationService.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); }); }); });