Move all tests to subdirectories underneath code to be tested

This commit is contained in:
2025-07-31 22:35:49 -04:00
parent 0fd8de13f9
commit b7299b8359
20 changed files with 79 additions and 79 deletions

View File

@@ -0,0 +1,149 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { ETANotificationScheduler } from "../ETANotificationScheduler";
import { UnoptimizedInMemoryShuttleRepository } from "../../../repositories/shuttle/UnoptimizedInMemoryShuttleRepository";
import { IEta, IShuttle, IStop } from "../../../entities/ShuttleRepositoryEntities";
import { addMockShuttleToRepository, addMockStopToRepository } from "../../../../test/testHelpers/repositorySetupHelpers";
import { AppleNotificationSender } from "../../senders/AppleNotificationSender";
import { InMemoryNotificationRepository } from "../../../repositories/notifications/InMemoryNotificationRepository";
import { NotificationRepository } from "../../../repositories/notifications/NotificationRepository";
jest.mock("http2");
jest.mock("../../senders/AppleNotificationSender");
const MockAppleNotificationSender = AppleNotificationSender as jest.MockedClass<typeof AppleNotificationSender>;
function mockNotificationSenderMethods(shouldSimulateNotificationSend: boolean) {
MockAppleNotificationSender.prototype.sendNotificationImmediately = jest.fn(async () => shouldSimulateNotificationSend);
}
/**
* 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 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,
"1",
);
notificationService.startListeningForUpdates();
});
function generateNotificationDataAndEta(shuttle: IShuttle, stop: IStop) {
const eta: IEta = {
shuttleId: shuttle.id,
stopId: stop.id,
secondsRemaining: 120,
systemId: "1",
updatedTime: new Date(),
};
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
// Wait for the callback to actually be called
await waitForMilliseconds(1000);
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)
// replace the old notification scheduler with a new one
// detach the old callback method from the shuttle repo
notificationService.stopListeningForUpdates();
// replace the notification repository with a fresh one too
const notificationRepository = new InMemoryNotificationRepository();
mockNotificationSenderMethods(false);
const updatedNotificationSender = new MockAppleNotificationSender(false);
notificationService = new ETANotificationScheduler(
shuttleRepository,
notificationRepository,
updatedNotificationSender,
"1",
);
notificationService.startListeningForUpdates();
// 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);
});
});
});

View File

@@ -0,0 +1,190 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import http2 from "http2";
import { EventEmitter } from "node:events";
import {
AppleNotificationSender,
NotificationAlertArguments
} from "../AppleNotificationSender";
import { ClientHttp2Session } from "node:http2";
jest.mock("http2");
const sampleKeyBase64 = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR1RBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJIa3dkd0lCQVFRZ3NybVNBWklhZ09mQ1A4c0IKV2kyQ0JYRzFPbzd2MWJpc3BJWkN3SXI0UkRlZ0NnWUlLb1pJemowREFRZWhSQU5DQUFUWkh4VjJ3UUpMTUJxKwp5YSt5ZkdpM2cyWlV2NmhyZmUrajA4eXRla1BIalhTMHF6Sm9WRUx6S0hhNkVMOVlBb1pEWEJ0QjZoK2ZHaFhlClNPY09OYmFmCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K";
class MockClient extends EventEmitter {
constructor(
private status: number,
) {
super()
}
request = jest.fn((_) => {
const mockRequest: any = new EventEmitter();
mockRequest.setEncoding = jest.fn();
mockRequest.write = jest.fn();
mockRequest.end = jest.fn(() => {
setTimeout(() => {
mockRequest.emit('response', { ':status': this.status });
}, 10);
});
return mockRequest;
});
close = jest.fn(() => {});
}
function mockHttp2Connect(status: number) {
(http2.connect as jest.Mock) = jest.fn(() => new MockClient(status));
}
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);
});
});
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", () => {
it('makes the connection to the http server if sending a notification for the first time', 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('reuses the existing connection if sending another notification', async () => {
const notificationArguments: NotificationAlertArguments = {
title: 'Test notification',
body: 'This notification will send',
}
const result1 = await notificationSender.sendNotificationImmediately('1', notificationArguments);
const result2 = await notificationSender.sendNotificationImmediately('1', notificationArguments);
expect(http2.connect).toHaveBeenCalledTimes(1);
expect(result1).toBe(true);
expect(result2).toBe(true);
});
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 () => {
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 () => {
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);
});
it("registers a handler to close the connection if `close` event fired", async () => {
const connectionCloseEvents = ['close', 'goaway', 'error', 'timeout'];
await Promise.all(connectionCloseEvents.map(async (event) => {
const mockClient = new MockClient(200);
notificationSender = new AppleNotificationSender(true, mockClient as unknown as ClientHttp2Session);
const notificationArguments: NotificationAlertArguments = {
title: 'Test notification',
body: ''
};
await notificationSender.sendNotificationImmediately('1', notificationArguments);
mockClient.emit(event);
expect(mockClient.close).toHaveBeenCalled();
}));
});
});
});