import { describe, expect, it, jest } from "@jest/globals"; import { CheckService } from "../../../src/domain/checks/check.service.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; import type { IChecksRepository } from "../../../src/domain/checks/check.repository.interface.ts"; import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts"; import type { MonitorStatusResponse, HardwareStatusPayload, PageSpeedStatusPayload } from "../../../src/types/network.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── const createMonitorsRepo = () => ({ findById: jest.fn().mockResolvedValue({}), }) as unknown as jest.Mocked; const createChecksRepo = () => ({ createChecks: jest.fn().mockImplementation((checks: unknown) => Promise.resolve(checks)), findByMonitorId: jest.fn().mockResolvedValue({ checks: [], count: 0 }), findByTeamId: jest.fn().mockResolvedValue({ checks: [], count: 0 }), findSummaryByTeamId: jest.fn().mockResolvedValue({}), deleteByMonitorId: jest.fn().mockResolvedValue(5), deleteByTeamId: jest.fn().mockResolvedValue(10), deleteOlderThan: jest.fn().mockResolvedValue(3), }) as unknown as jest.Mocked; const createService = () => { const logger = createMockLogger(); const monitorsRepository = createMonitorsRepo(); const checksRepository = createChecksRepo(); const service = new CheckService(monitorsRepository, logger as any, checksRepository); return { service, logger, monitorsRepository, checksRepository }; }; const makeStatusResponse = (overrides?: Partial): MonitorStatusResponse => ({ monitorId: "mon-1", teamId: "team-1", type: "http", status: true, code: 200, message: "OK", responseTime: 100, ...overrides, }) as MonitorStatusResponse; // ── Tests ──────────────────────────────────────────────────────────────────── describe("CheckService", () => { // ── toCheck ────────────────────────────────────────────────────────────── describe("toCheck", () => { it("builds a basic HTTP check with correct fields", () => { const { service } = createService(); const check = service.toCheck(makeStatusResponse()); expect(check).toBeDefined(); expect(check!.metadata).toEqual({ monitorId: "mon-1", teamId: "team-1", type: "http" }); expect(check!.status).toBe(true); expect(check!.statusCode).toBe(200); expect(check!.responseTime).toBe(100); expect(check!.message).toBe("OK"); expect(check!.id).toBeDefined(); expect(check!.createdAt).toBeDefined(); }); it("defaults responseTime to 0 when falsy", () => { const { service } = createService(); const check = service.toCheck(makeStatusResponse({ responseTime: undefined })); expect(check!.responseTime).toBe(0); }); it("includes timings when provided", () => { const timings = { start: 0, socket: 10, lookup: 20 }; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ timings } as any)); expect(check!.timings).toBe(timings); }); // ── PageSpeed ──────────────────────────────────────────────────────── describe("pagespeed type", () => { it("extracts category scores and audits from lighthouse result", () => { const payload: PageSpeedStatusPayload = { lighthouseResult: { categories: { accessibility: { score: 0.9 }, "best-practices": { score: 0.8 }, seo: { score: 0.95 }, performance: { score: 0.7 }, }, audits: { "cumulative-layout-shift": { id: "cls", title: "CLS", score: 0.1, displayValue: "0.1", numericValue: 0.1, numericUnit: "unitless" }, "speed-index": { id: "si", title: "SI", score: 0.5, displayValue: "3.0s", numericValue: 3000, numericUnit: "millisecond" }, "first-contentful-paint": { id: "fcp", title: "FCP", score: 0.8 }, "largest-contentful-paint": { id: "lcp", title: "LCP", score: 0.6 }, "total-blocking-time": { id: "tbt", title: "TBT", score: 0.9 }, }, }, } as any; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "pagespeed", payload } as any)); expect(check!.accessibility).toBe(90); expect(check!.bestPractices).toBe(80); expect(check!.seo).toBe(95); expect(check!.performance).toBe(70); expect(check!.audits!.cls).toEqual(expect.objectContaining({ id: "cls", score: 0.1, numericValue: 0.1 })); expect(check!.audits!.si).toEqual(expect.objectContaining({ id: "si", numericValue: 3000 })); expect(check!.audits!.fcp).toEqual(expect.objectContaining({ id: "fcp" })); }); it("returns undefined when pagespeed payload is missing", () => { const { service, logger } = createService(); const check = service.toCheck(makeStatusResponse({ type: "pagespeed", payload: undefined } as any)); expect(check).toBeUndefined(); expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Failed to build check" })); }); it("defaults categories and audits to empty when lighthouseResult is missing", () => { const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "pagespeed", payload: {} } as any)); expect(check!.accessibility).toBe(0); expect(check!.bestPractices).toBe(0); expect(check!.seo).toBe(0); expect(check!.performance).toBe(0); expect(check!.audits!.cls).toBeUndefined(); }); it("handles audit with non-number score (string)", () => { const payload = { lighthouseResult: { categories: {}, audits: { "cumulative-layout-shift": { id: "cls", title: "CLS", score: "informative" }, }, }, } as any; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "pagespeed", payload } as any)); expect(check!.audits!.cls!.score).toBe("informative"); }); it("handles audit with null score", () => { const payload = { lighthouseResult: { categories: {}, audits: { "cumulative-layout-shift": { id: "cls", title: "CLS", score: null }, }, }, } as any; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "pagespeed", payload } as any)); expect(check!.audits!.cls!.score).toBeNull(); }); it("handles audit with non-number numericValue", () => { const payload = { lighthouseResult: { categories: {}, audits: { "speed-index": { id: "si", title: "SI", score: 0.5, numericValue: "n/a" }, }, }, } as any; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "pagespeed", payload } as any)); expect(check!.audits!.si!.numericValue).toBeUndefined(); }); it("returns undefined for non-object audit", () => { const payload = { lighthouseResult: { categories: {}, audits: { "cumulative-layout-shift": null, "speed-index": "not an object", }, }, } as any; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "pagespeed", payload } as any)); expect(check!.audits!.cls).toBeUndefined(); expect(check!.audits!.si).toBeUndefined(); }); }); // ── Hardware ───────────────────────────────────────────────────────── describe("hardware type", () => { it("extracts cpu, memory, disk, host, net, and capture from payload", () => { const payload: HardwareStatusPayload = { data: { cpu: { usage_percent: 0.5 }, memory: { usage_percent: 0.6 }, disk: [{ device: "/dev/sda", usage_percent: 0.7 }], host: { os: "linux" }, net: [{ name: "eth0" }], }, capture: { screenshot: "base64data" }, } as any; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "hardware", payload } as any)); expect(check!.cpu).toEqual({ usage_percent: 0.5 }); expect(check!.memory).toEqual({ usage_percent: 0.6 }); expect(check!.disk).toHaveLength(1); expect(check!.host).toEqual({ os: "linux" }); expect(check!.net).toHaveLength(1); expect(check!.capture).toEqual({ screenshot: "base64data" }); }); it("extracts errors from array format", () => { const payload = { data: {}, errors: [{ message: "timeout" }], } as any; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "hardware", payload } as any)); expect(check!.errors).toEqual([{ message: "timeout" }]); }); it("extracts errors from nested object format", () => { const payload = { data: {}, errors: { errors: [{ message: "nested error" }] }, } as any; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "hardware", payload } as any)); expect(check!.errors).toEqual([{ message: "nested error" }]); }); it("handles undefined payload gracefully", () => { const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "hardware", payload: undefined } as any)); expect(check).toBeDefined(); expect(check!.cpu).toBeUndefined(); expect(check!.memory).toBeUndefined(); expect(check!.disk).toBeUndefined(); expect(check!.errors).toBeUndefined(); }); }); describe("docker type", () => { it("extracts containers and containerSummary from payload", () => { const payload = { containers: [ { id: "abc", name: "web", image: "nginx:latest", state: "running", status: "Up 3 hours", health: "healthy" }, { id: "def", name: "db", image: "mongo:8.0", state: "exited", status: "Exited (0)", health: "none" }, ], summary: { total: 2, running: 1, stopped: 1, unhealthy: 0 }, } as any; const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "docker", payload } as any)); expect(check!.containers).toHaveLength(2); expect(check!.containers![0]).toMatchObject({ id: "abc", state: "running", health: "healthy" }); expect(check!.containerSummary).toEqual({ total: 2, running: 1, stopped: 1, unhealthy: 0 }); }); it("handles a null payload gracefully (down check)", () => { const { service } = createService(); const check = service.toCheck(makeStatusResponse({ type: "docker", payload: null } as any)); expect(check).toBeDefined(); expect(check!.containers).toBeUndefined(); expect(check!.containerSummary).toBeUndefined(); }); }); }); // ── createChecks ───────────────────────────────────────────────────────── describe("createChecks", () => { it("delegates to repository", async () => { const { service, checksRepository } = createService(); const checks = [{ id: "c1" }] as any; await service.createChecks(checks); expect(checksRepository.createChecks).toHaveBeenCalledWith(checks); }); }); // ── getChecksByMonitor ─────────────────────────────────────────────────── describe("getChecksByMonitor", () => { it("returns checks from repository", async () => { const expected = { checks: [{ id: "c1" }], count: 1 }; const { service, checksRepository } = createService(); (checksRepository.findByMonitorId as jest.Mock).mockResolvedValue(expected); const result = await service.getChecksByMonitor({ monitorId: "mon-1", teamId: "team-1", sortOrder: "desc", dateRange: "day", page: 0, rowsPerPage: 10, }); expect(result).toBe(expected); }); it("defaults page to 0 and rowsPerPage to 5 when nullish", async () => { const { service, checksRepository } = createService(); await service.getChecksByMonitor({ monitorId: "mon-1", teamId: "team-1", sortOrder: "desc", dateRange: "day", page: null as any, rowsPerPage: null as any, }); expect(checksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "desc", "day", 0, 5, undefined, undefined); }); it("passes filter and status parameters", async () => { const { service, checksRepository } = createService(); await service.getChecksByMonitor({ monitorId: "mon-1", teamId: "team-1", sortOrder: "asc", dateRange: "week", page: 2, rowsPerPage: 20, filter: "resolve", status: false, }); expect(checksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "asc", "week", 2, 20, false, "resolve"); }); it("throws when monitorId is missing", async () => { const { service } = createService(); await expect( service.getChecksByMonitor({ monitorId: "", teamId: "team-1", sortOrder: "desc", dateRange: "day", page: 0, rowsPerPage: 10 }) ).rejects.toThrow("No monitor ID in request"); }); it("throws when teamId is missing", async () => { const { service } = createService(); await expect( service.getChecksByMonitor({ monitorId: "mon-1", teamId: "", sortOrder: "desc", dateRange: "day", page: 0, rowsPerPage: 10 }) ).rejects.toThrow("No team ID in request"); }); it("verifies monitor belongs to team via repository", async () => { const { service, monitorsRepository } = createService(); await service.getChecksByMonitor({ monitorId: "mon-1", teamId: "team-1", sortOrder: "desc", dateRange: "day", page: 0, rowsPerPage: 10, }); expect(monitorsRepository.findById).toHaveBeenCalledWith("mon-1", "team-1"); }); }); // ── getChecksByTeam ────────────────────────────────────────────────────── describe("getChecksByTeam", () => { it("returns checks from repository", async () => { const expected = { checks: [], count: 0 }; const { service, checksRepository } = createService(); (checksRepository.findByTeamId as jest.Mock).mockResolvedValue(expected); const result = await service.getChecksByTeam({ teamId: "team-1", sortOrder: "desc", dateRange: "day", page: 0, rowsPerPage: 10 }); expect(result).toBe(expected); }); it("defaults page to 0 and rowsPerPage to 5 when nullish", async () => { const { service, checksRepository } = createService(); await service.getChecksByTeam({ teamId: "team-1", sortOrder: "desc", dateRange: "day", page: null as any, rowsPerPage: null as any }); expect(checksRepository.findByTeamId).toHaveBeenCalledWith("desc", "day", 0, 5, "team-1", undefined); }); }); // ── getChecksSummaryByTeamId ────────────────────────────────────────────── describe("getChecksSummaryByTeamId", () => { it("delegates to repository", async () => { const summary = { total: 5, up: 4, down: 1 }; const { service, checksRepository } = createService(); (checksRepository.findSummaryByTeamId as jest.Mock).mockResolvedValue(summary); const result = await service.getChecksSummaryByTeamId({ teamId: "team-1", dateRange: "day" }); expect(result).toBe(summary); expect(checksRepository.findSummaryByTeamId).toHaveBeenCalledWith("team-1", "day"); }); }); // ── deleteChecks ───────────────────────────────────────────────────────── describe("deleteChecks", () => { it("verifies monitor ownership and deletes", async () => { const { service, monitorsRepository, checksRepository } = createService(); const result = await service.deleteChecks({ monitorId: "mon-1", teamId: "team-1" }); expect(monitorsRepository.findById).toHaveBeenCalledWith("mon-1", "team-1"); expect(checksRepository.deleteByMonitorId).toHaveBeenCalledWith("mon-1"); expect(result).toBe(5); }); }); describe("deleteChecksByTeamId", () => { it("delegates to repository", async () => { const { service, checksRepository } = createService(); const result = await service.deleteChecksByTeamId({ teamId: "team-1" }); expect(checksRepository.deleteByTeamId).toHaveBeenCalledWith("team-1"); expect(result).toBe(10); }); }); describe("deleteOlderThan", () => { it("delegates to repository", async () => { const date = new Date(); const { service, checksRepository } = createService(); const result = await service.deleteOlderThan(date); expect(checksRepository.deleteOlderThan).toHaveBeenCalledWith(date); expect(result).toBe(3); }); }); });