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