Files
Checkmate/server/tests/services/infrastructure/StatusService.test.js
T
2025-11-24 19:19:27 +00:00

263 lines
11 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);
});
});