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"; import { InMemoryNotificationRepository } from "../../../src/repositories/InMemoryNotificationRepository"; import { NotificationRepository } from "../../../src/repositories/NotificationRepository"; 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 shuttleRepository: UnoptimizedInMemoryShuttleRepository let notificationService: ETANotificationScheduler; let notificationRepository: NotificationRepository; beforeEach(() => { shuttleRepository = new UnoptimizedInMemoryShuttleRepository(); notificationRepository = new InMemoryNotificationRepository(); mockNotificationSenderMethods(true); const appleNotificationSender = new MockAppleNotificationSender(false); notificationService = new ETANotificationScheduler( shuttleRepository, notificationRepository, 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("etaSubscriberCallback", () => { it("sends and clears correct notification after ETA changed", async () => { // Arrange const shuttle = await addMockShuttleToRepository(shuttleRepository, "1"); const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1, notificationData2 } = generateNotificationDataAndEta(shuttle, stop); // Act 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(() => !notificationRepository.isNotificationScheduled(notificationData1)); const isFirstNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData1); const isSecondNotificationScheduled = await notificationRepository.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(shuttleRepository, "1"); const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop); notificationData1.secondsThreshold = eta.secondsRemaining - 10; // Act await notificationRepository.addOrUpdateNotification(notificationData1); await shuttleRepository.addOrUpdateEta(eta); // Assert await waitForMilliseconds(500); const isNotificationScheduled = await notificationRepository.isNotificationScheduled(notificationData1); expect(isNotificationScheduled).toBe(true); }); it("leaves notification in array if delivery unsuccessful", async () => { // Arrange const shuttle = await addMockShuttleToRepository(shuttleRepository, "1"); const stop = await addMockStopToRepository(shuttleRepository, "1"); const { eta, notificationData1 } = generateNotificationDataAndEta(shuttle, stop) mockNotificationSenderMethods(false); notificationService = new ETANotificationScheduler( shuttleRepository, new InMemoryNotificationRepository(), new MockAppleNotificationSender(), ) // Act 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 = await notificationRepository.isNotificationScheduled(notificationData1); expect(isNotificationScheduled).toBe(true); }); }); });