Files
project-inter-server/test/notifications/schedulers/ETANotificationSchedulerTests.test.ts
2025-03-27 10:23:30 -07:00

186 lines
6.8 KiB
TypeScript

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";
jest.mock("http2");
jest.mock("../../../src/notifications/senders/AppleNotificationSender");
const MockAppleNotificationSender = AppleNotificationSender as jest.MockedClass<typeof AppleNotificationSender>;
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<void> {
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,
new InMemoryNotificationRepository(),
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 InMemoryNotificationRepository(),
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);
});
});
});