diff --git a/src/fixtures/notifications.ts b/src/fixtures/notifications.ts new file mode 100644 index 000000000..637c8df8b --- /dev/null +++ b/src/fixtures/notifications.ts @@ -0,0 +1,52 @@ +const CREATED_TIME = new Date() +const READ_TIME = new Date() +const OLD_READ_TIME = new Date("1995-12-17T03:24:00") + +export const normalPriorityUnreadNotification = { + message: "low priority unread notification", + link: "google.com", + sourceUsername: "user", + type: "sent_request", + firstReadTime: null, + priority: 2, + createdAt: CREATED_TIME, +} + +export const normalPriorityReadNotification = { + ...normalPriorityUnreadNotification, + message: "low priority read notification", + firstReadTime: READ_TIME, +} + +export const highPriorityUnreadNotification = { + ...normalPriorityUnreadNotification, + message: "high priority unread notification", + priority: 1, +} + +export const highPriorityReadNotification = { + ...normalPriorityReadNotification, + message: "high priority read notification", + priority: 1, +} + +export const normalPriorityOldReadNotification = { + ...normalPriorityReadNotification, + message: "low priority old notification", + firstReadTime: OLD_READ_TIME, +} + +export const highPriorityOldReadNotification = { + ...highPriorityReadNotification, + message: "high priority old notification", + firstReadTime: OLD_READ_TIME, +} + +export const formatNotification = (notification: any) => ({ + message: notification.message, + createdAt: CREATED_TIME.toISOString(), + link: notification.link, + isRead: !!notification.firstReadTime, + sourceUsername: notification.sourceUsername, + type: notification.type, +}) diff --git a/src/integration/Notifications.spec.ts b/src/integration/Notifications.spec.ts new file mode 100644 index 000000000..ac2ee5901 --- /dev/null +++ b/src/integration/Notifications.spec.ts @@ -0,0 +1,438 @@ +import express from "express" +import request from "supertest" + +import { NotificationsRouter as _NotificationsRouter } from "@routes/v2/authenticatedSites/notifications" + +import { Notification, Repo, Site, SiteMember, User } from "@database/models" +import { generateRouter } from "@fixtures/app" +import UserSessionData from "@root/classes/UserSessionData" +import { + formatNotification, + highPriorityOldReadNotification, + highPriorityReadNotification, + highPriorityUnreadNotification, + normalPriorityOldReadNotification, + normalPriorityReadNotification, + normalPriorityUnreadNotification, +} from "@root/fixtures/notifications" +import { + mockEmail, + mockIsomerUserId, + mockSiteName, +} from "@root/fixtures/sessionData" +import { SitesRouter as _SitesRouter } from "@root/routes/v2/authenticated/sites" +import { notificationsService } from "@services/identity" + +const MOCK_SITE = "mockSite" +const MOCK_SITE_ID = "1" +const MOCK_SITE_MEMBER_ID = "1" + +const notificationsRouter = new _NotificationsRouter({ notificationsService }) +const notificationsSubrouter = notificationsRouter.getRouter() + +// Set up express with defaults and use the router under test +const subrouter = express() +// As we set certain properties on res.locals when the user signs in using github +// In order to do integration testing, we must expose a middleware +// that allows us to set this properties also +subrouter.use((req, res, next) => { + const userSessionData = new UserSessionData({ + isomerUserId: mockIsomerUserId, + email: mockEmail, + }) + res.locals.userSessionData = userSessionData + next() +}) +subrouter.use(notificationsSubrouter) +const app = generateRouter(subrouter) + +describe("Notifications Router", () => { + const MOCK_ADDITIONAL_USER_ID = "2" + const MOCK_ADDITIONAL_SITE_ID = "2" + const MOCK_ADDITIONAL_SITE_MEMBER_ID = "2" + const MOCK_ANOTHER_SITE_MEMBER_ID = "3" + + beforeAll(async () => { + // Set up User and Site table entries + await User.create({ + id: mockIsomerUserId, + }) + await User.create({ + id: MOCK_ADDITIONAL_USER_ID, + }) + await Site.create({ + id: MOCK_SITE_ID, + name: MOCK_SITE, + apiTokenName: "token", + jobStatus: "READY", + siteStatus: "LAUNCHED", + creatorId: mockIsomerUserId, + }) + await SiteMember.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + role: "ADMIN", + id: MOCK_SITE_MEMBER_ID, + }) + await Repo.create({ + name: mockSiteName, + url: "url", + siteId: MOCK_SITE_ID, + }) + await SiteMember.create({ + userId: MOCK_ADDITIONAL_USER_ID, + siteId: MOCK_SITE_ID, + role: "ADMIN", + id: MOCK_ADDITIONAL_SITE_MEMBER_ID, + }) + await Site.create({ + id: MOCK_ADDITIONAL_SITE_ID, + name: MOCK_SITE, + apiTokenName: "token", + jobStatus: "READY", + siteStatus: "LAUNCHED", + creatorId: mockIsomerUserId, + }) + await SiteMember.create({ + userId: mockIsomerUserId, + siteId: MOCK_ADDITIONAL_SITE_ID, + role: "ADMIN", + id: MOCK_ANOTHER_SITE_MEMBER_ID, + }) + await Repo.create({ + name: `${mockSiteName}2`, + url: "url", + siteId: MOCK_ADDITIONAL_SITE_ID, + }) + }) + + afterAll(async () => { + await Notification.sync({ force: true }) + await SiteMember.sync({ force: true }) + await Site.sync({ force: true }) + await User.sync({ force: true }) + await Repo.sync({ force: true }) + }) + + describe("GET /", () => { + afterEach(async () => { + // Clean up so that different tests using + // the same notifications don't interfere with each other + await Notification.sync({ force: true }) + }) + it("should return sorted list of most recent notifications if there are no unread", async () => { + // Arrange + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_SITE_MEMBER_ID, + ...highPriorityOldReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_SITE_MEMBER_ID, + ...highPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityOldReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityOldReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityReadNotification, + }) + await Notification.create({ + userId: MOCK_ADDITIONAL_USER_ID, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_ADDITIONAL_SITE_MEMBER_ID, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_ADDITIONAL_SITE_ID, + siteMemberId: MOCK_ANOTHER_SITE_MEMBER_ID, + ...normalPriorityUnreadNotification, + }) + // Notifications with different user or site are not returned + const expected = [ + highPriorityReadNotification, + normalPriorityReadNotification, + normalPriorityReadNotification, + normalPriorityReadNotification, + highPriorityOldReadNotification, + normalPriorityOldReadNotification, + ].map((notification) => formatNotification(notification)) + + // Act + const actual = await request(app).get("/") + + // Assert + expect(actual.body).toMatchObject(expected) + }) + + it("should return only unread notifications if there are any", async () => { + // Arrange + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_SITE_MEMBER_ID, + ...highPriorityOldReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_SITE_MEMBER_ID, + ...highPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityOldReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...highPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...highPriorityUnreadNotification, + }) + await Notification.create({ + userId: MOCK_ADDITIONAL_USER_ID, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_ADDITIONAL_SITE_MEMBER_ID, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_ADDITIONAL_SITE_ID, + siteMemberId: MOCK_ANOTHER_SITE_MEMBER_ID, + ...normalPriorityUnreadNotification, + }) + const expected = [ + highPriorityUnreadNotification, + highPriorityUnreadNotification, + normalPriorityUnreadNotification, + normalPriorityUnreadNotification, + normalPriorityUnreadNotification, + normalPriorityUnreadNotification, + ].map((notification) => formatNotification(notification)) + + // Act + const actual = await request(app).get("/") + + // Assert + expect(actual.body).toMatchObject(expected) + }) + }) + + describe("GET /allNotifications", () => { + afterEach(async () => { + // Clean up so that different tests using + // the same notifications don't interfere with each other + await Notification.sync({ force: true }) + }) + it("should return sorted list of all notifications", async () => { + // Arrange + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_SITE_MEMBER_ID, + ...highPriorityOldReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_SITE_MEMBER_ID, + ...highPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityOldReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...highPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: MOCK_ADDITIONAL_USER_ID, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_ADDITIONAL_SITE_MEMBER_ID, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_ADDITIONAL_SITE_ID, + siteMemberId: MOCK_ANOTHER_SITE_MEMBER_ID, + ...normalPriorityUnreadNotification, + }) + // Notifications with different user or site are not returned + const expected = [ + highPriorityUnreadNotification, + normalPriorityUnreadNotification, + highPriorityReadNotification, + normalPriorityReadNotification, + normalPriorityReadNotification, + highPriorityOldReadNotification, + normalPriorityOldReadNotification, + ].map((notification) => formatNotification(notification)) + + // Act + const actual = await request(app).get("/allNotifications") + + // Assert + expect(actual.body).toMatchObject(expected) + }) + }) + + describe("POST /", () => { + afterEach(async () => { + // Clean up so that different tests using + // the same notifications don't interfere with each other + await Notification.sync({ force: true }) + }) + it("should mark all notifications from the user as read", async () => { + // Arrange + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_SITE_MEMBER_ID, + ...highPriorityOldReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_SITE_MEMBER_ID, + ...highPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityOldReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...highPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityReadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_SITE_ID, + siteMemberId: 1, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: MOCK_ADDITIONAL_USER_ID, + siteId: MOCK_SITE_ID, + siteMemberId: MOCK_ADDITIONAL_SITE_MEMBER_ID, + ...normalPriorityUnreadNotification, + }) + await Notification.create({ + userId: mockIsomerUserId, + siteId: MOCK_ADDITIONAL_SITE_ID, + siteMemberId: MOCK_ANOTHER_SITE_MEMBER_ID, + ...normalPriorityUnreadNotification, + }) + const expected = 200 + + // Act + const actual = await request(app).post("/").send({}) + + // Assert + expect(actual.statusCode).toBe(expected) + expect( + await Notification.findAll({ + where: { siteMemberId: 1, first_read_time: null }, + }) + ).toEqual([]) + }) + }) +}) diff --git a/src/integration/Sites.spec.ts b/src/integration/Sites.spec.ts index 8bb86e77e..865881bdf 100644 --- a/src/integration/Sites.spec.ts +++ b/src/integration/Sites.spec.ts @@ -155,6 +155,15 @@ describe("Sites Router", () => { where: { userId: mockIsomerUserId }, }) }) + + afterAll(async () => { + await IsomerAdmin.sync({ force: true }) + await SiteMember.sync({ force: true }) + await Site.sync({ force: true }) + await User.sync({ force: true }) + await Repo.sync({ force: true }) + }) + it("should return list of only sites available to email user", async () => { // Arrange const expected = { diff --git a/src/integration/Users.spec.ts b/src/integration/Users.spec.ts index 6634c4ad8..0d5a4e630 100644 --- a/src/integration/Users.spec.ts +++ b/src/integration/Users.spec.ts @@ -68,6 +68,11 @@ describe("Users Router", () => { mockAxios.reset() }) + afterAll(async () => { + await User.sync({ force: true }) + await Whitelist.sync({ force: true }) + }) + describe("/email/otp", () => { afterEach(async () => { // Clean up so that different tests using diff --git a/src/routes/v2/authenticatedSites/__tests__/Notifications.spec.ts b/src/routes/v2/authenticatedSites/__tests__/Notifications.spec.ts new file mode 100644 index 000000000..060d9cbf5 --- /dev/null +++ b/src/routes/v2/authenticatedSites/__tests__/Notifications.spec.ts @@ -0,0 +1,109 @@ +import express from "express" +import request from "supertest" + +import { attachReadRouteHandlerWrapper } from "@middleware/routeHandler" + +import { NotificationsRouter as _NotificationsRouter } from "@routes/v2/authenticatedSites/notifications" + +import { generateRouter } from "@fixtures/app" +import { mockSiteName, mockIsomerUserId } from "@fixtures/sessionData" +import NotificationsService from "@services/identity/NotificationsService" + +describe("Notifications Router", () => { + const mockNotificationsService = { + listRecent: jest.fn(), + listAll: jest.fn(), + markNotificationsAsRead: jest.fn(), + } + + const NotificationsRouter = new _NotificationsRouter({ + notificationsService: (mockNotificationsService as unknown) as NotificationsService, + }) + + const subrouter = express() + + // We can use read route handler here because we don't need to lock the repo + subrouter.get( + "/:siteName/notifications/", + attachReadRouteHandlerWrapper(NotificationsRouter.getRecentNotifications) + ) + subrouter.get( + "/:siteName/notifications/allNotifications", + attachReadRouteHandlerWrapper(NotificationsRouter.getAllNotifications) + ) + subrouter.post( + "/:siteName/notifications/", + attachReadRouteHandlerWrapper(NotificationsRouter.markNotificationsAsRead) + ) + + const app = generateRouter(subrouter) + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("getRecentNotifications", () => { + it("should call the underlying service when there is a GET request", async () => { + // Arrange + const mockNotificationsValue: never[] = [] + mockNotificationsService.listRecent.mockResolvedValueOnce( + mockNotificationsValue + ) + + // Act + const resp = await request(app) + .get(`/${mockSiteName}/notifications/`) + .expect(200) + + // Assert + expect(resp.body).toStrictEqual(mockNotificationsValue) + expect(mockNotificationsService.listRecent).toHaveBeenCalledWith({ + siteName: mockSiteName, + userId: mockIsomerUserId, + }) + }) + }) + + describe("getAllNotifications", () => { + it("should call the underlying service when there is a GET request", async () => { + // Arrange + const mockNotificationsValue: never[] = [] + mockNotificationsService.listAll.mockResolvedValueOnce( + mockNotificationsValue + ) + + // Act + const resp = await request(app) + .get(`/${mockSiteName}/notifications/allNotifications`) + .expect(200) + + // Assert + expect(resp.body).toStrictEqual(mockNotificationsValue) + expect(mockNotificationsService.listAll).toHaveBeenCalledWith({ + siteName: mockSiteName, + userId: mockIsomerUserId, + }) + }) + }) + + describe("markNotificationsAsRead", () => { + it("should call the underlying service when there is a POST request", async () => { + // Arrange + const mockRequestBody = {} + + // Act + await request(app) + .post(`/${mockSiteName}/notifications/`) + .send(mockRequestBody) + .expect(200) + + // Assert + expect( + mockNotificationsService.markNotificationsAsRead + ).toHaveBeenCalledWith({ + siteName: mockSiteName, + userId: mockIsomerUserId, + }) + }) + }) +}) diff --git a/src/services/identity/__tests__/NotificationsService.spec.ts b/src/services/identity/__tests__/NotificationsService.spec.ts new file mode 100644 index 000000000..611cd57ca --- /dev/null +++ b/src/services/identity/__tests__/NotificationsService.spec.ts @@ -0,0 +1,210 @@ +import { ModelStatic } from "sequelize/types" + +import { Notification, SiteMember } from "@root/database/models" +import { mockSiteName, mockUserId } from "@root/fixtures/identity" + +import _NotificationsService from "../NotificationsService" + +const MockRepository = { + findOne: jest.fn(), + findAll: jest.fn(), + update: jest.fn(), + create: jest.fn(), +} +const MockSiteMember = { + findOne: jest.fn(), +} + +const NotificationsService = new _NotificationsService({ + repository: (MockRepository as unknown) as ModelStatic, + siteMember: (MockSiteMember as unknown) as ModelStatic, +}) + +const mockNotifications = [ + { + message: "one", + createdAt: "2022-10-04 07:42:31.597857+00", + link: "link", + sourceUsername: "blah", + type: "type", + isRead: true, + }, + { + message: "two", + createdAt: "2022-10-04 07:42:31.597857+00", + link: "link", + sourceUsername: "blah", + type: "type", + isRead: true, + }, + { + message: "three", + createdAt: "2022-10-04 07:42:31.597857+00", + link: "link", + sourceUsername: "blah", + type: "type", + isRead: true, + }, + { + message: "four", + createdAt: "2022-10-04 07:42:31.597857+00", + link: "link", + sourceUsername: "blah", + type: "type", + isRead: true, + }, + { + message: "five", + createdAt: "2022-10-04 07:42:31.597857+00", + link: "link", + sourceUsername: "blah", + type: "type", + isRead: true, + }, + { + message: "six", + createdAt: "2022-10-04 07:42:31.597857+00", + link: "link", + sourceUsername: "blah", + type: "type", + isRead: true, + }, + { + message: "seven", + createdAt: "2022-10-04 07:42:31.597857+00", + link: "link", + sourceUsername: "blah", + type: "type", + isRead: true, + }, +] + +const mockNotificationsResponse = mockNotifications.map((notification) => ({ + ...notification, + firstReadTime: "yes", +})) + +describe("Notification Service", () => { + afterEach(() => jest.clearAllMocks()) + + describe("listRecent", () => { + afterEach(() => jest.clearAllMocks()) + it("should return the most recent 6 notifications by calling listRecent", async () => { + // Arrange + const expected = mockNotifications.slice(0, 6) + + MockRepository.findAll.mockResolvedValueOnce([]) + MockRepository.findAll.mockResolvedValueOnce( + mockNotificationsResponse.slice(0, 6) + ) + + // Act + const actual = NotificationsService.listRecent({ + userId: mockUserId, + siteName: mockSiteName, + }) + + // Assert + await expect(actual).resolves.toStrictEqual(expected) + expect(MockRepository.findAll).toHaveBeenCalledTimes(2) + }) + + it("should return the result directly if new notifications available", async () => { + // Arrange + const expected = mockNotifications.slice(0, 2) + MockRepository.findAll.mockResolvedValueOnce( + mockNotificationsResponse.slice(0, 2) + ) + + // Act + const actual = NotificationsService.listRecent({ + userId: mockUserId, + siteName: mockSiteName, + }) + + // Assert + await expect(actual).resolves.toStrictEqual(expected) + expect(MockRepository.findAll).toHaveBeenCalledTimes(1) + }) + }) + + it("should return all notifications with listAll", async () => { + // Arrange + const expected = mockNotifications + MockRepository.findAll.mockResolvedValueOnce(mockNotificationsResponse) + + // Act + const actual = NotificationsService.listAll({ + userId: mockUserId, + siteName: mockSiteName, + }) + + // Assert + await expect(actual).resolves.toStrictEqual(expected) + expect(MockRepository.findAll).toHaveBeenCalledTimes(1) + }) + + it("should update all notifications with markNotificationsAsRead", async () => { + // Arrange + MockSiteMember.findOne.mockResolvedValueOnce({ id: mockUserId }) + MockRepository.update.mockResolvedValueOnce({}) + + // Act + const actual = NotificationsService.markNotificationsAsRead({ + userId: mockUserId, + siteName: mockSiteName, + }) + + // Assert + await expect(actual).resolves.not.toThrow() + expect(MockSiteMember.findOne).toHaveBeenCalledTimes(1) + expect(MockRepository.update).toHaveBeenCalledTimes(1) + }) + + describe("create", () => { + it("should create a new notification if no similar one exists", async () => { + // Arrange + MockSiteMember.findOne.mockResolvedValueOnce({ id: mockUserId }) + MockRepository.findOne.mockResolvedValueOnce(null) + + // Act + const actual = NotificationsService.create({ + userId: mockUserId, + siteName: mockSiteName, + link: "link", + notificationType: "sent_request", + notificationSourceUsername: "user", + }) + + // Assert + await expect(actual).resolves.not.toThrow() + expect(MockSiteMember.findOne).toHaveBeenCalledTimes(1) + expect(MockRepository.findOne).toHaveBeenCalledTimes(1) + expect(MockRepository.create).toHaveBeenCalledTimes(1) + }) + + it("should update an existing notification if a similar one exists", async () => { + // Arrange + const notificationUpdate = jest.fn() + MockSiteMember.findOne.mockResolvedValueOnce({ id: mockUserId }) + MockRepository.findOne.mockResolvedValueOnce({ + update: notificationUpdate, + }) + + // Act + const actual = NotificationsService.create({ + userId: mockUserId, + siteName: mockSiteName, + link: "link", + notificationType: "sent_request", + notificationSourceUsername: "user", + }) + + // Assert + await expect(actual).resolves.not.toThrow() + expect(MockSiteMember.findOne).toHaveBeenCalledTimes(1) + expect(MockRepository.findOne).toHaveBeenCalledTimes(1) + expect(notificationUpdate).toHaveBeenCalledTimes(1) + }) + }) +})