From 6c8adf85942c220164560b278c73a6683dc66267 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 10 Apr 2026 10:39:04 -0700 Subject: [PATCH] add tests for maintenancewindowsercie --- .../services/maintenanceWindowService.test.ts | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 server/test/unit/services/maintenanceWindowService.test.ts diff --git a/server/test/unit/services/maintenanceWindowService.test.ts b/server/test/unit/services/maintenanceWindowService.test.ts new file mode 100644 index 000000000..302ff128c --- /dev/null +++ b/server/test/unit/services/maintenanceWindowService.test.ts @@ -0,0 +1,252 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { MaintenanceWindowService } from "../../../src/service/business/maintenanceWindowService.ts"; +import type { IMaintenanceWindowsRepository } from "../../../src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts"; +import type { IMonitorsRepository } from "../../../src/repositories/monitors/IMonitorsRepository.ts"; +import type { MaintenanceWindow } from "../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const createMaintenanceWindowsRepo = () => + ({ + create: jest.fn().mockResolvedValue(makeWindow()), + findById: jest.fn().mockResolvedValue(makeWindow()), + findByMonitorId: jest.fn().mockResolvedValue([makeWindow()]), + findByTeamId: jest.fn().mockResolvedValue([makeWindow()]), + updateById: jest.fn().mockResolvedValue(makeWindow()), + deleteById: jest.fn().mockResolvedValue(makeWindow()), + countByTeamId: jest.fn().mockResolvedValue(1), + }) as unknown as jest.Mocked; + +const createMonitorsRepo = () => + ({ + findByIds: jest.fn().mockResolvedValue([ + { id: "mon-1", teamId: "team-1" }, + { id: "mon-2", teamId: "team-1" }, + ]), + }) as unknown as jest.Mocked; + +const createService = (overrides?: { + monitorsRepository?: ReturnType; + maintenanceWindowsRepository?: ReturnType; +}) => { + const monitorsRepository = overrides?.monitorsRepository ?? createMonitorsRepo(); + const maintenanceWindowsRepository = overrides?.maintenanceWindowsRepository ?? createMaintenanceWindowsRepo(); + const service = new MaintenanceWindowService({ monitorsRepository, maintenanceWindowsRepository }); + return { service, monitorsRepository, maintenanceWindowsRepository }; +}; + +const makeWindow = (overrides?: Partial): MaintenanceWindow => ({ + id: "mw-1", + monitorId: "mon-1", + teamId: "team-1", + active: true, + name: "Scheduled Maintenance", + duration: 60, + durationUnit: "minutes", + repeat: 0, + start: "2026-04-10T02:00:00Z", + end: "2026-04-10T03:00:00Z", + createdAt: "2026-01-01T00:00:00Z", + updatedAt: "2026-01-01T00:00:00Z", + ...overrides, +}); + +const defaultCreateParams = { + teamId: "team-1", + monitorIDs: ["mon-1", "mon-2"], + name: "Scheduled Maintenance", + active: true, + duration: 60, + durationUnit: "minutes" as const, + repeat: 0, + start: "2026-04-10T02:00:00Z", + end: "2026-04-10T03:00:00Z", +}; + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("MaintenanceWindowService", () => { + describe("serviceName", () => { + it("returns maintenanceWindowService from static property", () => { + expect(MaintenanceWindowService.SERVICE_NAME).toBe("maintenanceWindowService"); + }); + + it("returns maintenanceWindowService from instance getter", () => { + const { service } = createService(); + expect(service.serviceName).toBe("maintenanceWindowService"); + }); + }); + + // ── createMaintenanceWindow ────────────────────────────────────────────── + + describe("createMaintenanceWindow", () => { + it("creates a maintenance window for each monitor", async () => { + const { service, maintenanceWindowsRepository } = createService(); + + await service.createMaintenanceWindow(defaultCreateParams); + + expect(maintenanceWindowsRepository.create).toHaveBeenCalledTimes(2); + expect(maintenanceWindowsRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + teamId: "team-1", + monitorId: "mon-1", + name: "Scheduled Maintenance", + active: true, + duration: 60, + durationUnit: "minutes", + repeat: 0, + start: "2026-04-10T02:00:00Z", + end: "2026-04-10T03:00:00Z", + }) + ); + expect(maintenanceWindowsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ monitorId: "mon-2" })); + }); + + it("verifies monitor ownership via findByIds", async () => { + const { service, monitorsRepository } = createService(); + + await service.createMaintenanceWindow(defaultCreateParams); + + expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["mon-1", "mon-2"]); + }); + + it("throws 403 when a monitor belongs to a different team", async () => { + const monitorsRepository = createMonitorsRepo(); + (monitorsRepository.findByIds as jest.Mock).mockResolvedValue([ + { id: "mon-1", teamId: "team-1" }, + { id: "mon-2", teamId: "team-other" }, + ]); + const { service, maintenanceWindowsRepository } = createService({ monitorsRepository }); + + await expect(service.createMaintenanceWindow(defaultCreateParams)).rejects.toThrow( + "Unauthorized to create maintenance window for one or more monitors" + ); + expect(maintenanceWindowsRepository.create).not.toHaveBeenCalled(); + }); + + it("does not throw when all monitors belong to the team", async () => { + const { service } = createService(); + + await expect(service.createMaintenanceWindow(defaultCreateParams)).resolves.toBeUndefined(); + }); + + it("creates a single window when monitorIDs has one entry", async () => { + const { service, maintenanceWindowsRepository } = createService(); + + await service.createMaintenanceWindow({ ...defaultCreateParams, monitorIDs: ["mon-1"] }); + + expect(maintenanceWindowsRepository.create).toHaveBeenCalledTimes(1); + }); + }); + + // ── getMaintenanceWindowById ───────────────────────────────────────────── + + describe("getMaintenanceWindowById", () => { + it("delegates to repository with id and teamId", async () => { + const expected = makeWindow(); + const { service, maintenanceWindowsRepository } = createService(); + (maintenanceWindowsRepository.findById as jest.Mock).mockResolvedValue(expected); + + const result = await service.getMaintenanceWindowById({ id: "mw-1", teamId: "team-1" }); + + expect(result).toBe(expected); + expect(maintenanceWindowsRepository.findById).toHaveBeenCalledWith("mw-1", "team-1"); + }); + }); + + // ── getMaintenanceWindowsByTeamId ──────────────────────────────────────── + + describe("getMaintenanceWindowsByTeamId", () => { + it("returns windows and count", async () => { + const windows = [makeWindow()]; + const { service, maintenanceWindowsRepository } = createService(); + (maintenanceWindowsRepository.findByTeamId as jest.Mock).mockResolvedValue(windows); + (maintenanceWindowsRepository.countByTeamId as jest.Mock).mockResolvedValue(1); + + const result = await service.getMaintenanceWindowsByTeamId({ teamId: "team-1" }); + + expect(result).toEqual({ maintenanceWindows: windows, maintenanceWindowCount: 1 }); + }); + + it("defaults page to 0 and rowsPerPage to 10 when not provided", async () => { + const { service, maintenanceWindowsRepository } = createService(); + + await service.getMaintenanceWindowsByTeamId({ teamId: "team-1" }); + + expect(maintenanceWindowsRepository.findByTeamId).toHaveBeenCalledWith("team-1", 0, 10, undefined, undefined, undefined); + }); + + it("passes pagination and filter parameters through", async () => { + const { service, maintenanceWindowsRepository } = createService(); + + await service.getMaintenanceWindowsByTeamId({ + teamId: "team-1", + active: true, + page: 2, + rowsPerPage: 5, + field: "name", + order: "asc", + }); + + expect(maintenanceWindowsRepository.findByTeamId).toHaveBeenCalledWith("team-1", 2, 5, "name", "asc", true); + expect(maintenanceWindowsRepository.countByTeamId).toHaveBeenCalledWith("team-1", true); + }); + }); + + // ── getMaintenanceWindowsByMonitorId ───────────────────────────────────── + + describe("getMaintenanceWindowsByMonitorId", () => { + it("delegates to repository with monitorId and teamId", async () => { + const windows = [makeWindow()]; + const { service, maintenanceWindowsRepository } = createService(); + (maintenanceWindowsRepository.findByMonitorId as jest.Mock).mockResolvedValue(windows); + + const result = await service.getMaintenanceWindowsByMonitorId({ monitorId: "mon-1", teamId: "team-1" }); + + expect(result).toBe(windows); + expect(maintenanceWindowsRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "team-1"); + }); + }); + + // ── deleteMaintenanceWindow ────────────────────────────────────────────── + + describe("deleteMaintenanceWindow", () => { + it("delegates to repository and returns deleted window", async () => { + const deleted = makeWindow(); + const { service, maintenanceWindowsRepository } = createService(); + (maintenanceWindowsRepository.deleteById as jest.Mock).mockResolvedValue(deleted); + + const result = await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" }); + + expect(result).toBe(deleted); + expect(maintenanceWindowsRepository.deleteById).toHaveBeenCalledWith("mw-1", "team-1"); + }); + }); + + // ── editMaintenanceWindow ──────────────────────────────────────────────── + + describe("editMaintenanceWindow", () => { + it("delegates to repository with id, teamId, and body", async () => { + const updated = makeWindow({ name: "Updated Name" }); + const { service, maintenanceWindowsRepository } = createService(); + (maintenanceWindowsRepository.updateById as jest.Mock).mockResolvedValue(updated); + + const result = await service.editMaintenanceWindow({ id: "mw-1", teamId: "team-1", body: { name: "Updated Name" } }); + + expect(result).toBe(updated); + expect(maintenanceWindowsRepository.updateById).toHaveBeenCalledWith("mw-1", "team-1", { name: "Updated Name" }); + }); + + it("passes partial body fields through", async () => { + const { service, maintenanceWindowsRepository } = createService(); + + await service.editMaintenanceWindow({ + id: "mw-1", + teamId: "team-1", + body: { active: false, repeat: 3600000 }, + }); + + expect(maintenanceWindowsRepository.updateById).toHaveBeenCalledWith("mw-1", "team-1", { active: false, repeat: 3600000 }); + }); + }); +});