diff --git a/server/test/unit/services/checkEvaluator.test.ts b/server/test/unit/services/checkEvaluator.test.ts new file mode 100644 index 000000000..a22ceb1c7 --- /dev/null +++ b/server/test/unit/services/checkEvaluator.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { CheckEvaluator } from "../../../src/worker/worker.check-evaluator.ts"; +import { MonitorStatusPolicy } from "../../../src/worker/worker.monitor-status-policy.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "m1", + teamId: "team", + type: "http", + interval: 60000, + status: "up", + ...overrides, + }) as Monitor; + +const createEvaluator = (overrides?: Record) => { + const defaults = { + statusService: { + updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: makeMonitor({ status: "up" }), statusChanged: false, prevStatus: "up", code: 200 }), + }, + monitorStatusPolicy: new MonitorStatusPolicy(), + ...overrides, + }; + const evaluator = new CheckEvaluator(defaults.statusService as any, defaults.monitorStatusPolicy as any); + return { evaluator, defaults }; +}; + +describe("CheckEvaluator", () => { + it("threads status, check, and monitor through to the status service", async () => { + const { evaluator, defaults } = createEvaluator(); + const status = { monitorId: "m1", status: true, code: 200, message: "OK" } as any; + const check = { id: "check-1" } as any; + const monitor = makeMonitor({ status: "up" }); + + await evaluator.evaluate(status, check, monitor); + + expect(defaults.statusService.updateMonitorStatus).toHaveBeenCalledWith(status, check, monitor); + }); + + it("returns an evaluation that opens an incident on a down transition", async () => { + const status = { monitorId: "m1", status: false, code: 500, message: "Error" } as any; + const check = { id: "check-1" } as any; + const statusChange = { monitor: makeMonitor({ status: "down" }), statusChanged: true, prevStatus: "up", code: 500 }; + const { evaluator } = createEvaluator({ + statusService: { updateMonitorStatus: jest.fn().mockResolvedValue(statusChange) }, + }); + + const result = await evaluator.evaluate(status, check, makeMonitor()); + + expect(result).toMatchObject({ + monitor: statusChange.monitor, + status, + check, + statusChange, + decision: expect.objectContaining({ + shouldCreateIncident: true, + shouldSendNotification: true, + incidentReason: "status_down", + }), + }); + }); + + it("returns an evaluation that resolves an incident on recovery", async () => { + const status = { monitorId: "m1", status: true, code: 200, message: "OK" } as any; + const statusChange = { monitor: makeMonitor({ status: "up" }), statusChanged: true, prevStatus: "down", code: 200 }; + const { evaluator } = createEvaluator({ + statusService: { updateMonitorStatus: jest.fn().mockResolvedValue(statusChange) }, + }); + + const result = await evaluator.evaluate(status, { id: "check-1" } as any, makeMonitor({ status: "down" })); + + expect(result.decision).toMatchObject({ shouldResolveIncident: true, shouldSendNotification: true }); + }); + + it("produces a no-op decision when status did not change", async () => { + const status = { monitorId: "m1", status: true, code: 200, message: "OK" } as any; + const { evaluator } = createEvaluator(); + + const result = await evaluator.evaluate(status, { id: "check-1" } as any, makeMonitor()); + + expect(result.decision).toMatchObject({ + shouldCreateIncident: false, + shouldResolveIncident: false, + shouldSendNotification: false, + }); + }); +}); diff --git a/server/test/unit/services/checkPipeline.test.ts b/server/test/unit/services/checkPipeline.test.ts index 645d58787..008e879fa 100644 --- a/server/test/unit/services/checkPipeline.test.ts +++ b/server/test/unit/services/checkPipeline.test.ts @@ -20,114 +20,6 @@ const activeWindow = () => { return { active: true, start: new Date(now - 1000).toISOString(), end: new Date(now + 1000).toISOString(), repeat: 0 }; }; -// ── CheckPipeline ──────────────────────────────────────────────────────────── - -const createCheckPipeline = (overrides?: Record) => { - const defaults = { - logger: createMockLogger(), - networkService: { requestStatus: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) }, - statusService: { - updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: { id: "m1", status: "up" }, statusChanged: false, prevStatus: "up", code: 200 }), - }, - checkService: { toCheck: jest.fn().mockReturnValue({ id: "check-1" }) }, - buffer: { addToBuffer: jest.fn() }, - monitorsRepository: { updateById: jest.fn().mockResolvedValue({}) }, - maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([]) }, - monitorStatusPolicy: new MonitorStatusPolicy(), - ...overrides, - }; - const checkProducer = new CheckProducer( - defaults.monitorsRepository as any, - defaults.maintenanceWindowsRepository as any, - defaults.checkService as any, - defaults.networkService as any, - defaults.buffer as any, - defaults.logger as any - ); - const checkEvaluator = new CheckEvaluator(defaults.statusService as any, defaults.monitorStatusPolicy as any); - const pipeline = new CheckPipeline(checkProducer, checkEvaluator); - return { pipeline, defaults }; -}; - -describe("CheckPipeline", () => { - it("throws when monitor id is missing", async () => { - const { pipeline } = createCheckPipeline(); - await expect(pipeline.run({} as Monitor)).rejects.toThrow("No monitor id"); - }); - - it("skips and sets maintenance status when in maintenance window", async () => { - const { pipeline, defaults } = createCheckPipeline({ - maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) }, - }); - - const result = await pipeline.run(makeMonitor({ status: "up" })); - - expect(result).toBeNull(); - expect(defaults.networkService.requestStatus).not.toHaveBeenCalled(); - expect(defaults.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { status: "maintenance", statusWindow: [] }); - }); - - it("does not re-write status when already in maintenance", async () => { - const { pipeline, defaults } = createCheckPipeline({ - maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) }, - }); - - await pipeline.run(makeMonitor({ status: "maintenance" })); - - expect(defaults.monitorsRepository.updateById).not.toHaveBeenCalled(); - }); - - it("throws when network response is null", async () => { - const { pipeline } = createCheckPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(null) }, - }); - await expect(pipeline.run(makeMonitor())).rejects.toThrow("No network response"); - }); - - it("returns null and warns when toCheck returns null", async () => { - const { pipeline, defaults } = createCheckPipeline({ - checkService: { toCheck: jest.fn().mockReturnValue(null) }, - }); - - const result = await pipeline.run(makeMonitor()); - - expect(result).toBeNull(); - expect(defaults.logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("No check could be built") })); - expect(defaults.statusService.updateMonitorStatus).not.toHaveBeenCalled(); - }); - - it("buffers the check, updates status, and returns an evaluation", async () => { - const status = { monitorId: "m1", status: false, code: 500, message: "Error" }; - const statusChange = { monitor: { id: "m1", status: "down" }, statusChanged: true, prevStatus: "up", code: 500 }; - const { pipeline, defaults } = createCheckPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(status) }, - statusService: { updateMonitorStatus: jest.fn().mockResolvedValue(statusChange) }, - }); - - const result = await pipeline.run(makeMonitor()); - - expect(defaults.buffer.addToBuffer).toHaveBeenCalledWith({ id: "check-1" }); - expect(defaults.statusService.updateMonitorStatus).toHaveBeenCalled(); - expect(result).toMatchObject({ - monitor: statusChange.monitor, - status, - check: { id: "check-1" }, - statusChange, - decision: expect.objectContaining({ shouldCreateIncident: true, shouldSendNotification: true, incidentReason: "status_down" }), - }); - }); - - it("proceeds with the check when no maintenance window is active", async () => { - const { pipeline, defaults } = createCheckPipeline({ - maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([{ ...activeWindow(), active: false }]) }, - }); - - await pipeline.run(makeMonitor()); - - expect(defaults.networkService.requestStatus).toHaveBeenCalled(); - }); -}); - // ── GeoChecksPipeline ────────────────────────────────────────────────────────── const createGeoPipeline = (overrides?: Record) => { diff --git a/server/test/unit/services/checkProducer.test.ts b/server/test/unit/services/checkProducer.test.ts index bf93c8ea2..4edd12bb5 100644 --- a/server/test/unit/services/checkProducer.test.ts +++ b/server/test/unit/services/checkProducer.test.ts @@ -56,7 +56,7 @@ describe("CheckProducer", () => { const result = await producer.produce(makeMonitor({ status: "up" })); expect(result).toBeNull(); - expect(defaults.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { status: "maintenance" }); + expect(defaults.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { status: "maintenance", statusWindow: [] }); expect(defaults.networkService.requestStatus).not.toHaveBeenCalled(); expect(defaults.buffer.addToBuffer).not.toHaveBeenCalled(); }); diff --git a/server/test/unit/services/statusService.test.ts b/server/test/unit/services/statusService.test.ts index 99db9072a..5712de301 100644 --- a/server/test/unit/services/statusService.test.ts +++ b/server/test/unit/services/statusService.test.ts @@ -564,7 +564,7 @@ describe("StatusService", () => { const { service, monitorsRepository } = createService(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -579,7 +579,7 @@ describe("StatusService", () => { const { service, monitorsRepository } = createService(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -599,7 +599,7 @@ describe("StatusService", () => { const { service, monitorsRepository } = createService(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor); expect(result.monitor.status).toBe("up"); });