Files
Checkmate/server/test/unit/services/monitorService.test.ts
T

1644 lines
67 KiB
TypeScript

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