import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { InMemoryNotificationRepository } from "../../src/repositories/notifications/InMemoryNotificationRepository"; import { NotificationEvent, NotificationRepository } from "../../src/repositories/notifications/NotificationRepository"; import { RedisNotificationRepository } from "../../src/repositories/notifications/RedisNotificationRepository"; interface RepositoryHolder { name: string; factory(): Promise, teardown(): Promise, } class InMemoryRepositoryHolder implements RepositoryHolder { name = 'InMemoryNotificationRepository'; factory = async () => { return new InMemoryNotificationRepository(); } teardown = async () => {} } class RedisNotificationRepositoryHolder implements RepositoryHolder { repo: RedisNotificationRepository | undefined; name = 'RedisNotificationRepository'; factory = async () => { this.repo = new RedisNotificationRepository(); await this.repo.connect(); return this.repo; } teardown = async () => { if (this.repo) { await this.repo.clearAllData(); await this.repo.disconnect(); } } } const repositoryImplementations = [ new InMemoryRepositoryHolder(), new RedisNotificationRepositoryHolder(), ] describe.each(repositoryImplementations)('$name', (holder) => { let repo: NotificationRepository; beforeEach(async () => { repo = await holder.factory(); }); afterEach(async () => { await holder.teardown(); }) const notification = { deviceId: "device1", shuttleId: "shuttle1", stopId: "stop1", secondsThreshold: 180 }; describe("getAllNotificationsForShuttleAndStopId", () => { it("gets notifications correctly", async () => { await repo.addOrUpdateNotification(notification); const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); expect(result).toHaveLength(1); expect(result[0]).toEqual(notification); }); it("returns empty array if no notifications", async () => { const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); expect(result).toEqual([]); }); }); describe("getSecondsThresholdForNotificationIfExists", () => { it("gets the seconds threshold if exists", async () => { await repo.addOrUpdateNotification(notification); const result = await repo.getSecondsThresholdForNotificationIfExists({ deviceId: "device1", shuttleId: "shuttle1", stopId: "stop1" }); expect(result).toBe(180); }); it("returns null if there is no seconds threshold", async () => { const result = await repo.getSecondsThresholdForNotificationIfExists({ deviceId: "device1", shuttleId: "shuttle1", stopId: "stop1" }); expect(result).toBeNull(); }); }); describe("addOrUpdateNotification", () => { // Add/get flow is covered in getAllNotificationsForShuttleAndStopId it("updates the seconds threshold if the notification exists already", async () => { await repo.addOrUpdateNotification(notification); await repo.addOrUpdateNotification({...notification, secondsThreshold: 300}); const result = await repo.getSecondsThresholdForNotificationIfExists({ deviceId: "device1", shuttleId: "shuttle1", stopId: "stop1" }); expect(result).toBe(300); }); }); describe("deleteNotificationIfExists", () => { it("deletes the notification", async () => { await repo.addOrUpdateNotification(notification); await repo.deleteNotificationIfExists(notification); const result = await repo.getAllNotificationsForShuttleAndStopId("shuttle1", "stop1"); expect(result).toHaveLength(0); }); it("does nothing if there's no notification", async () => { await expect(repo.deleteNotificationIfExists({ deviceId: "device1", shuttleId: "shuttle1", stopId: "stop1" })).resolves.not.toThrow(); }); }); describe("subscribeToNotificationChanges", () => { it("calls subscribers when something is added", async () => { const mockCallback = jest.fn(); repo.subscribeToNotificationChanges(mockCallback); await repo.addOrUpdateNotification(notification); const expectedEvent: NotificationEvent = { event: 'addOrUpdate', notification, } expect(mockCallback).toHaveBeenCalledTimes(1); expect(mockCallback).toHaveBeenCalledWith(expectedEvent); }); it("calls subscribers when something is updated", async () => { const mockCallback = jest.fn(); repo.subscribeToNotificationChanges(mockCallback); await repo.addOrUpdateNotification(notification); const updatedNotification = { ...notification, secondsThreshold: notification.secondsThreshold + 60, }; await repo.addOrUpdateNotification(updatedNotification); const expectedEvent: NotificationEvent = { event: 'addOrUpdate', notification, } expect(mockCallback).toHaveBeenCalledTimes(2); expect(mockCallback).toHaveBeenCalledWith(expectedEvent); }); it("calls subscribers when something is deleted", async () => { await repo.addOrUpdateNotification(notification); const mockCallback = jest.fn(); repo.subscribeToNotificationChanges(mockCallback); await repo.deleteNotificationIfExists(notification); expect(mockCallback).toHaveBeenCalledTimes(1); const expectedEvent: NotificationEvent = { event: 'delete', notification, }; expect(mockCallback).toHaveBeenCalledWith(expectedEvent); }); }); describe("unsubscribeFromNotificationChanges", () => { it("stops calling subscribers when unsubscribed", async () => { const mockCallback = jest.fn(); repo.subscribeToNotificationChanges(mockCallback); await repo.addOrUpdateNotification(notification); repo.unsubscribeFromNotificationChanges(mockCallback); await repo.deleteNotificationIfExists(notification); expect(mockCallback).toHaveBeenCalledTimes(1); }); }); describe("isNotificationScheduled", () => { it("returns true if the notification is in the repo", async () => { await repo.addOrUpdateNotification(notification); const result = await repo.isNotificationScheduled(notification); expect(result).toBe(true); }); it("returns false if the notification isn't in the repo", async () => { const result = await repo.isNotificationScheduled(notification); expect(result).toBe(false); }) }); });