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 { IDockerLogsRepository } from "../../../src/domain/docker/docker-log.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 type { IEncryptionService } from "../../../src/service/encryption/encryptionService.ts"; import { AppError } from "../../../src/utils/AppError.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(), findDockerContainerChecks: 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 createDockerLogsRepositoryMock = () => ({ findByContainerName: jest.fn(), deleteByMonitorId: jest.fn().mockResolvedValue(0), }) as unknown as IDockerLogsRepository; 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 createEncryptionServiceMock = (overrides: Partial = {}) => ({ isConfigured: jest.fn(() => true), currentKeyId: jest.fn(() => "key-1"), keyIdOf: jest.fn(() => "key-1"), needsReencryption: jest.fn(() => false), encrypt: jest.fn((plaintext: string) => `enc:${plaintext}`), decrypt: jest.fn((ciphertext: string) => ciphertext.replace(/^enc:/, "")), ...overrides, }) as unknown as IEncryptionService; 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; dockerLogsRepository?: IDockerLogsRepository; incidentsRepository?: IIncidentsRepository; encryptionService?: IEncryptionService; 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(), dockerLogsRepository: overrides.dockerLogsRepository ?? createDockerLogsRepositoryMock(), monitorStatsRepository: overrides.monitorStatsRepository ?? createMonitorStatsRepositoryMock(), statusPagesRepository: overrides.statusPagesRepository ?? createStatusPagesRepositoryMock(), incidentsRepository: overrides.incidentsRepository ?? createIncidentsRepositoryMock(), encryptionService: overrides.encryptionService ?? createEncryptionServiceMock(), }); return { service, jobQueue, logger }; }; describe("MonitorService", () => { describe("createMonitor", () => { it("creates a monitor, adds a job and returns the created document", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.create as jest.Mock).mockResolvedValue(monitor); const { service, jobQueue } = createService({ monitorsRepository }); const result = 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); expect(result).toBe(monitor); }); it("strips a stray proxyId when proxyMode is not custom", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.create as jest.Mock).mockResolvedValue(monitor); const { service } = createService({ monitorsRepository }); await service.createMonitor(TEAM_ID, USER_ID, { ...monitor, proxyMode: "inherit", proxyId: "64b7a1f2c9e77a001a2b3c4d", } as any); const createdBody = (monitorsRepository.create as jest.Mock).mock.calls[0][0] as Record; expect(createdBody.proxyId).toBeUndefined(); }); it("keeps proxyId when proxyMode is custom", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor(); (monitorsRepository.create as jest.Mock).mockResolvedValue(monitor); const { service } = createService({ monitorsRepository }); await service.createMonitor(TEAM_ID, USER_ID, { ...monitor, proxyMode: "custom", proxyId: "64b7a1f2c9e77a001a2b3c4d", } as any); const createdBody = (monitorsRepository.create as jest.Mock).mock.calls[0][0] as Record; expect(createdBody.proxyId).toBe("64b7a1f2c9e77a001a2b3c4d"); }); 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, port, game, grpc, websocket, dns", async () => { for (const monitorType of ["ping", "port", "game", "grpc", "websocket", "dns"] 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("throws 400 for unsupported monitor type (docker)", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor({ type: "docker" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "docker", aggregateData: { totalChecks: 0 }, upChecks: { totalChecks: 0 }, aggregate: [], latest: null, }); (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("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("getDockerDetailsById", () => { it("returns docker details for a docker monitor", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const monitor = makeMonitor({ type: "docker" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "docker", aggregateData: { totalChecks: 10 }, upChecks: { totalChecks: 9 }, aggregate: [{ _id: "2024-01-01T00:00:00Z", avgResponseTime: 12, upCount: 9, totalCount: 10, avgRunning: 3, avgTotal: 4, avgUnhealthy: 0 }], latest: { containers: [], summary: { total: 4, running: 3, stopped: 1, unhealthy: 0 }, checkedAt: "2024-01-01T00:00:00Z", }, }); (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue({ monitorId: MONITOR_ID }); const { service } = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); const result = await service.getDockerDetailsById({ 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("aggregate"); expect(result.stats).toHaveProperty("latest"); expect(result.stats.latest?.summary).toMatchObject({ total: 4, running: 3, stopped: 1, unhealthy: 0 }); 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.getDockerDetailsById({ teamId: TEAM_ID, monitorId: "missing", dateRange: "recent" })).rejects.toThrow( "Monitor with ID missing not found." ); }); it("throws 400 when monitor type is not docker", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const monitor = makeMonitor({ type: "http" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const { service } = createService({ monitorsRepository }); await expect(service.getDockerDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" })).rejects.toThrow( "monitors are not supported for docker details" ); }); it("throws 500 when checksData monitorType is not docker", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitor = makeMonitor({ type: "docker" }); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findByDateRangeAndMonitorId as jest.Mock).mockResolvedValue({ monitorType: "http" }); const { service } = createService({ monitorsRepository, checksRepository }); await expect(service.getDockerDetailsById({ teamId: TEAM_ID, monitorId: MONITOR_ID, dateRange: "recent" })).rejects.toThrow( "Unable to load docker stats for this monitor" ); }); }); describe("getDockerContainerByName", () => { const makeBucket = (overrides: Partial> = {}) => ({ _id: "2024-01-01T00:00:00Z", avgCpuPct: 12.5, avgMemoryUsedBytes: 1024, avgMemoryPct: 50, minRestartCount: 0, maxRestartCount: 0, ...overrides, }); it("returns container details for a docker monitor", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitor = makeMonitor({ type: "docker" }); const container = { id: "abc", name: "checkmate-mongo", image: "mongo:8.0", state: "running", status: "Up 3 hours", health: "healthy" }; (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (checksRepository.findDockerContainerChecks as jest.Mock).mockResolvedValue({ aggregate: [makeBucket()], latest: { container, checkedAt: "2024-01-01T00:00:00Z" }, }); const { service } = createService({ monitorsRepository, checksRepository }); const result = await service.getDockerContainerByName({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "checkmate-mongo", dateRange: "recent", }); expect(checksRepository.findDockerContainerChecks).toHaveBeenCalledWith(MONITOR_ID, "checkmate-mongo", "recent"); expect(result.monitor).toMatchObject({ id: MONITOR_ID }); expect(result.stats.latest?.container).toMatchObject({ name: "checkmate-mongo" }); expect(result.stats.restartsInRange).toBe(0); }); it("sums restarts within and between buckets", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); (checksRepository.findDockerContainerChecks as jest.Mock).mockResolvedValue({ aggregate: [ makeBucket({ _id: "01", minRestartCount: 0, maxRestartCount: 2 }), makeBucket({ _id: "02", minRestartCount: 3, maxRestartCount: 3 }), makeBucket({ _id: "03", minRestartCount: null, maxRestartCount: null }), makeBucket({ _id: "04", minRestartCount: 3, maxRestartCount: 4 }), ], latest: null, }); const { service } = createService({ monitorsRepository, checksRepository }); const result = await service.getDockerContainerByName({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "checkmate-mongo", dateRange: "day", }); // 2 within bucket 01, +1 between 01 and 02, +1 within 04 (null bucket skipped) expect(result.stats.restartsInRange).toBe(4); }); it("counts a restart-count drop as a recreate", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); (checksRepository.findDockerContainerChecks as jest.Mock).mockResolvedValue({ aggregate: [ makeBucket({ _id: "01", minRestartCount: 5, maxRestartCount: 5 }), makeBucket({ _id: "02", minRestartCount: 1, maxRestartCount: 1 }), ], latest: null, }); const { service } = createService({ monitorsRepository, checksRepository }); const result = await service.getDockerContainerByName({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "checkmate-mongo", dateRange: "day", }); // counter dropped 5 -> 1: the recreate itself plus the 1 restart the new counter shows expect(result.stats.restartsInRange).toBe(2); }); 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.getDockerContainerByName({ teamId: TEAM_ID, monitorId: "missing", containerName: "checkmate-mongo", dateRange: "recent" }) ).rejects.toThrow("Monitor with ID missing not found."); }); it("throws 400 when monitor type is not docker", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "http" })); const { service } = createService({ monitorsRepository }); await expect( service.getDockerContainerByName({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "checkmate-mongo", dateRange: "recent" }) ).rejects.toThrow("monitors are not supported for docker container details"); }); }); describe("getDockerContainerLogs", () => { it("throws 404 when the monitor is not found", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(null); const { service } = createService({ monitorsRepository }); await expect(service.getDockerContainerLogs({ teamId: TEAM_ID, monitorId: "missing", containerName: "web", limit: 20 })).rejects.toThrow( "Monitor with ID missing not found" ); }); it("throws 400 for a non-docker monitor", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "http" })); const { service } = createService({ monitorsRepository }); await expect(service.getDockerContainerLogs({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "web", limit: 20 })).rejects.toThrow( "monitors are not supported for docker container logs" ); }); it("passes pagination through and returns the last checkedAt as a full-page cursor", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const dockerLogsRepository = createDockerLogsRepositoryMock(); const before = new Date("2026-01-02T00:00:00.000Z"); const logs = [{ checkedAt: "2026-01-01T02:00:00.000Z" }, { checkedAt: "2026-01-01T01:00:00.000Z" }]; (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); (dockerLogsRepository.findByContainerName as jest.Mock).mockResolvedValue(logs); const { service } = createService({ monitorsRepository, dockerLogsRepository }); const result = await service.getDockerContainerLogs({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "web", before, limit: 2, }); expect(dockerLogsRepository.findByContainerName).toHaveBeenCalledWith({ monitorId: MONITOR_ID, containerName: "web", before, limit: 2 }); expect(result.nextCursor).toBe("2026-01-01T01:00:00.000Z"); }); it("returns a null cursor for a short page", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const dockerLogsRepository = createDockerLogsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); (dockerLogsRepository.findByContainerName as jest.Mock).mockResolvedValue([{ checkedAt: "2026-01-01T01:00:00.000Z" }]); const { service } = createService({ monitorsRepository, dockerLogsRepository }); const result = await service.getDockerContainerLogs({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "web", limit: 2 }); expect(result.nextCursor).toBeNull(); }); it("passes after through and returns a null cursor even for a full page", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const dockerLogsRepository = createDockerLogsRepositoryMock(); const after = new Date("2026-01-01T00:00:00.000Z"); const logs = [{ checkedAt: "2026-01-01T02:00:00.000Z" }, { checkedAt: "2026-01-01T01:00:00.000Z" }]; (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); (dockerLogsRepository.findByContainerName as jest.Mock).mockResolvedValue(logs); const { service } = createService({ monitorsRepository, dockerLogsRepository }); const result = await service.getDockerContainerLogs({ teamId: TEAM_ID, monitorId: MONITOR_ID, containerName: "web", after, limit: 2, }); expect(dockerLogsRepository.findByContainerName).toHaveBeenCalledWith({ monitorId: MONITOR_ID, containerName: "web", after, limit: 2 }); expect(result.logs).toBe(logs); expect(result.nextCursor).toBeNull(); }); }); 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([]); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID }); expect(result.summary).toBeNull(); expect(result.monitors).toEqual([]); }); it("uses snapshot (latest 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); expect(result.monitors[0].recentChecks[0].responseTime).toBe(30); }); 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); expect(result.monitors[0].recentChecks[0].responseTime).toBe(20); }); 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" }, { unsetProxyId: false }); expect(jobQueue.updateJob).toHaveBeenCalledWith(updatedMonitor); }); it("keeps proxyId and does not unset it when proxyMode is custom", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.updateById as jest.Mock).mockResolvedValue(makeMonitor()); const { service } = createService({ monitorsRepository }); await service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { proxyMode: "custom", proxyId: "64b7a1f2c9e77a001a2b3c4d" } as any, }); expect(monitorsRepository.updateById).toHaveBeenCalledWith( MONITOR_ID, TEAM_ID, { proxyMode: "custom", proxyId: "64b7a1f2c9e77a001a2b3c4d" }, { unsetProxyId: false } ); }); it("strips proxyId and unsets it when proxyMode moves off custom", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.updateById as jest.Mock).mockResolvedValue(makeMonitor()); const { service } = createService({ monitorsRepository }); await service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { proxyMode: "inherit", proxyId: "64b7a1f2c9e77a001a2b3c4d" } as any, }); expect(monitorsRepository.updateById).toHaveBeenCalledWith(MONITOR_ID, TEAM_ID, { proxyMode: "inherit" }, { unsetProxyId: true }); }); }); 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 incidentsRepository = createIncidentsRepositoryMock(); 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({}); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const { service, jobQueue } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, geoChecksRepository, incidentsRepository, }); 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); expect(incidentsRepository.deleteByMonitorId).toHaveBeenCalledTimes(2); }); it("logs a per-child warning and continues when a child deletion fails with Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const monitors = [makeMonitor({ id: "m1" })]; (monitorsRepository.deleteByTeamId as jest.Mock).mockResolvedValue({ monitors, deletedCount: 1 }); (checksRepository.deleteByMonitorId as jest.Mock).mockRejectedValue(new Error("fail")); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); const { service, jobQueue } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, geoChecksRepository, incidentsRepository, logger, }); const result = await service.deleteAllMonitors({ teamId: TEAM_ID }); expect(result).toBe(1); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Error deleting checks"), stack: expect.any(String), }) ); expect(jobQueue.deleteJob).toHaveBeenCalledTimes(1); }); it("logs warning when scheduler job deletion fails with non-Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); const incidentsRepository = createIncidentsRepositoryMock(); const monitors = [makeMonitor({ id: "m1" })]; (monitorsRepository.deleteByTeamId as jest.Mock).mockResolvedValue({ monitors, deletedCount: 1 }); (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({}); (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); // 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, dockerLogsRepository: createDockerLogsRepositoryMock(), monitorStatsRepository, statusPagesRepository, incidentsRepository, encryptionService: createEncryptionServiceMock(), }); 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 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: "", }); }); }); }); describe("MonitorService — Docker TLS credentials", () => { const KEY_PEM = "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----"; const tlsBody = { type: "docker" as const, url: "tcp://host:2376", dockerTlsCa: "ca-pem", dockerTlsCert: "cert-pem", dockerTlsKey: KEY_PEM }; const expectAppError = async (fn: () => Promise, status: number, message: string | RegExp) => { let caught: unknown; try { await fn(); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(AppError); expect((caught as AppError).status).toBe(status); expect((caught as AppError).message).toMatch(message); }; describe("createMonitor", () => { it("encrypts the key and sets dockerTlsKeySet for a TLS url", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.create as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); const encryptionService = createEncryptionServiceMock(); const { service } = createService({ monitorsRepository, encryptionService }); await service.createMonitor(TEAM_ID, USER_ID, makeMonitor(tlsBody) as any); expect(encryptionService.encrypt).toHaveBeenCalledWith(KEY_PEM); expect(monitorsRepository.create).toHaveBeenCalledWith( expect.objectContaining({ dockerTlsCa: "ca-pem", dockerTlsCert: "cert-pem", dockerTlsKey: `enc:${KEY_PEM}`, dockerTlsKeySet: true }), TEAM_ID, USER_ID ); }); it("does not encrypt anything for a socket url and leaves dockerTlsKeySet false", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.create as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); const encryptionService = createEncryptionServiceMock(); const { service } = createService({ monitorsRepository, encryptionService }); await service.createMonitor(TEAM_ID, USER_ID, makeMonitor({ type: "docker", url: "unix:///var/run/docker.sock" }) as any); expect(encryptionService.encrypt).not.toHaveBeenCalled(); const [created] = (monitorsRepository.create as jest.Mock).mock.calls[0] as [Record]; expect(created.dockerTlsKeySet).toBe(false); expect(created.dockerTlsKey).toBeUndefined(); }); it("ignores TLS fields on non-docker monitors", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.create as jest.Mock).mockResolvedValue(makeMonitor()); const encryptionService = createEncryptionServiceMock(); const { service } = createService({ monitorsRepository, encryptionService }); await service.createMonitor(TEAM_ID, USER_ID, makeMonitor({ dockerTlsKey: KEY_PEM }) as any); expect(encryptionService.encrypt).not.toHaveBeenCalled(); expect(monitorsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ dockerTlsKey: KEY_PEM }), TEAM_ID, USER_ID); }); it("rejects with a 422 naming ENCRYPTION_KEY when encryption is not configured", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const encryptionService = createEncryptionServiceMock({ isConfigured: jest.fn(() => false) as any }); const { service } = createService({ monitorsRepository, encryptionService }); await expectAppError(() => service.createMonitor(TEAM_ID, USER_ID, makeMonitor(tlsBody) as any), 422, /ENCRYPTION_KEY/); expect(encryptionService.encrypt).not.toHaveBeenCalled(); expect(monitorsRepository.create).not.toHaveBeenCalled(); }); }); describe("editMonitor", () => { it("keeps the stored key when the key field is blank", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker", dockerTlsKeySet: true })); (monitorsRepository.updateById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); const encryptionService = createEncryptionServiceMock(); const { service } = createService({ monitorsRepository, encryptionService }); await service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { ...tlsBody, dockerTlsKey: "" } }); expect(encryptionService.encrypt).not.toHaveBeenCalled(); const [, , patch] = (monitorsRepository.updateById as jest.Mock).mock.calls[0] as [string, string, Record]; expect(patch).not.toHaveProperty("dockerTlsKey"); expect(patch).not.toHaveProperty("dockerTlsKeySet"); expect(patch).toMatchObject({ dockerTlsCa: "ca-pem", dockerTlsCert: "cert-pem" }); }); it("rejects a blank key with a 422 when no key is stored", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker", dockerTlsKeySet: false })); const { service } = createService({ monitorsRepository }); await expectAppError( () => service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { ...tlsBody, dockerTlsKey: "" } }), 422, /required for a TLS Docker host/ ); expect(monitorsRepository.updateById).not.toHaveBeenCalled(); }); it("encrypts a replacement key without reading the stored monitor", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.updateById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); const encryptionService = createEncryptionServiceMock(); const { service } = createService({ monitorsRepository, encryptionService }); await service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: tlsBody }); expect(monitorsRepository.findById).not.toHaveBeenCalled(); expect(monitorsRepository.updateById).toHaveBeenCalledWith( MONITOR_ID, TEAM_ID, expect.objectContaining({ dockerTlsKey: `enc:${KEY_PEM}`, dockerTlsKeySet: true }), { unsetProxyId: false } ); }); it("encrypts a replacement key when the body omits the url", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.updateById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); const encryptionService = createEncryptionServiceMock(); const { service } = createService({ monitorsRepository, encryptionService }); await service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { type: "docker", dockerTlsKey: KEY_PEM } }); expect(encryptionService.encrypt).toHaveBeenCalledWith(KEY_PEM); expect(monitorsRepository.updateById).toHaveBeenCalledWith( MONITOR_ID, TEAM_ID, expect.objectContaining({ dockerTlsKey: `enc:${KEY_PEM}`, dockerTlsKeySet: true }), { unsetProxyId: false } ); }); it("clears dockerTlsKeySet when the url changes back to a socket", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.updateById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); const encryptionService = createEncryptionServiceMock(); const { service } = createService({ monitorsRepository, encryptionService }); await service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { type: "docker", url: "unix:///var/run/docker.sock", dockerTlsKey: KEY_PEM }, }); expect(encryptionService.encrypt).not.toHaveBeenCalled(); expect(monitorsRepository.findById).not.toHaveBeenCalled(); const [, , patch] = (monitorsRepository.updateById as jest.Mock).mock.calls[0] as [string, string, Record]; expect(patch.dockerTlsKeySet).toBe(false); expect(patch.dockerTlsKey).toBeUndefined(); }); it("rejects with a 422 naming ENCRYPTION_KEY when encryption is not configured", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const encryptionService = createEncryptionServiceMock({ isConfigured: jest.fn(() => false) as any }); const { service } = createService({ monitorsRepository, encryptionService }); await expectAppError(() => service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: tlsBody }), 422, /ENCRYPTION_KEY/); expect(monitorsRepository.updateById).not.toHaveBeenCalled(); }); }); describe("editMonitor for Capture hosts", () => { const captureUrl = "http://capture:59232/api/v1/metrics/docker"; it("rejects a url change to Capture with a 422 when no secret is stored or provided", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker", url: "unix:///var/run/docker.sock" })); const { service } = createService({ monitorsRepository }); await expectAppError( () => service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { type: "docker", url: captureUrl } }), 422, /Capture API secret is required/ ); expect(monitorsRepository.updateById).not.toHaveBeenCalled(); }); it("keeps the stored secret and disables logs when only the url changes to Capture", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker", secret: "stored-secret", dockerLogsEnabled: true })); (monitorsRepository.updateById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); const { service } = createService({ monitorsRepository }); await service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { type: "docker", url: captureUrl } }); const [, , patch] = (monitorsRepository.updateById as jest.Mock).mock.calls[0] as [string, string, Record]; expect(patch).not.toHaveProperty("secret"); expect(patch.dockerLogsEnabled).toBe(false); }); it("rejects a blank secret against a stored Capture url when the body omits the url", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker", url: captureUrl, secret: "stored-secret" })); const { service } = createService({ monitorsRepository }); await expectAppError( () => service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { type: "docker", secret: " " } }), 422, /Capture API secret is required/ ); expect(monitorsRepository.updateById).not.toHaveBeenCalled(); }); it("clears the secret without reading the stored monitor when the url changes to a socket", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.updateById as jest.Mock).mockResolvedValue(makeMonitor({ type: "docker" })); const { service } = createService({ monitorsRepository }); await service.editMonitor({ teamId: TEAM_ID, monitorId: MONITOR_ID, body: { type: "docker", url: "unix:///var/run/docker.sock", secret: "stale-capture-secret" }, }); expect(monitorsRepository.findById).not.toHaveBeenCalled(); const [, , patch] = (monitorsRepository.updateById as jest.Mock).mock.calls[0] as [string, string, Record]; expect(patch.secret).toBe(""); }); }); });