449 lines
17 KiB
TypeScript
449 lines
17 KiB
TypeScript
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<IMonitorsRepository>;
|
|
|
|
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<IChecksRepository>;
|
|
|
|
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>): 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);
|
|
});
|
|
});
|
|
});
|