Files
Checkmate/server/tests/services/infrastructure/StatusService.test.js
T

363 lines
14 KiB
JavaScript

import StatusService from "@/services/infrastructure/StatusService.js";
import { MonitorStats } from "@/db/models/index.js";
import ApiError from "@/utils/ApiError.js";
import { Types } from "mongoose";
const buildStats = (overrides = {}) => {
const saveMock = overrides.save ||
jest.fn().mockImplementation(async function () {
return this;
});
return {
monitorId: {},
avgResponseTime: 0,
maxResponseTime: 0,
totalChecks: 1,
totalUpChecks: 0,
totalDownChecks: 0,
uptimePercentage: 0,
lastCheckTimestamp: 0,
lastResponseTime: 0,
timeOfLastFailure: 0,
currentStreak: 0,
currentStreakStatus: "up",
currentStreakStartedAt: 0,
createdAt: new Date(),
updatedAt: new Date(),
save: saveMock,
...overrides,
};
};
const buildStatusResponse = (overrides = {}) => ({
monitorId: "monitor-id",
teamId: "team-id",
type: "http",
status: "up",
message: "OK",
responseTime: 0,
...overrides,
});
const buildMonitor = (overrides) => {
const monitor = {
_id: new Types.ObjectId(),
teamId: "team-id",
status: "up",
latestChecks: [],
n: 1,
save: jest.fn(),
...overrides,
};
if (!monitor.latestChecks) {
monitor.latestChecks = [];
}
monitor.save.mockImplementation(async () => monitor);
return {
monitor: monitor,
saveMock: monitor.save,
};
};
describe("StatusService.calculateAvgResponseTime", () => {
const service = new StatusService();
it("returns latest response time when average is unset", () => {
const stats = buildStats({ avgResponseTime: 0, totalChecks: 1 });
const statusResponse = buildStatusResponse({ responseTime: 250 });
const result = service.calculateAvgResponseTime(stats, statusResponse);
expect(result).toBe(250);
});
it("computes rolling average when checks exist", () => {
const stats = buildStats({ avgResponseTime: 120, totalChecks: 5 });
const statusResponse = buildStatusResponse({ responseTime: 300 });
const result = service.calculateAvgResponseTime(stats, statusResponse);
expect(result).toBeCloseTo(156);
});
});
describe("StatusService.updateMonitorStatus", () => {
const service = new StatusService();
it("initializing monitor adopts first status", async () => {
const { monitor, saveMock } = buildMonitor({
status: "initializing",
latestChecks: [],
});
const statusResponse = buildStatusResponse({ status: "up", responseTime: 120 });
const [savedMonitor, statusChanged] = await service.updateMonitorStatus(monitor, statusResponse);
expect(saveMock).toHaveBeenCalledTimes(1);
expect(monitor.status).toBe("up");
expect(monitor.latestChecks).toHaveLength(1);
expect(monitor.latestChecks?.[0]?.status).toBe("up");
expect(monitor.lastCheckedAt).toBeInstanceOf(Date);
expect(savedMonitor).toBe(monitor);
expect(statusChanged).toBe(false);
});
it("returns early when not enough checks accumulated", async () => {
const { monitor, saveMock } = buildMonitor({
status: "up",
n: 3,
latestChecks: [],
});
const statusResponse = buildStatusResponse({ status: "down", responseTime: 90 });
const [, statusChanged] = await service.updateMonitorStatus(monitor, statusResponse);
expect(saveMock).toHaveBeenCalledTimes(1);
expect(monitor.status).toBe("up");
expect(monitor.latestChecks).toHaveLength(1);
expect(statusChanged).toBe(false);
});
it("updates status when last n checks differ", async () => {
const { monitor, saveMock } = buildMonitor({
status: "up",
n: 2,
latestChecks: [
{
status: "down",
responseTime: 80,
checkedAt: new Date("2024-01-01"),
},
],
});
const statusResponse = buildStatusResponse({ status: "down", responseTime: 70 });
const [, statusChanged] = await service.updateMonitorStatus(monitor, statusResponse);
expect(saveMock).toHaveBeenCalledTimes(1);
expect(monitor.status).toBe("down");
expect(statusChanged).toBe(true);
expect(monitor.latestChecks.slice(-2).every((c) => c.status === "down")).toBe(true);
});
it("keeps only latest 25 checks", async () => {
const existingChecks = Array.from({ length: 25 }, (_, index) => ({
status: "up",
responseTime: index,
checkedAt: new Date(`2024-01-${index + 1}`),
}));
const { monitor, saveMock } = buildMonitor({
status: "up",
n: 1,
latestChecks: [...existingChecks],
});
const statusResponse = buildStatusResponse({ status: "up", responseTime: 200 });
await service.updateMonitorStatus(monitor, statusResponse);
expect(saveMock).toHaveBeenCalledTimes(1);
expect(monitor.latestChecks).toHaveLength(25);
expect(monitor.latestChecks?.[0]?.responseTime).toBe(1);
expect(monitor.latestChecks?.some((check) => check.responseTime === 0)).toBe(false);
const lastCheck = monitor.latestChecks?.[monitor.latestChecks.length - 1] ?? null;
expect(lastCheck?.responseTime).toBe(200);
});
it("initializes latestChecks when missing", async () => {
const { monitor, saveMock } = buildMonitor({});
delete monitor.latestChecks;
const statusResponse = buildStatusResponse({ status: "up", responseTime: 75 });
const [, statusChanged] = await service.updateMonitorStatus(monitor, statusResponse);
expect(saveMock).toHaveBeenCalledTimes(1);
expect(Array.isArray(monitor.latestChecks)).toBe(true);
expect(monitor.latestChecks).toHaveLength(1);
expect(statusChanged).toBe(false);
});
});
describe("StatusService.updateMonitorStats", () => {
const service = new StatusService();
let findOneSpy;
let createSpy;
beforeEach(() => {
findOneSpy = jest.spyOn(MonitorStats, "findOne");
createSpy = jest.spyOn(MonitorStats, "create");
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
jest.restoreAllMocks();
});
it("creates MonitorStats when missing and returns saved stats", async () => {
const now = new Date("2024-02-01T00:00:00Z");
jest.setSystemTime(now);
findOneSpy.mockResolvedValue(null);
const { monitor } = buildMonitor({});
const statusResponse = buildStatusResponse({ status: "up", responseTime: 123 });
const stats = buildStats({ totalChecks: 0, totalUpChecks: 0, totalDownChecks: 0 });
const saveMock = stats.save;
createSpy.mockResolvedValue(stats);
const result = await service.updateMonitorStats(monitor, statusResponse, false);
expect(findOneSpy).toHaveBeenCalledWith({ monitorId: monitor._id });
expect(createSpy).toHaveBeenCalledWith({
monitorId: monitor._id,
currentStreakStartedAt: expect.any(Number),
});
expect(saveMock).toHaveBeenCalledTimes(1);
expect(result).toBe(stats);
expect(stats.totalChecks).toBe(1);
expect(stats.totalUpChecks).toBe(1);
expect(stats.lastResponseTime).toBe(123);
expect(stats.lastCheckTimestamp).toBe(now.getTime());
});
it("increments counters and preserves streak when status unchanged", async () => {
const now = new Date("2024-02-01T12:00:00Z");
jest.setSystemTime(now);
const stats = buildStats({
totalChecks: 4,
totalUpChecks: 3,
totalDownChecks: 1,
avgResponseTime: 100,
maxResponseTime: 250,
lastResponseTime: 90,
currentStreak: 2,
currentStreakStatus: "up",
currentStreakStartedAt: 111,
lastCheckTimestamp: 1,
timeOfLastFailure: 555,
});
const saveMock = stats.save;
findOneSpy.mockResolvedValue(stats);
const { monitor } = buildMonitor({});
const statusResponse = buildStatusResponse({ status: "up", responseTime: 150 });
const result = await service.updateMonitorStats(monitor, statusResponse, false);
expect(findOneSpy).toHaveBeenCalledWith({ monitorId: monitor._id });
expect(saveMock).toHaveBeenCalledTimes(1);
expect(result).toBe(stats);
expect(stats.totalChecks).toBe(5);
expect(stats.totalUpChecks).toBe(4);
expect(stats.totalDownChecks).toBe(1);
expect(stats.currentStreak).toBe(3);
expect(stats.currentStreakStatus).toBe("up");
expect(stats.currentStreakStartedAt).toBe(111);
expect(stats.lastCheckTimestamp).toBe(now.getTime());
expect(stats.timeOfLastFailure).toBe(555);
expect(stats.avgResponseTime).toBeCloseTo(110);
expect(stats.uptimePercentage).toBeCloseTo(4 / 5);
expect(stats.lastResponseTime).toBe(150);
expect(stats.maxResponseTime).toBe(250);
});
it("resets streak and records failure on status change to down", async () => {
const now = new Date("2024-02-01T12:05:00Z");
jest.setSystemTime(now);
const stats = buildStats({
totalChecks: 10,
totalUpChecks: 7,
totalDownChecks: 3,
avgResponseTime: 200,
maxResponseTime: 300,
currentStreak: 5,
currentStreakStatus: "up",
currentStreakStartedAt: 999,
timeOfLastFailure: 444,
});
const saveMock = stats.save;
findOneSpy.mockResolvedValue(stats);
const { monitor } = buildMonitor({});
const statusResponse = buildStatusResponse({ status: "down", responseTime: 400 });
const result = await service.updateMonitorStats(monitor, statusResponse, true);
expect(findOneSpy).toHaveBeenCalledWith({ monitorId: monitor._id });
expect(saveMock).toHaveBeenCalledTimes(1);
expect(result).toBe(stats);
expect(stats.totalChecks).toBe(11);
expect(stats.totalUpChecks).toBe(7);
expect(stats.totalDownChecks).toBe(4);
expect(stats.currentStreak).toBe(1);
expect(stats.currentStreakStatus).toBe("down");
expect(stats.currentStreakStartedAt).toBe(now.getTime());
expect(stats.lastCheckTimestamp).toBe(now.getTime());
expect(stats.timeOfLastFailure).toBe(now.getTime());
expect(stats.avgResponseTime).toBeCloseTo((200 * 10 + 400) / 11);
expect(stats.uptimePercentage).toBeCloseTo(7 / 11);
expect(stats.lastResponseTime).toBe(400);
expect(stats.maxResponseTime).toBe(400);
});
});
// Additional coverage: evaluateThresholds branches
describe("StatusService.evaluateThresholds", () => {
const service = new StatusService();
const buildCheck = (systemOverrides = {}) => ({
system: {
cpu: { usage_percent: 0.2, temperature: [40, 45] },
memory: { usage_percent: 0.3 },
disk: [{ usage_percent: 0.4 }],
...systemOverrides,
},
});
it("returns early when monitor is not infrastructure or not up", async () => {
const { monitor } = buildMonitor({ type: "http", status: "up", thresholds: { cpu: 50 } });
const check = buildCheck();
const res1 = await service.evaluateThresholds(monitor, check);
expect(res1).toEqual({ hasBreach: false, notes: [], metrics: expect.any(Object) });
const { monitor: m2 } = buildMonitor({ type: "infrastructure", status: "down", thresholds: { cpu: 50 } });
const res2 = await service.evaluateThresholds(m2, check);
expect(res2).toEqual({ hasBreach: false, notes: [], metrics: expect.any(Object) });
});
it("returns early when thresholds or system missing", async () => {
const { monitor } = buildMonitor({ type: "infrastructure", status: "up" });
const check = buildCheck();
const res1 = await service.evaluateThresholds(monitor, check);
expect(res1.hasBreach).toBe(false);
const { monitor: m2 } = buildMonitor({ type: "infrastructure", status: "up", thresholds: { cpu: 50 } });
const res2 = await service.evaluateThresholds(m2, { system: undefined });
expect(res2.hasBreach).toBe(false);
});
it("detects cpu/memory/disk/temperature breaches and fills notes", async () => {
const { monitor } = buildMonitor({
type: "infrastructure",
status: "up",
thresholds: { cpu: 50, memory: 60, disk: 80, temperature: 70 },
});
const check = buildCheck({
cpu: { usage_percent: 0.8, temperature: [60, 75] }, // 80% & 75C
memory: { usage_percent: 0.7 }, // 70%
disk: [{ usage_percent: 0.5 }, { usage_percent: 0.95 }], // max 95%
});
const res = await service.evaluateThresholds(monitor, check);
expect(res.hasBreach).toBe(true);
expect(res.notes).toEqual(
expect.arrayContaining([
"cpu 80% > 50%",
"memory 70% > 60%",
"disk 95.00% > 80%",
"temperature 75C > 70C",
])
);
expect(res.metrics.cpu).toMatchObject({ value: 80, threshold: 50, breached: true });
expect(res.metrics.memory).toMatchObject({ value: 70, threshold: 60, breached: true });
expect(res.metrics.disk).toMatchObject({ value: 95, threshold: 80, breached: true });
});
it("handles non-breach values and undefined fields safely", async () => {
const { monitor } = buildMonitor({
type: "infrastructure",
status: "up",
thresholds: { cpu: 95, memory: 95, disk: 99, temperature: 100 },
});
const check = buildCheck({
cpu: { usage_percent: undefined, temperature: [] },
memory: {},
disk: [{}, { usage_percent: 0.1 }], // max 10%
});
const res = await service.evaluateThresholds(monitor, check);
expect(res.hasBreach).toBe(false);
expect(res.notes).toEqual([]);
expect(res.metrics.cpu.breached).toBe(false);
expect(res.metrics.memory.breached).toBe(false);
expect(res.metrics.disk.breached).toBe(false);
});
it("returns safe default when unexpected error occurs", async () => {
const { monitor } = buildMonitor({
type: "infrastructure",
status: "up",
thresholds: { cpu: 50 },
});
// Create a system object that throws when accessed to exercise catch {}
const throwingSystem = new Proxy(
{},
{
get() {
throw new Error("boom");
},
}
);
const res = await service.evaluateThresholds(monitor, { system: throwingSystem });
expect(res).toEqual({ hasBreach: false, notes: [], metrics: expect.any(Object) });
});
});