add notificationsService tests
This commit is contained in:
1 parent
7329306ccf
commit
ba5368f942
9 files changed
+806
-31
No files matched your search
@@ -6,6 +6,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
||||
"test:services": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/service/business/*.ts' --collectCoverageFrom='src/service/infrastructure/statusService.ts' --collectCoverageFrom='src/service/infrastructure/notificationsService.ts' --collectCoverageFrom='src/service/infrastructure/SuperSimpleQueue/*.ts' test/unit/services",
|
||||
"dev": "nodemon --exec tsx src/index.js",
|
||||
"start": "node --watch ./dist/index.js",
|
||||
"build": "tsc && tsc-alias && cp -r src/templates dist/templates",
|
||||
|
||||
@@ -107,16 +107,16 @@ export class CheckService implements ICheckService {
|
||||
numericUnit: audit.numericUnit,
|
||||
};
|
||||
};
|
||||
check.accessibility = (categories?.accessibility?.score || 0) * 100;
|
||||
check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100;
|
||||
check.seo = (categories?.seo?.score || 0) * 100;
|
||||
check.performance = (categories?.performance?.score || 0) * 100;
|
||||
check.accessibility = (categories.accessibility?.score || 0) * 100;
|
||||
check.bestPractices = (categories["best-practices"]?.score || 0) * 100;
|
||||
check.seo = (categories.seo?.score || 0) * 100;
|
||||
check.performance = (categories.performance?.score || 0) * 100;
|
||||
check.audits = {
|
||||
cls: mapAudit(audits?.["cumulative-layout-shift"]),
|
||||
si: mapAudit(audits?.["speed-index"]),
|
||||
fcp: mapAudit(audits?.["first-contentful-paint"]),
|
||||
lcp: mapAudit(audits?.["largest-contentful-paint"]),
|
||||
tbt: mapAudit(audits?.["total-blocking-time"]),
|
||||
cls: mapAudit(audits["cumulative-layout-shift"]),
|
||||
si: mapAudit(audits["speed-index"]),
|
||||
fcp: mapAudit(audits["first-contentful-paint"]),
|
||||
lcp: mapAudit(audits["largest-contentful-paint"]),
|
||||
tbt: mapAudit(audits["total-blocking-time"]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export class CheckService implements ICheckService {
|
||||
const hardwarePayload = payload as HardwareStatusPayload | undefined;
|
||||
const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {};
|
||||
const errorsSource = Array.isArray(hardwarePayload?.errors)
|
||||
? hardwarePayload?.errors
|
||||
? hardwarePayload.errors
|
||||
: (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors;
|
||||
check.cpu = cpu;
|
||||
check.memory = memory;
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
||||
import type { Notification } from "../../src/types/notification.ts";
|
||||
import type { NotificationMessage } from "../../src/types/notificationMessage.ts";
|
||||
import { createMockLogger } from "../helpers/createMockLogger.ts";
|
||||
import type { Notification } from "../../../src/types/notification.ts";
|
||||
import type { NotificationMessage } from "../../../src/types/notificationMessage.ts";
|
||||
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
||||
|
||||
const mockPost = jest.fn();
|
||||
jest.unstable_mockModule("got", () => ({
|
||||
@@ -9,7 +9,7 @@ jest.unstable_mockModule("got", () => ({
|
||||
HTTPError: class HTTPError extends Error {},
|
||||
}));
|
||||
|
||||
const { TeamsProvider } = await import("../../src/service/infrastructure/notificationProviders/teams.ts");
|
||||
const { TeamsProvider } = await import("../../../src/service/infrastructure/notificationProviders/teams.ts");
|
||||
|
||||
const createNotification = (overrides?: Partial<Notification>): Notification => ({
|
||||
id: "notif-1",
|
||||
@@ -0,0 +1,427 @@
|
||||
import { describe, expect, it, jest } from "@jest/globals";
|
||||
import { CheckService } from "../../../src/service/business/checkService.ts";
|
||||
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
||||
import type { IChecksRepository, IMonitorsRepository } from "../../../src/repositories/index.ts";
|
||||
import type { MonitorStatusResponse, HardwareStatusPayload, PageSpeedStatusPayload } from "../../../src/types/index.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", () => {
|
||||
describe("serviceName", () => {
|
||||
it("returns checkService", () => {
|
||||
const { service } = createService();
|
||||
expect(service.serviceName).toBe("checkService");
|
||||
});
|
||||
});
|
||||
|
||||
// ── buildCheck ───────────────────────────────────────────────────────────
|
||||
|
||||
describe("buildCheck", () => {
|
||||
it("builds a basic HTTP check with correct fields", () => {
|
||||
const { service } = createService();
|
||||
const check = service.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(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.buildCheck(makeStatusResponse({ type: "hardware", payload } as any));
|
||||
|
||||
expect(check!.errors).toEqual([{ message: "nested error" }]);
|
||||
});
|
||||
|
||||
it("handles undefined payload gracefully", () => {
|
||||
const { service } = createService();
|
||||
const check = service.buildCheck(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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", undefined, 0, 5, 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: "error",
|
||||
status: false,
|
||||
});
|
||||
|
||||
expect(checksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "asc", "week", "error", 2, 20, false);
|
||||
});
|
||||
|
||||
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", undefined, 0, 5, "team-1");
|
||||
});
|
||||
});
|
||||
|
||||
// ── 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+7
-7
@@ -1,11 +1,11 @@
|
||||
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
||||
import { IncidentService } from "../../src/service/business/incidentService.ts";
|
||||
import { createMockLogger } from "../helpers/createMockLogger.ts";
|
||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "../../src/repositories/index.ts";
|
||||
import type { INotificationMessageBuilder } from "../../src/service/infrastructure/notificationMessageBuilder.ts";
|
||||
import type { Monitor } from "../../src/types/monitor.ts";
|
||||
import type { Incident } from "../../src/types/index.ts";
|
||||
import type { MonitorActionDecision } from "../../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts";
|
||||
import { IncidentService } from "../../../src/service/business/incidentService.ts";
|
||||
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "../../../src/repositories/index.ts";
|
||||
import type { INotificationMessageBuilder } from "../../../src/service/infrastructure/notificationMessageBuilder.ts";
|
||||
import type { Monitor } from "../../../src/types/monitor.ts";
|
||||
import type { Incident } from "../../../src/types/index.ts";
|
||||
import type { MonitorActionDecision } from "../../../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import { MonitorService } from "../../src/service/business/monitorService.ts";
|
||||
import { MonitorService } from "../../../src/service/business/monitorService.ts";
|
||||
import type {
|
||||
IChecksRepository,
|
||||
IGeoChecksRepository,
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
IMonitorStatsRepository,
|
||||
IMonitorsRepository,
|
||||
IStatusPagesRepository,
|
||||
} from "../../src/repositories/index.ts";
|
||||
} from "../../../src/repositories/index.ts";
|
||||
|
||||
const createMonitorsRepositoryMock = () =>
|
||||
({
|
||||
@@ -0,0 +1,347 @@
|
||||
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
||||
import { NotificationsService } from "../../../src/service/infrastructure/notificationsService.ts";
|
||||
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
||||
import type { Notification, Monitor, MonitorStatusResponse } from "../../../src/types/index.ts";
|
||||
import type { MonitorActionDecision } from "../../../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const createProvider = () => ({
|
||||
sendMessage: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
|
||||
sendTestAlert: jest.fn<() => Promise<boolean>>().mockResolvedValue(true),
|
||||
});
|
||||
|
||||
const createNotificationsRepo = () => ({
|
||||
create: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
findNotificationsByIds: jest.fn(),
|
||||
findByTeamId: jest.fn(),
|
||||
updateById: jest.fn(),
|
||||
deleteById: jest.fn(),
|
||||
});
|
||||
|
||||
const createMonitorsRepo = () => ({
|
||||
removeNotificationFromMonitors: jest.fn(),
|
||||
});
|
||||
|
||||
const createSettingsService = (clientHost = "https://app.example.com") => ({
|
||||
getSettings: jest.fn().mockReturnValue({ clientHost }),
|
||||
});
|
||||
|
||||
const createMessageBuilder = () => ({
|
||||
buildMessage: jest.fn().mockReturnValue({ type: "monitor_down", content: { title: "Down" } }),
|
||||
extractThresholdBreaches: jest.fn(),
|
||||
});
|
||||
|
||||
const createService = (overrides?: Record<string, unknown>) => {
|
||||
const logger = createMockLogger();
|
||||
const notificationsRepository = createNotificationsRepo();
|
||||
const monitorsRepository = createMonitorsRepo();
|
||||
const webhookProvider = createProvider();
|
||||
const emailProvider = createProvider();
|
||||
const slackProvider = createProvider();
|
||||
const discordProvider = createProvider();
|
||||
const pagerDutyProvider = createProvider();
|
||||
const matrixProvider = createProvider();
|
||||
const teamsProvider = createProvider();
|
||||
const telegramProvider = createProvider();
|
||||
const settingsService = createSettingsService();
|
||||
const notificationMessageBuilder = createMessageBuilder();
|
||||
|
||||
const defaults = {
|
||||
logger,
|
||||
notificationsRepository,
|
||||
monitorsRepository,
|
||||
webhookProvider,
|
||||
emailProvider,
|
||||
slackProvider,
|
||||
discordProvider,
|
||||
pagerDutyProvider,
|
||||
matrixProvider,
|
||||
teamsProvider,
|
||||
telegramProvider,
|
||||
settingsService,
|
||||
notificationMessageBuilder,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
const service = new NotificationsService(
|
||||
defaults.notificationsRepository as any,
|
||||
defaults.monitorsRepository as any,
|
||||
defaults.webhookProvider as any,
|
||||
defaults.emailProvider as any,
|
||||
defaults.slackProvider as any,
|
||||
defaults.discordProvider as any,
|
||||
defaults.pagerDutyProvider as any,
|
||||
defaults.matrixProvider as any,
|
||||
defaults.teamsProvider as any,
|
||||
defaults.telegramProvider as any,
|
||||
defaults.settingsService as any,
|
||||
defaults.logger as any,
|
||||
defaults.notificationMessageBuilder as any
|
||||
);
|
||||
|
||||
return { service, ...defaults };
|
||||
};
|
||||
|
||||
const makeNotification = (overrides?: Partial<Notification>): Notification =>
|
||||
({
|
||||
id: "notif-1",
|
||||
userId: "user-1",
|
||||
teamId: "team-1",
|
||||
type: "email",
|
||||
notificationName: "Email Alert",
|
||||
address: "test@example.com",
|
||||
createdAt: "2026-01-01T00:00:00Z",
|
||||
updatedAt: "2026-01-01T00:00:00Z",
|
||||
...overrides,
|
||||
}) as Notification;
|
||||
|
||||
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
|
||||
({
|
||||
id: "mon-1",
|
||||
teamId: "team-1",
|
||||
name: "Test Monitor",
|
||||
type: "http",
|
||||
notifications: ["notif-1"],
|
||||
...overrides,
|
||||
}) as Monitor;
|
||||
|
||||
const makeDecision = (overrides?: Partial<MonitorActionDecision>): MonitorActionDecision => ({
|
||||
shouldCreateIncident: false,
|
||||
shouldResolveIncident: false,
|
||||
shouldSendNotification: true,
|
||||
incidentReason: null,
|
||||
notificationReason: "status_change",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeStatusResponse = () => ({ monitorId: "mon-1", status: false, code: 500 }) as unknown as MonitorStatusResponse;
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("NotificationsService", () => {
|
||||
// ── handleNotifications ───────────────────────────────────────────────────
|
||||
|
||||
describe("handleNotifications", () => {
|
||||
it("returns false when shouldSendNotification is false", async () => {
|
||||
const { service } = createService();
|
||||
const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision({ shouldSendNotification: false }));
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("sends notifications to all configured providers and returns true", async () => {
|
||||
const { service, notificationsRepository, emailProvider } = createService();
|
||||
(notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification({ type: "email" })]);
|
||||
|
||||
const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision());
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(emailProvider.sendMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("routes to correct provider for each notification type", async () => {
|
||||
const types = ["webhook", "slack", "matrix", "pager_duty", "discord", "email", "teams", "telegram"] as const;
|
||||
for (const type of types) {
|
||||
const deps = createService();
|
||||
(deps.notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification({ type })]);
|
||||
|
||||
await deps.service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision());
|
||||
|
||||
const providerMap: Record<string, ReturnType<typeof createProvider>> = {
|
||||
webhook: deps.webhookProvider,
|
||||
slack: deps.slackProvider,
|
||||
matrix: deps.matrixProvider,
|
||||
pager_duty: deps.pagerDutyProvider,
|
||||
discord: deps.discordProvider,
|
||||
email: deps.emailProvider,
|
||||
teams: deps.teamsProvider,
|
||||
telegram: deps.telegramProvider,
|
||||
};
|
||||
expect(providerMap[type].sendMessage).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns false and logs warning for unknown notification type", async () => {
|
||||
const { service, notificationsRepository, logger } = createService();
|
||||
(notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification({ type: "carrier_pigeon" as any })]);
|
||||
|
||||
const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision());
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: expect.stringContaining("Unknown notification type: carrier_pigeon") })
|
||||
);
|
||||
});
|
||||
|
||||
it("returns false and logs warning when notificationMessage is undefined", async () => {
|
||||
const notificationMessageBuilder = createMessageBuilder();
|
||||
notificationMessageBuilder.buildMessage.mockReturnValue(undefined);
|
||||
const { service, notificationsRepository, logger } = createService({ notificationMessageBuilder });
|
||||
(notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification()]);
|
||||
|
||||
const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision());
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Notification message not provided" }));
|
||||
});
|
||||
|
||||
it("handles monitors with no notification IDs", async () => {
|
||||
const { service, notificationsRepository } = createService();
|
||||
(notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([]);
|
||||
|
||||
const result = await service.handleNotifications(makeMonitor({ notifications: undefined as any }), makeStatusResponse(), makeDecision());
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(notificationsRepository.findNotificationsByIds).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it("returns false and logs when some notifications fail", async () => {
|
||||
const { service, notificationsRepository, emailProvider, slackProvider, logger } = createService();
|
||||
(notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([
|
||||
makeNotification({ id: "n1", type: "email" }),
|
||||
makeNotification({ id: "n2", type: "slack" }),
|
||||
]);
|
||||
emailProvider.sendMessage.mockResolvedValue(true);
|
||||
slackProvider.sendMessage.mockResolvedValue(false);
|
||||
|
||||
const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision());
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("1 success, 1 failure") }));
|
||||
});
|
||||
|
||||
it("uses fallback clientHost when settings.clientHost is empty", async () => {
|
||||
const settingsService = createSettingsService("");
|
||||
const { service, notificationsRepository, notificationMessageBuilder } = createService({ settingsService });
|
||||
(notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification()]);
|
||||
|
||||
await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision());
|
||||
|
||||
expect(notificationMessageBuilder.buildMessage).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
"Host not defined"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ── sendTestNotification ─────────────────────────────────────────────────
|
||||
|
||||
describe("sendTestNotification", () => {
|
||||
it.each([["email"], ["slack"], ["discord"], ["pager_duty"], ["matrix"], ["webhook"], ["teams"], ["telegram"]] as const)(
|
||||
"routes %s to the correct provider",
|
||||
async (type) => {
|
||||
const deps = createService();
|
||||
const notification = makeNotification({ type: type as any });
|
||||
|
||||
const result = await deps.service.sendTestNotification(notification);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const providerMap: Record<string, ReturnType<typeof createProvider>> = {
|
||||
webhook: deps.webhookProvider,
|
||||
slack: deps.slackProvider,
|
||||
matrix: deps.matrixProvider,
|
||||
pager_duty: deps.pagerDutyProvider,
|
||||
discord: deps.discordProvider,
|
||||
email: deps.emailProvider,
|
||||
teams: deps.teamsProvider,
|
||||
telegram: deps.telegramProvider,
|
||||
};
|
||||
expect(providerMap[type].sendTestAlert).toHaveBeenCalledWith(notification);
|
||||
}
|
||||
);
|
||||
|
||||
it("returns false for unknown notification type", async () => {
|
||||
const { service } = createService();
|
||||
const result = await service.sendTestNotification(makeNotification({ type: "unknown" as any }));
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── testAllNotifications ─────────────────────────────────────────────────
|
||||
|
||||
describe("testAllNotifications", () => {
|
||||
it("returns true when all test alerts succeed", async () => {
|
||||
const { service, notificationsRepository } = createService();
|
||||
(notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([
|
||||
makeNotification({ type: "email" }),
|
||||
makeNotification({ type: "slack" }),
|
||||
]);
|
||||
|
||||
const result = await service.testAllNotifications(["notif-1", "notif-2"]);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when any test alert fails", async () => {
|
||||
const { service, notificationsRepository, emailProvider } = createService();
|
||||
emailProvider.sendTestAlert.mockResolvedValue(false);
|
||||
(notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification({ type: "email" })]);
|
||||
|
||||
const result = await service.testAllNotifications(["notif-1"]);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── CRUD operations ──────────────────────────────────────────────────────
|
||||
|
||||
describe("createNotification", () => {
|
||||
it("sets userId and teamId and delegates to repository", async () => {
|
||||
const created = makeNotification();
|
||||
const { service, notificationsRepository } = createService();
|
||||
(notificationsRepository.create as jest.Mock).mockResolvedValue(created);
|
||||
|
||||
const result = await service.createNotification({ type: "email", address: "a@b.com" }, "user-1", "team-1");
|
||||
|
||||
expect(result).toBe(created);
|
||||
expect(notificationsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ userId: "user-1", teamId: "team-1", type: "email" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("findById", () => {
|
||||
it("delegates to repository", async () => {
|
||||
const notification = makeNotification();
|
||||
const { service, notificationsRepository } = createService();
|
||||
(notificationsRepository.findById as jest.Mock).mockResolvedValue(notification);
|
||||
|
||||
const result = await service.findById("notif-1", "team-1");
|
||||
expect(result).toBe(notification);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findNotificationsByTeamId", () => {
|
||||
it("delegates to repository", async () => {
|
||||
const notifications = [makeNotification()];
|
||||
const { service, notificationsRepository } = createService();
|
||||
(notificationsRepository.findByTeamId as jest.Mock).mockResolvedValue(notifications);
|
||||
|
||||
const result = await service.findNotificationsByTeamId("team-1");
|
||||
expect(result).toBe(notifications);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateById", () => {
|
||||
it("delegates to repository", async () => {
|
||||
const updated = makeNotification({ address: "new@example.com" });
|
||||
const { service, notificationsRepository } = createService();
|
||||
(notificationsRepository.updateById as jest.Mock).mockResolvedValue(updated);
|
||||
|
||||
const result = await service.updateById("notif-1", "team-1", { address: "new@example.com" });
|
||||
expect(result).toBe(updated);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteById", () => {
|
||||
it("deletes notification and removes from monitors", async () => {
|
||||
const deleted = makeNotification();
|
||||
const { service, notificationsRepository, monitorsRepository } = createService();
|
||||
(notificationsRepository.deleteById as jest.Mock).mockResolvedValue(deleted);
|
||||
|
||||
const result = await service.deleteById("notif-1", "team-1");
|
||||
|
||||
expect(result).toBe(deleted);
|
||||
expect(monitorsRepository.removeNotificationFromMonitors).toHaveBeenCalledWith("notif-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
|
||||
import { StatusService } from "../../src/service/infrastructure/statusService.ts";
|
||||
import { createMockLogger } from "../helpers/createMockLogger.ts";
|
||||
import type { Monitor, MonitorStatusResponse, Check, HardwareStatusPayload } from "../../src/types/index.ts";
|
||||
import type { IMonitorsRepository, IMonitorStatsRepository, IChecksRepository } from "../../src/repositories/index.ts";
|
||||
import type { IBufferService } from "../../src/service/infrastructure/bufferService.ts";
|
||||
import { StatusService } from "../../../src/service/infrastructure/statusService.ts";
|
||||
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
||||
import type { Monitor, MonitorStatusResponse, Check, HardwareStatusPayload } from "../../../src/types/index.ts";
|
||||
import type { IMonitorsRepository, IMonitorStatsRepository, IChecksRepository } from "../../../src/repositories/index.ts";
|
||||
import type { IBufferService } from "../../../src/service/infrastructure/bufferService.ts";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it, jest } from "@jest/globals";
|
||||
import { SuperSimpleQueueHelper } from "../../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts";
|
||||
import type { Monitor } from "../../src/types/monitor.ts";
|
||||
import { createMockLogger } from "../helpers/createMockLogger.ts";
|
||||
import { SuperSimpleQueueHelper } from "../../../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts";
|
||||
import type { Monitor } from "../../../src/types/monitor.ts";
|
||||
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
||||
|
||||
const createHelper = (overrides?: Record<string, unknown>) => {
|
||||
const maintenanceWindowsRepository = {
|
||||
Reference in new issue
Block a user