import { describe, expect, it, jest, beforeEach } from "@jest/globals"; import { MonitorService } from "../../../src/domain/monitors/monitor.service.ts"; import type { IChecksRepository } from "../../../src/domain/checks/check.repository.interface.ts"; import type { IGeoChecksRepository } from "../../../src/domain/geo-checks/geo-check.repository.interface.ts"; import type { IIncidentsRepository } from "../../../src/domain/incidents/incident.repository.interface.ts"; import type { IMonitorStatsRepository } from "../../../src/domain/monitor-stats/monitor-stats.repository.interface.ts"; import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts"; import type { IStatusPagesRepository } from "../../../src/domain/status-pages/status-page-repository.interface.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; const createMonitorsRepositoryMock = () => ({ create: jest.fn(), createMonitors: jest.fn(), findById: jest.fn(), findByTeamId: jest.fn(), findByTeamIdWithStats: jest.fn(), findByIds: jest.fn(), findMonitorCountByTeamIdAndType: jest.fn(), findMonitorsSummaryByTeamId: jest.fn(), updateById: jest.fn(), updateNotifications: jest.fn(), togglePauseById: jest.fn(), bulkTogglePause: jest.fn(), deleteById: jest.fn(), deleteByTeamId: jest.fn(), }) as unknown as IMonitorsRepository; const createChecksRepositoryMock = () => ({ findByDateRangeAndMonitorId: jest.fn(), deleteByMonitorId: jest.fn(), }) as unknown as IChecksRepository; const createMonitorStatsRepositoryMock = () => ({ findByMonitorId: jest.fn(), deleteByMonitorId: jest.fn(), }) as unknown as IMonitorStatsRepository; const createStatusPagesRepositoryMock = () => ({ removeMonitorFromStatusPages: jest.fn(), }) as unknown as IStatusPagesRepository; const createGeoChecksRepositoryMock = () => ({ findGroupedByMonitorIdAndDateRange: jest.fn(), deleteByMonitorId: jest.fn(), }) as unknown as IGeoChecksRepository; const createIncidentsRepositoryMock = () => ({ deleteByMonitorId: jest.fn(), }) as unknown as IIncidentsRepository; const createJobQueueMock = () => ({ addJob: jest.fn(), updateJob: jest.fn(), resumeJob: jest.fn(), pauseJob: jest.fn(), deleteJob: jest.fn(), }); const TEAM_ID = "team-1"; const MONITOR_ID = "monitor-1"; const USER_ID = "user-1"; const makeMonitor = (overrides: Record = {}) => ({ id: MONITOR_ID, teamId: TEAM_ID, userId: USER_ID, name: "Test Monitor", type: "http" as const, url: "https://example.com", isActive: true, interval: 60000, recentChecks: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), ...overrides, }); const createService = ( overrides: { monitorsRepository?: IMonitorsRepository; checksRepository?: IChecksRepository; monitorStatsRepository?: IMonitorStatsRepository; statusPagesRepository?: IStatusPagesRepository; geoChecksRepository?: IGeoChecksRepository; incidentsRepository?: IIncidentsRepository; jobQueue?: ReturnType; logger?: ReturnType; games?: Record; } = {} ) => { const jobQueue = overrides.jobQueue ?? createJobQueueMock(); const logger = overrides.logger ?? createMockLogger(); const service = new MonitorService({ scheduler: jobQueue as any, logger: logger as any, games: (overrides.games ?? { cs2: { name: "Counter-Strike 2" } }) as any, monitorsRepository: overrides.monitorsRepository ?? createMonitorsRepositoryMock(), checksRepository: overrides.checksRepository ?? createChecksRepositoryMock(), geoChecksRepository: overrides.geoChecksRepository ?? createGeoChecksRepositoryMock(), monitorStatsRepository: overrides.monitorStatsRepository ?? createMonitorStatsRepositoryMock(), statusPagesRepository: overrides.statusPagesRepository ?? createStatusPagesRepositoryMock(), incidentsRepository: overrides.incidentsRepository ?? createIncidentsRepositoryMock(), }); return { service, jobQueue, logger }; }; describe("MonitorService", () => { describe("serviceName", () => { it("returns 'MonitorService'", () => { const { service } = createService(); expect(service.serviceName).toBe("MonitorService"); }); }); describe("createMonitor", () => { it("creates a monitor and adds a job", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.create as jest.Mock).mockResolvedValue(monitor); const { service, jobQueue } = createService({ monitorsRepository }); await service.createMonitor(TEAM_ID, USER_ID, monitor as any); expect(monitorsRepository.create).toHaveBeenCalledWith(monitor, TEAM_ID, USER_ID); expect(jobQueue.addJob).toHaveBeenCalledWith(MONITOR_ID, monitor); }); it("throws when repository returns null", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.create as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository }); await expect(service.createMonitor(TEAM_ID, USER_ID, {} as any)).rejects.toThrow("Failed to create monitor"); }); }); describe("createMonitors", () => { it("creates multiple monitors and adds jobs for each", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitors = [makeMonitor({ id: "m1" }), makeMonitor({ id: "m2" })]; (monitorsRepository.createMonitors as jest.Mock).mockResolvedValue(monitors); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.createMonitors(monitors as any); expect(result).toHaveLength(2); expect(jobQueue.addJob).toHaveBeenCalledTimes(2); }); it("throws when repository returns empty array", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.createMonitors as jest.Mock).mockResolvedValue([]); const { service } = createService({ monitorsRepository }); await expect(service.createMonitors([makeMonitor()] as any)).rejects.toThrow("Failed to create monitors"); }); it("throws when repository returns null", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.createMonitors as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository }); await expect(service.createMonitors([makeMonitor()] as any)).rejects.toThrow("Failed to create monitors"); }); }); describe("addDemoMonitors", () => { it("creates demo monitors from JSON data and adds jobs", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const demoMonitors = [makeMonitor({ id: "d1", name: "Google" }), makeMonitor({ id: "d2", name: "Facebook" })]; (monitorsRepository.createMonitors as jest.Mock).mockResolvedValue(demoMonitors); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.addDemoMonitors({ userId: USER_ID, teamId: TEAM_ID }); expect(result).toHaveLength(2); expect(monitorsRepository.createMonitors).toHaveBeenCalledTimes(1); const callArg = (monitorsRepository.createMonitors as jest.Mock).mock.calls[0]![0] as any[]; expect(callArg[0]).toMatchObject({ userId: USER_ID, teamId: TEAM_ID, type: "http", interval: 60000 }); expect(jobQueue.addJob).toHaveBeenCalledTimes(2); }); }); describe("getUptimeDetailsById", () => { it("returns monitorData and monitorStats for an http monitor", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "http", groupedChecks: [{ _id: "2024-01-01", avgResponseTime: 100, totalChecks: 2 }], groupedUpChecks: [{ _id: "2024-01-01", totalChecks: 2, avgResponseTime: 90 }], groupedDownChecks: [{ _id: "2024-01-01", totalChecks: 0, avgResponseTime: 0 }], uptimePercentage: 0.99, avgResponseTime: 95, }); (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue({ monitorId: MONITOR_ID }); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); const result = await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" }); expect(result).toHaveProperty("monitorData"); expect(result).toHaveProperty("monitorStats"); expect(result.monitorData.monitor).toMatchObject({ id: MONITOR_ID }); }); it("supports all uptime monitor types: ping, docker, port, game, grpc, websocket", async () => { for (const monitorType of ["ping", "docker", "port", "game", "grpc", "websocket"] as const) { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor({ type: monitorType }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType, groupedChecks: [], groupedUpChecks: [], groupedDownChecks: [], uptimePercentage: 1, avgResponseTime: 50, }); (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); const result = await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "day" }); expect(result.monitorData.monitor.type).toBe(monitorType); } }); it("throws 404 when monitor not found", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository }); await expect(service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: "missing", dateRange: "recent" })).rejects.toThrow( "Monitor with ID missing not found." ); }); it("throws 400 for unsupported monitor type (hardware)", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor({ type: "hardware" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "hardware", groupedChecks: [], groupedUpChecks: [], groupedDownChecks: [], uptimePercentage: 0, avgResponseTime: 0, }); (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); await expect(service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" })).rejects.toThrow( "monitors are not supported for uptime details" ); }); it("throws 400 for unsupported monitor type (pagespeed)", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor({ type: "pagespeed" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "pagespeed", groupedChecks: [], groupedUpChecks: [], groupedDownChecks: [], uptimePercentage: 0, avgResponseTime: 0, }); (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); await expect(service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "week" })).rejects.toThrow( "monitors are not supported for uptime details" ); }); it("uses different date ranges correctly", async () => { for (const dateRange of ["recent", "day", "week", "month", "all"] as const) { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "http", groupedChecks: [], groupedUpChecks: [], groupedDownChecks: [], uptimePercentage: 1, avgResponseTime: 50, }); (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange }); expect(checksRepository.findByDateRangeAndMonitorId).toHaveBeenCalled(); } }); }); describe("getHardwareDetailsById", () => { it("returns hardware details for a hardware monitor", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor({ type: "hardware" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "hardware", aggregateData: { totalChecks: 10 }, upChecks: { totalChecks: 9 }, checks: [], }); (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue({ monitorId: MONITOR_ID }); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); const result = await service.getHardwareDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" }); expect(result.monitor).toMatchObject({ id: MONITOR_ID }); expect(result.stats).toHaveProperty("aggregateData"); expect(result.stats).toHaveProperty("upChecks"); expect(result.stats).toHaveProperty("checks"); expect(result.monitorStats).toMatchObject({ monitorId: MONITOR_ID }); }); it("throws 404 when monitor not found", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository }); await expect(service.getHardwareDetailsById({ teamId: TEAM_ID, monitorId: "missing", dateRange: "recent" })).rejects.toThrow( "Monitor with ID missing not found." ); }); it("throws 400 when monitor type is not hardware", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor({ type: "http" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const { service } = createService({ monitorsRepository }); await expect(service.getHardwareDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" })).rejects.toThrow( "monitors are not supported for hardware details" ); }); it("throws 500 when checksData monitorType is not hardware", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitor = makeMonitor({ type: "hardware" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "http" }); const { service } = createService({ monitorsRepository, checksRepository }); await expect(service.getHardwareDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" })).rejects.toThrow( "Unable to load hardware stats for this monitor" ); }); }); describe("getPageSpeedDetailsById", () => { it("returns pagespeed details for a pagespeed monitor", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor({ type: "pagespeed" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "pagespeed", groupedChecks: [{ _id: "2024-01-01", performance: 95 }], }); (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue({ monitorId: MONITOR_ID }); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); const result = await service.getPageSpeedDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" }); expect(result.monitorData.monitor).toMatchObject({ id: MONITOR_ID }); expect(result.monitorData.groupedChecks).toHaveLength(1); expect(result.monitorStats).toMatchObject({ monitorId: MONITOR_ID }); }); it("throws 404 when monitor not found", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository }); await expect(service.getPageSpeedDetailsById({ teamId: TEAM_ID, monitorId: "missing", dateRange: "recent" })).rejects.toThrow( "Monitor with ID missing not found." ); }); it("throws 400 when monitor type is not pagespeed", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor({ type: "http" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const { service } = createService({ monitorsRepository }); await expect(service.getPageSpeedDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" })).rejects.toThrow( "monitors are not supported for pagespeed details" ); }); it("throws 500 when checksData monitorType is not pagespeed", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitor = makeMonitor({ type: "pagespeed" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "http" }); const { service } = createService({ monitorsRepository, checksRepository }); await expect(service.getPageSpeedDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" })).rejects.toThrow( "Unable to load pagespeed stats for this monitor" ); }); it("handles pagespeed with different date range", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor({ type: "pagespeed" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "pagespeed", groupedChecks: [], }); (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); await service.getPageSpeedDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "week" }); expect(checksRepository.findByDateRangeAndMonitorId).toHaveBeenCalled(); }); }); describe("getGeoChecksByMonitorId", () => { it("throws 404 when monitor not found", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository }); await expect(service.getGeoChecksByMonitorId({ teamId: TEAM_ID, monitorId: "missing", dateRange: "recent" })).rejects.toThrow( "Monitor with ID missing not found." ); }); it("returns empty when monitor type does not support geo checks", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor({ type: "hardware", geoCheckEnabled: true }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const { service } = createService({ monitorsRepository }); const result = await service.getGeoChecksByMonitorId({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" }); expect(result).toEqual({ groupedGeoChecks: [] }); }); it("returns empty when geoCheckEnabled is false", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor({ type: "http", geoCheckEnabled: false }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const { service } = createService({ monitorsRepository }); const result = await service.getGeoChecksByMonitorId({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" }); expect(result).toEqual({ groupedGeoChecks: [] }); }); it("returns empty when geoCheckEnabled is undefined", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor({ type: "http", geoCheckEnabled: undefined }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const { service } = createService({ monitorsRepository }); const result = await service.getGeoChecksByMonitorId({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" }); expect(result).toEqual({ groupedGeoChecks: [] }); }); it("returns grouped geo checks when supported and enabled", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor({ type: "http", geoCheckEnabled: true }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const geoData = [{ continent: "NA", avgResponseTime: 50, totalChecks: 5 }]; (geoChecksRepository.findGroupedByMonitorIdAndDateRange as jest.Mock).mockResolvedValue(geoData); const { service } = createService({ monitorsRepository, geoChecksRepository }); const result = await service.getGeoChecksByMonitorId({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent", continents: ["NA"] as any, }); expect(result.groupedGeoChecks).toEqual(geoData); expect(geoChecksRepository.findGroupedByMonitorIdAndDateRange).toHaveBeenCalled(); }); it("works with ping monitor type", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor({ type: "ping", geoCheckEnabled: true }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (geoChecksRepository.findGroupedByMonitorIdAndDateRange as jest.Mock).mockResolvedValue([]); const { service } = createService({ monitorsRepository, geoChecksRepository }); const result = await service.getGeoChecksByMonitorId({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "day" }); expect(result.groupedGeoChecks).toEqual([]); }); }); describe("getMonitorById", () => { it("delegates to repository findById", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorById({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(result).toEqual(monitor); expect(monitorsRepository.findById).toHaveBeenCalledWith(MONITOR_ID, TEAM_ID); }); }); describe("getMonitorsByTeamId", () => { it("returns monitors from repository", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitors = [makeMonitor({ id: "m1" }), makeMonitor({ id: "m2" })]; (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue(monitors); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsByTeamId({ teamId: TEAM_ID }); expect(result).toHaveLength(2); }); it("passes type and filter to repository", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([]); const { service } = createService({ monitorsRepository }); await service.getMonitorsByTeamId({ teamId: TEAM_ID, type: "http", filter: "test" }); expect(monitorsRepository.findByTeamId).toHaveBeenCalledWith(TEAM_ID, { type: "http", filter: "test" }); }); }); describe("getMonitorsWithChecksByTeamId", () => { it("returns summary, count, and monitors with normalized checks", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 1, upMonitors: 1 }); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1); (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([ makeMonitor({ recentChecks: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, ], }), ]); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID }); expect(result.summary).toMatchObject({ totalMonitors: 1 }); expect(result.count).toBe(1); expect(result.monitors).toHaveLength(1); }); it("returns null summary when repository returns null", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue(null); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(0); (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID }); expect(result.summary).toBeNull(); expect(result.monitors).toEqual([]); }); it("uses snapshot (first check only) for hardware type monitors", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 1 }); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1); (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([ makeMonitor({ type: "hardware", recentChecks: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, { responseTime: 30, status: true, message: "OK" }, ], }), ]); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID, type: "hardware" }); expect(result.monitors[0].recentChecks).toHaveLength(1); }); it("uses snapshot for hardware type when type is array with hardware", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 1 }); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1); (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([ makeMonitor({ type: "hardware", recentChecks: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, ], }), ]); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID, type: ["hardware"] }); expect(result.monitors[0].recentChecks).toHaveLength(1); }); it("normalizes checks for non-snapshot types when type is an array", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 1 }); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1); (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([ makeMonitor({ type: "http", recentChecks: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, ], }), ]); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID, type: ["http", "ping"] }); expect(result.monitors[0].recentChecks.length).toBeGreaterThan(0); }); it("normalizes checks when type is undefined (mixed types)", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 2 }); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(2); (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([ makeMonitor({ id: "m1", type: "http", recentChecks: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, ], }), makeMonitor({ id: "m2", type: "hardware", recentChecks: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, { responseTime: 30, status: true, message: "OK" }, ], }), ]); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID }); // http monitor gets normalized, hardware gets snapshot (1 check) expect(result.monitors[1].recentChecks).toHaveLength(1); expect(result.monitors[0].recentChecks.length).toBeGreaterThan(0); }); it("handles monitors with empty recentChecks", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 1 }); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1); (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([makeMonitor({ recentChecks: [] })]); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID }); expect(result.monitors[0].recentChecks).toEqual([]); }); it("handles monitors with undefined recentChecks", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 1 }); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1); (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([makeMonitor({ recentChecks: undefined })]); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID }); expect(result.monitors[0].recentChecks).toEqual([]); }); it("passes all query params to repository", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 0 }); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(0); (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([]); const { service } = createService({ monitorsRepository }); await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID, limit: 10, type: "http", page: 1, rowsPerPage: 25, filter: "test", field: "name", order: "asc", }); expect(monitorsRepository.findByTeamIdWithStats).toHaveBeenCalledWith(TEAM_ID, { limit: 10, type: "http", page: 1, rowsPerPage: 25, filter: "test", field: "name", order: "asc", }); }); }); describe("getAllGames", () => { it("returns the games map", () => { const games = { cs2: { name: "Counter-Strike 2" } }; const { service } = createService({ games }); expect(service.getAllGames()).toEqual(games); }); }); describe("editMonitor", () => { it("updates monitor and refreshes job", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const updatedMonitor = makeMonitor({ name: "Updated" }); (monitorsRepository.updateById as jest.Mock).mockResolvedValue(updatedMonitor); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { name: "Updated" } }); expect(result).toMatchObject({ name: "Updated" }); expect(monitorsRepository.updateById).toHaveBeenCalledWith(MONITOR_ID, TEAM_ID, { name: "Updated" }); expect(jobQueue.updateJob).toHaveBeenCalledWith(updatedMonitor); }); }); describe("updateNotifications", () => { it("updates notifications and refreshes jobs when modifiedCount > 0", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitors = [makeMonitor({ id: "m1" }), makeMonitor({ id: "m2" })]; (monitorsRepository.updateNotifications as jest.Mock).mockResolvedValue(2); (monitorsRepository.findByIds as jest.Mock).mockResolvedValue(monitors); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.updateNotifications({ teamId: TEAM_ID, monitorIds: ["m1", "m2"], notificationIds: ["n1"], action: "add", }); expect(result).toBe(2); expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["m1", "m2"]); expect(jobQueue.updateJob).toHaveBeenCalledTimes(2); }); it("skips job refresh when modifiedCount is 0", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.updateNotifications as jest.Mock).mockResolvedValue(0); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.updateNotifications({ teamId: TEAM_ID, monitorIds: ["m1"], notificationIds: ["n1"], action: "remove", }); expect(result).toBe(0); expect(monitorsRepository.findByIds).not.toHaveBeenCalled(); expect(jobQueue.updateJob).not.toHaveBeenCalled(); }); }); describe("pauseMonitor", () => { it("resumes job when monitor is active after toggle", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor({ isActive: true }); (monitorsRepository.togglePauseById as jest.Mock).mockResolvedValue(monitor); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.pauseMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(result.isActive).toBe(true); expect(jobQueue.resumeJob).toHaveBeenCalledWith(monitor); expect(jobQueue.pauseJob).not.toHaveBeenCalled(); }); it("pauses job when monitor is inactive after toggle", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor({ isActive: false }); (monitorsRepository.togglePauseById as jest.Mock).mockResolvedValue(monitor); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.pauseMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(result.isActive).toBe(false); expect(jobQueue.pauseJob).toHaveBeenCalledWith(monitor); expect(jobQueue.resumeJob).not.toHaveBeenCalled(); }); }); describe("bulkPauseMonitors", () => { it("resumes jobs for monitors that are active after bulk toggle", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitors = [makeMonitor({ id: "m1", isActive: true }), makeMonitor({ id: "m2", isActive: true })]; (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue(monitors); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.bulkPauseMonitors({ teamId: TEAM_ID, monitorIds: ["m1", "m2"], pause: false, }); expect(result.monitors).toHaveLength(2); expect(result.failedCount).toBe(0); expect(monitorsRepository.bulkTogglePause).toHaveBeenCalledWith(["m1", "m2"], TEAM_ID, false); expect(jobQueue.resumeJob).toHaveBeenCalledTimes(2); expect(jobQueue.pauseJob).not.toHaveBeenCalled(); }); it("pauses jobs for monitors that are inactive after bulk toggle", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitors = [makeMonitor({ id: "m1", isActive: false }), makeMonitor({ id: "m2", isActive: false })]; (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue(monitors); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.bulkPauseMonitors({ teamId: TEAM_ID, monitorIds: ["m1", "m2"], pause: true, }); expect(result.monitors).toHaveLength(2); expect(result.failedCount).toBe(0); expect(monitorsRepository.bulkTogglePause).toHaveBeenCalledWith(["m1", "m2"], TEAM_ID, true); expect(jobQueue.pauseJob).toHaveBeenCalledTimes(2); expect(jobQueue.resumeJob).not.toHaveBeenCalled(); }); it("handles mixed active/inactive monitors correctly", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const activeMonitor = makeMonitor({ id: "m1", isActive: true }); const pausedMonitor = makeMonitor({ id: "m2", isActive: false }); (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue([activeMonitor, pausedMonitor]); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.bulkPauseMonitors({ teamId: TEAM_ID, monitorIds: ["m1", "m2"], pause: true, }); expect(result.monitors).toHaveLength(2); expect(result.failedCount).toBe(0); expect(jobQueue.resumeJob).toHaveBeenCalledTimes(1); expect(jobQueue.resumeJob).toHaveBeenCalledWith(activeMonitor); expect(jobQueue.pauseJob).toHaveBeenCalledTimes(1); expect(jobQueue.pauseJob).toHaveBeenCalledWith(pausedMonitor); }); it("returns empty array when no monitors were affected", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue([]); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.bulkPauseMonitors({ teamId: TEAM_ID, monitorIds: ["m1", "m2"], pause: true, }); expect(result.monitors).toHaveLength(0); expect(result.failedCount).toBe(0); expect(jobQueue.pauseJob).not.toHaveBeenCalled(); expect(jobQueue.resumeJob).not.toHaveBeenCalled(); }); it("works with a single monitor", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor({ id: "m1", isActive: false }); (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue([monitor]); const { service, jobQueue } = createService({ monitorsRepository }); const result = await service.bulkPauseMonitors({ teamId: TEAM_ID, monitorIds: ["m1"], pause: true, }); expect(result.monitors).toHaveLength(1); expect(result.failedCount).toBe(0); expect(jobQueue.pauseJob).toHaveBeenCalledWith(monitor); }); it("returns failedCount and logs errors when multiple job operations reject", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitors = [ makeMonitor({ id: "m1", isActive: false }), makeMonitor({ id: "m2", isActive: false }), makeMonitor({ id: "m3", isActive: false }), ]; (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue(monitors); const jobQueue = createJobQueueMock(); (jobQueue.pauseJob as jest.Mock) .mockRejectedValueOnce(new Error("Queue failure 1")) .mockResolvedValueOnce(undefined) .mockRejectedValueOnce(new Error("Queue failure 2")); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, jobQueue, logger }); const result = await service.bulkPauseMonitors({ teamId: TEAM_ID, monitorIds: ["m1", "m2", "m3"], pause: true, }); expect(result.monitors).toHaveLength(3); expect(result.failedCount).toBe(2); expect(logger.error).toHaveBeenCalledTimes(2); expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Failed to sync job queue for monitor m1"), method: "bulkPauseMonitors", }) ); expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Failed to sync job queue for monitor m3"), method: "bulkPauseMonitors", }) ); }); it("logs a resume failure with undefined stack when rejection is not an Error instance", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitors = [makeMonitor({ id: "m1", isActive: true })]; (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue(monitors); const jobQueue = createJobQueueMock(); // Reject with a non-Error value to exercise the `instanceof Error` false branch (jobQueue.resumeJob as jest.Mock).mockRejectedValueOnce("queue blew up"); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, jobQueue, logger }); const result = await service.bulkPauseMonitors({ teamId: TEAM_ID, monitorIds: ["m1"], pause: false, }); expect(result.failedCount).toBe(1); expect(logger.error).toHaveBeenCalledTimes(1); expect(logger.error).toHaveBeenCalledWith({ message: "Failed to sync job queue for monitor m1 during bulk resume", service: "MonitorService", method: "bulkPauseMonitors", stack: undefined, }); }); it("logs 'unknown' when the failing monitor has no id", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitors = [makeMonitor({ id: "", isActive: false })]; (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue(monitors); const jobQueue = createJobQueueMock(); (jobQueue.pauseJob as jest.Mock).mockRejectedValueOnce(new Error("boom")); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, jobQueue, logger }); const result = await service.bulkPauseMonitors({ teamId: TEAM_ID, monitorIds: ["m1"], pause: true, }); expect(result.failedCount).toBe(1); expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ message: "Failed to sync job queue for monitor unknown during bulk pause", }) ); }); it("logs 'unknown' when the failing monitor at the rejected index is nullish", async () => { const monitorsRepository = createMonitorsRepositoryMock(); // Defensive: bulkTogglePause returns an array containing a nullish entry; the // inner map throws TypeError, the forEach hits the `?.` short-circuit branch. (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue([undefined]); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, logger }); const result = await service.bulkPauseMonitors({ teamId: TEAM_ID, monitorIds: ["m1"], pause: true, }); expect(result.failedCount).toBe(1); expect(logger.error).toHaveBeenCalledWith( expect.objectContaining({ message: "Failed to sync job queue for monitor unknown during bulk pause", }) ); }); }); describe("deleteMonitor", () => { it("deletes monitor and all associated data", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(5); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(1); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(2); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(3); const { service, jobQueue } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, }); const result = await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(result).toEqual(monitor); expect(monitorStatsRepository.deleteByMonitorId).toHaveBeenCalledWith(MONITOR_ID); expect(checksRepository.deleteByMonitorId).toHaveBeenCalledWith(MONITOR_ID); expect(statusPagesRepository.removeMonitorFromStatusPages).toHaveBeenCalledWith(MONITOR_ID); expect(incidentsRepository.deleteByMonitorId).toHaveBeenCalledWith(MONITOR_ID, TEAM_ID); expect(geoChecksRepository.deleteByMonitorId).toHaveBeenCalledWith(MONITOR_ID); expect(jobQueue.deleteJob).toHaveBeenCalledWith(monitor); }); it("logs warning when monitorStats deletion fails with Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockRejectedValue(new Error("stats error")); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting monitor stats"), stack: expect.any(String), }) ); }); it("logs warning with undefined stack when monitorStats deletion fails with non-Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockRejectedValue("string error"); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting monitor stats"), stack: undefined, }) ); }); it("logs warning when checks deletion fails with Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (checksRepository.deleteByMonitorId as jest.Mock).mockRejectedValue(new Error("checks error")); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting checks"), stack: expect.any(String), }) ); }); it("logs warning when checks deletion fails with non-Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (checksRepository.deleteByMonitorId as jest.Mock).mockRejectedValue(42); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting checks"), stack: undefined, }) ); }); it("logs warning when status pages removal fails with Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockRejectedValue(new Error("status pages error")); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error removing monitor"), stack: expect.any(String), }) ); }); it("logs warning when status pages removal fails with non-Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockRejectedValue("oops"); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error removing monitor"), stack: undefined, }) ); }); it("logs warning when incidents deletion fails with Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (incidentsRepository.deleteByMonitorId as jest.Mock).mockRejectedValue(new Error("incidents error")); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting incidents"), stack: expect.any(String), }) ); }); it("logs warning when incidents deletion fails with non-Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (incidentsRepository.deleteByMonitorId as jest.Mock).mockRejectedValue(null); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting incidents"), stack: undefined, }) ); }); it("logs warning when geo checks deletion fails with Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockRejectedValue(new Error("geo error")); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting geo checks"), stack: expect.any(String), }) ); }); it("logs warning when geo checks deletion fails with non-Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.deleteById as jest.Mock).mockResolvedValue(monitor); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockRejectedValue({ code: 500 }); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, geoChecksRepository, logger, }); await service.deleteMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID }); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting geo checks"), stack: undefined, }) ); }); }); describe("deleteAllMonitors", () => { it("deletes all monitors and associated data", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitors = [makeMonitor({ id: "m1" }), makeMonitor({ id: "m2" })]; (monitorsRepository.deleteByTeamId as jest.Mock).mockResolvedValue({ monitors, deletedCount: 2 }); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); const { service, jobQueue } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, geoChecksRepository, }); const result = await service.deleteAllMonitors({ teamId: TEAM_ID }); expect(result).toBe(2); expect(jobQueue.deleteJob).toHaveBeenCalledTimes(2); expect(checksRepository.deleteByMonitorId).toHaveBeenCalledTimes(2); expect(geoChecksRepository.deleteByMonitorId).toHaveBeenCalledTimes(2); expect(statusPagesRepository.removeMonitorFromStatusPages).toHaveBeenCalledTimes(2); expect(monitorStatsRepository.deleteByMonitorId).toHaveBeenCalledTimes(2); }); it("logs warning when associated record deletion fails with Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitors = [makeMonitor({ id: "m1" })]; (monitorsRepository.deleteByTeamId as jest.Mock).mockResolvedValue({ monitors, deletedCount: 1 }); (checksRepository.deleteByMonitorId as jest.Mock).mockRejectedValue(new Error("fail")); const logger = createMockLogger(); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, geoChecksRepository, logger, }); const result = await service.deleteAllMonitors({ teamId: TEAM_ID }); expect(result).toBe(1); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting associated records"), stack: expect.any(String), }) ); }); it("logs warning when associated record deletion fails with non-Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const monitors = [makeMonitor({ id: "m1" })]; (monitorsRepository.deleteByTeamId as jest.Mock).mockResolvedValue({ monitors, deletedCount: 1 }); // deleteJob throws non-Error const jobQueue = createJobQueueMock(); (jobQueue.deleteJob as jest.Mock).mockRejectedValue("string error"); const logger = createMockLogger(); const service = new MonitorService({ scheduler: jobQueue as any, logger: logger as any, games: {} as any, monitorsRepository, checksRepository, geoChecksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository: createIncidentsRepositoryMock(), }); const result = await service.deleteAllMonitors({ teamId: TEAM_ID }); expect(result).toBe(1); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting associated records"), stack: undefined, }) ); }); it("handles empty monitors array", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.deleteByTeamId as jest.Mock).mockResolvedValue({ monitors: [], deletedCount: 0 }); const { service } = createService({ monitorsRepository }); const result = await service.deleteAllMonitors({ teamId: TEAM_ID }); expect(result).toBe(0); }); }); describe("exportMonitorsToJSON", () => { it("returns monitors when found", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitors = [makeMonitor({ id: "m1" })]; (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue(monitors); const { service } = createService({ monitorsRepository }); const result = await service.exportMonitorsToJSON({ teamId: TEAM_ID }); expect(result).toEqual(monitors); }); it("throws when no monitors found (null)", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository }); await expect(service.exportMonitorsToJSON({ teamId: TEAM_ID })).rejects.toThrow("No monitors found to export."); }); it("throws when monitors array is empty", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([]); const { service } = createService({ monitorsRepository }); await expect(service.exportMonitorsToJSON({ teamId: TEAM_ID })).rejects.toThrow("No monitors found to export."); }); }); describe("importMonitorsFromJSON", () => { it("imports monitors and returns count", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const importedMonitors = [ { name: "Test 1", type: "http", url: "https://example.com", interval: 60000 }, { name: "Test 2", type: "ping", url: "https://test.com", interval: 30000 }, ]; const createdMonitors = [makeMonitor({ id: "new1" }), makeMonitor({ id: "new2" })]; (monitorsRepository.createMonitors as jest.Mock).mockResolvedValue(createdMonitors); const { service } = createService({ monitorsRepository }); const result = await service.importMonitorsFromJSON({ teamId: TEAM_ID, userId: USER_ID, monitors: importedMonitors as any, }); expect(result.imported).toBe(2); expect(result.errors).toEqual([]); }); it("cleans monitor fields before creating", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const createdMonitors = [makeMonitor({ id: "new1" })]; (monitorsRepository.createMonitors as jest.Mock).mockResolvedValue(createdMonitors); const { service } = createService({ monitorsRepository }); await service.importMonitorsFromJSON({ teamId: TEAM_ID, userId: USER_ID, monitors: [{ name: "Test", type: "http", url: "https://example.com", interval: 60000 }] as any, }); const callArg = (monitorsRepository.createMonitors as jest.Mock).mock.calls[0]![0] as any[]; expect(callArg[0]).toMatchObject({ teamId: TEAM_ID, userId: USER_ID, id: "", recentChecks: [], createdAt: "", updatedAt: "", }); }); }); });