mirror of
https://github.com/brendan-ch/project-inter-server.git
synced 2026-04-17 07:50:31 +00:00
243 lines
8.6 KiB
TypeScript
243 lines
8.6 KiB
TypeScript
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
import { NotificationService } from "../../src/services/NotificationService";
|
|
import { UnoptimizedInMemoryRepository } from "../../src/repositories/UnoptimizedInMemoryRepository";
|
|
import fs from "fs";
|
|
import http2 from "http2";
|
|
import { IEta, IShuttle, IStop } from "../../src/entities/entities";
|
|
import { addMockShuttleToRepository, addMockStopToRepository } from "../testHelpers/repositorySetupHelpers";
|
|
import EventEmitter = require("node:events");
|
|
|
|
jest.mock("fs");
|
|
jest.mock("http2");
|
|
|
|
const sampleKey = `-----BEGIN PRIVATE KEY-----
|
|
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgsrmSAZIagOfCP8sB
|
|
Wi2CBXG1Oo7v1bispIZCwIr4RDegCgYIKoZIzj0DAQehRANCAATZHxV2wQJLMBq+
|
|
ya+yfGi3g2ZUv6hrfe+j08ytekPHjXS0qzJoVELzKHa6EL9YAoZDXBtB6h+fGhXe
|
|
SOcONbaf
|
|
-----END PRIVATE KEY-----`
|
|
|
|
/**
|
|
* 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));
|
|
}
|
|
|
|
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("NotificationService", () => {
|
|
let repository: UnoptimizedInMemoryRepository
|
|
let notificationService: NotificationService;
|
|
|
|
beforeEach(() => {
|
|
repository = new UnoptimizedInMemoryRepository();
|
|
notificationService = new NotificationService(repository);
|
|
|
|
// Ensure that tests don't hit the server
|
|
process.env = {
|
|
...process.env,
|
|
APNS_KEY_ID: "1",
|
|
APNS_TEAM_ID: "1",
|
|
APNS_KEY_PATH: "./dummy-path.p8",
|
|
APNS_BUNDLE_ID: "dev.bchen.ProjectInter"
|
|
};
|
|
|
|
(fs.readFileSync as jest.Mock).mockReturnValue(sampleKey);
|
|
});
|
|
|
|
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,
|
|
stopId: stop.id,
|
|
secondsRemaining: 120,
|
|
};
|
|
|
|
const notificationData1 = {
|
|
deviceId: "1",
|
|
shuttleId: eta.shuttleId,
|
|
stopId: eta.stopId,
|
|
}
|
|
const notificationData2 = {
|
|
...notificationData1,
|
|
deviceId: "2",
|
|
}
|
|
return { eta, notificationData1, notificationData2 };
|
|
}
|
|
|
|
describe("scheduleNotification", () => {
|
|
it("schedules the notification", async () => {
|
|
// arrange
|
|
const notificationData = {
|
|
deviceId: "1",
|
|
shuttleId: "1",
|
|
stopId: "1"
|
|
};
|
|
|
|
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);
|
|
eta.secondsRemaining = notificationService.secondsThresholdForNotificationToFire + 100;
|
|
|
|
// 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)
|
|
mockHttp2Connect(403);
|
|
|
|
// 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('getAPNsFullUrlToUse', () => {
|
|
it('should return the production URL when NODE_ENV is set to "production"', () => {
|
|
process.env.NODE_ENV = 'production';
|
|
const deviceId = 'testDeviceId';
|
|
const result = NotificationService.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 NODE_ENV is not set to "production"', () => {
|
|
process.env.NODE_ENV = 'development';
|
|
const deviceId = 'testDeviceId';
|
|
const result = NotificationService.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 () => {
|
|
// 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);
|
|
});
|
|
});
|
|
});
|