Files
Checkmate/server/test/unit/services/monitorService.test.ts
2026-09-21 10:22:46 -07:00

2223 lines
95 KiB
TypeScript

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<IEncryptionService> = {}) =>
({
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<string, unknown> = {}) => ({
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<typeof createJobQueueMock>;
logger?: ReturnType<typeof createMockLogger>;
games?: Record<string, unknown>;
} = {}
) => {
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<string, unknown>;
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<string, unknown>;
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<Record<string, number | string | null>> = {}) => ({
_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<unknown>, 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<string, unknown>];
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<string, unknown>];
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<string, unknown>];
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<string, unknown>];
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<string, unknown>];
expect(patch.secret).toBe("");
});
});
});