2223 lines
95 KiB
TypeScript
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("");
|
|
});
|
|
});
|
|
});
|