add notificaiton provider tests

This commit is contained in:
Alex Holliday committed 2026-04-09 11:05:21 -07:00
1 parent 63efef65c7
commit a0f73e4e8d
24 files changed
+1116 -47

No files matched your search

+1 -1
View File
@@ -6,7 +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/**/*.ts' test/unit/services test/unit/providers",
"test:services": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/service/**/*.ts' test/unit/services test/unit/providers/network test/unit/providers/notifications",
"dev": "nodemon --exec tsx src/index.js",
"start": "node --watch ./dist/index.js",
"build": "tsc && tsc-alias && cp -r src/templates dist/templates",
@@ -63,7 +63,7 @@ export class MatrixProvider implements INotificationProvider {
message: `Matrix notification failed : ${err.message}`,
service: SERVICE_NAME,
method: "sendMessage",
stack: err?.stack,
stack: err.stack,
});
return false;
}
@@ -0,0 +1,51 @@
import type { Notification } from "../../src/types/index.ts";
import type { NotificationMessage } from "../../src/types/notificationMessage.ts";
export const makeNotification = (overrides?: Partial<Notification>): Notification =>
({
address: "https://hooks.example.com/webhook",
accessToken: "token-abc",
homeserverUrl: "https://matrix.example.com",
roomId: "!room:example.com",
...overrides,
}) as Notification;
export const makeMessage = (overrides?: Partial<NotificationMessage>): NotificationMessage => ({
type: "monitor_down",
severity: "critical",
monitor: { id: "mon-1", name: "Test Monitor", url: "https://example.com", type: "http", status: "down" },
content: {
title: "Monitor Down: Test Monitor",
summary: 'Monitor "Test Monitor" is currently down.',
details: ["URL: https://example.com", "Status: Down"],
timestamp: new Date("2025-01-01T00:00:00Z"),
},
clientHost: "https://app.example.com",
metadata: { teamId: "team-1", notificationReason: "status_change" },
...overrides,
});
export const makeMessageWithThresholds = (): NotificationMessage =>
makeMessage({
type: "threshold_breach",
severity: "warning",
monitor: { id: "mon-1", name: "Infra Server", url: "https://infra.example.com", type: "hardware", status: "up" },
content: {
title: "Threshold Exceeded: Infra Server",
summary: "Thresholds exceeded.",
details: ["URL: https://infra.example.com"],
thresholds: [
{ metric: "cpu", currentValue: 90, threshold: 80, unit: "%", formattedValue: "90.0%" },
{ metric: "memory", currentValue: 85, threshold: 70, unit: "%", formattedValue: "85.0%" },
],
timestamp: new Date("2025-01-01T00:00:00Z"),
},
});
export const makeMessageWithIncident = (): NotificationMessage =>
makeMessage({
content: {
...makeMessage().content,
incident: { id: "inc-1", url: "https://app.example.com/incidents/inc-1", createdAt: new Date("2025-01-01T00:00:00Z") },
},
});
@@ -0,0 +1,40 @@
import { describe, expect, it } from "@jest/globals";
import type { INotificationProvider } from "../../src/service/infrastructure/notificationProviders/INotificationProvider.ts";
import type { Notification } from "../../src/types/index.ts";
import type { NotificationMessage } from "../../src/types/notificationMessage.ts";
const makeMessage = (): NotificationMessage => ({
type: "monitor_down",
severity: "critical",
monitor: { id: "mon-1", name: "Test", url: "https://example.com", type: "http", status: "down" },
content: {
title: "Monitor Down: Test",
summary: "Test is down",
details: ["URL: https://example.com"],
timestamp: new Date("2025-01-01T00:00:00Z"),
},
clientHost: "https://app.example.com",
metadata: { teamId: "team-1", notificationReason: "status_change" },
});
export const testNotificationProviderContract = (
name: string,
opts: {
create: () => INotificationProvider;
makeNotification: () => Partial<Notification>;
}
) => {
describe(`INotificationProvider contract: ${name}`, () => {
it("sendTestAlert returns a boolean", async () => {
const provider = opts.create();
const result = await provider.sendTestAlert(opts.makeNotification());
expect(typeof result).toBe("boolean");
});
it("sendMessage returns a boolean", async () => {
const provider = opts.create();
const result = await provider.sendMessage(opts.makeNotification() as Notification, makeMessage());
expect(typeof result).toBe("boolean");
});
});
};
@@ -1,6 +1,6 @@
import { describe, expect, it, jest } from "@jest/globals";
import { AdvancedMatcher } from "../../../src/service/infrastructure/network/AdvancedMatcher.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { AdvancedMatcher } from "../../../../src/service/infrastructure/network/AdvancedMatcher.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,9 +1,9 @@
import { describe, expect, it, jest } from "@jest/globals";
import { DockerProvider } from "../../../src/service/infrastructure/network/DockerProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { DockerProvider } from "../../../../src/service/infrastructure/network/DockerProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,9 +1,9 @@
import { describe, expect, it, jest } from "@jest/globals";
import { GameProvider } from "../../../src/service/infrastructure/network/GameProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { GameProvider } from "../../../../src/service/infrastructure/network/GameProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,7 +1,7 @@
import { describe, expect, it, jest } from "@jest/globals";
import { GrpcProvider } from "../../../src/service/infrastructure/network/GrpcProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { GrpcProvider } from "../../../../src/service/infrastructure/network/GrpcProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,8 +1,8 @@
import { describe, expect, it, jest } from "@jest/globals";
import { HardwareProvider } from "../../../src/service/infrastructure/network/HardwareProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import type { HttpProvider } from "../../../src/service/infrastructure/network/HttpProvider.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { HardwareProvider } from "../../../../src/service/infrastructure/network/HardwareProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import type { HttpProvider } from "../../../../src/service/infrastructure/network/HttpProvider.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,7 +1,7 @@
import { describe, expect, it, jest } from "@jest/globals";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Mocks ────────────────────────────────────────────────────────────────────
@@ -38,7 +38,7 @@ jest.unstable_mockModule("got", () => ({
},
}));
const { HttpProvider } = await import("../../../src/service/infrastructure/network/HttpProvider.ts");
const { HttpProvider } = await import("../../../../src/service/infrastructure/network/HttpProvider.ts");
const gotModule = await import("got");
const { HTTPError, RequestError } = gotModule;
@@ -1,10 +1,10 @@
import { describe, expect, it, jest } from "@jest/globals";
import { PageSpeedProvider } from "../../../src/service/infrastructure/network/PageSpeedProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../helpers/createMockLogger.ts";
import type { HttpProvider } from "../../../src/service/infrastructure/network/HttpProvider.ts";
import type { ISettingsService } from "../../../src/service/system/settingsService.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { PageSpeedProvider } from "../../../../src/service/infrastructure/network/PageSpeedProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import type { HttpProvider } from "../../../../src/service/infrastructure/network/HttpProvider.ts";
import type { ISettingsService } from "../../../../src/service/system/settingsService.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,7 +1,7 @@
import { describe, expect, it, jest } from "@jest/globals";
import { PingProvider } from "../../../src/service/infrastructure/network/PingProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { PingProvider } from "../../../../src/service/infrastructure/network/PingProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,8 +1,8 @@
import { describe, expect, it, jest } from "@jest/globals";
import { PortProvider } from "../../../src/service/infrastructure/network/PortProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { PortProvider } from "../../../../src/service/infrastructure/network/PortProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@jest/globals";
import { timeRequest, NETWORK_ERROR, PING_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import { timeRequest, NETWORK_ERROR, PING_ERROR } from "../../../../src/service/infrastructure/network/utils.ts";
describe("network utils", () => {
describe("timeRequest", () => {
@@ -1,8 +1,8 @@
import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals";
import { WebSocketProvider } from "../../../src/service/infrastructure/network/WebSocketProvider.ts";
import { testStatusProviderContract } from "../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../src/types/index.ts";
import { WebSocketProvider } from "../../../../src/service/infrastructure/network/WebSocketProvider.ts";
import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts";
import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts";
import type { Monitor } from "../../../../src/types/index.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
@@ -0,0 +1,132 @@
import { describe, expect, it, jest } from "@jest/globals";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts";
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
const mockGotPost = jest.fn().mockResolvedValue({});
jest.unstable_mockModule("got", () => ({ default: { post: mockGotPost } }));
const { DiscordProvider } = await import("../../../../src/service/infrastructure/notificationProviders/discord.ts");
const createProvider = () => {
const logger = createMockLogger();
const provider = new DiscordProvider(logger as any);
return { provider, logger };
};
testNotificationProviderContract("DiscordProvider", {
create: () => {
mockGotPost.mockResolvedValue({});
return createProvider().provider;
},
makeNotification: () => makeNotification(),
});
describe("DiscordProvider", () => {
beforeEach(() => mockGotPost.mockReset().mockResolvedValue({}));
describe("sendTestAlert", () => {
it("sends test message and returns true", async () => {
const { provider } = createProvider();
const result = await provider.sendTestAlert(makeNotification());
expect(result).toBe(true);
expect(mockGotPost).toHaveBeenCalledWith(
"https://hooks.example.com/webhook",
expect.objectContaining({ json: expect.objectContaining({ content: expect.any(String) }) })
);
});
it("returns false when address is missing", async () => {
const { provider } = createProvider();
expect(await provider.sendTestAlert(makeNotification({ address: "" }))).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("network"));
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendTestAlert" }));
});
it("handles undefined thrown values", async () => {
mockGotPost.mockRejectedValue(undefined);
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined }));
});
});
describe("sendMessage", () => {
it("sends embed and returns true", async () => {
const { provider } = createProvider();
const result = await provider.sendMessage(makeNotification() as any, makeMessage());
expect(result).toBe(true);
expect(mockGotPost).toHaveBeenCalledWith(
"https://hooks.example.com/webhook",
expect.objectContaining({ json: expect.objectContaining({ embeds: expect.any(Array) }) })
);
});
it("returns false when address is missing", async () => {
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification({ address: "" }) as any, makeMessage())).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendMessage" }));
});
it("handles undefined thrown values in sendMessage", async () => {
mockGotPost.mockRejectedValue(undefined);
const { provider, logger } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined }));
});
it("includes threshold breaches in embed fields", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds());
const payload = mockGotPost.mock.calls[0][1].json;
const fields = payload.embeds[0].fields;
expect(fields.some((f: any) => f.name === "Threshold Breaches")).toBe(true);
});
it("includes details in embed fields", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage());
const fields = mockGotPost.mock.calls[0][1].json.embeds[0].fields;
expect(fields.some((f: any) => f.name === "Details")).toBe(true);
});
it("maps all severity levels to colors", async () => {
const { provider } = createProvider();
for (const severity of ["critical", "warning", "success", "info"] as const) {
mockGotPost.mockClear();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity }));
const color = mockGotPost.mock.calls[0][1].json.embeds[0].color;
expect(color).toBeGreaterThan(0);
}
});
it("uses info color for unknown severity", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity: "unknown" as any }));
const color = mockGotPost.mock.calls[0][1].json.embeds[0].color;
expect(color).toBe(0x3b82f6);
});
it("omits threshold fields when not present", async () => {
const { provider } = createProvider();
const msg = makeMessage();
msg.content.thresholds = undefined;
msg.content.details = undefined;
await provider.sendMessage(makeNotification() as any, msg);
const fields = mockGotPost.mock.calls[0][1].json.embeds[0].fields;
expect(fields.some((f: any) => f.name === "Threshold Breaches")).toBe(false);
expect(fields.some((f: any) => f.name === "Details")).toBe(false);
});
});
});
@@ -0,0 +1,149 @@
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import { EmailProvider } from "../../../../src/service/infrastructure/notificationProviders/email.ts";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { makeNotification, makeMessage } from "../../../helpers/notificationMessage.ts";
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
import type { IEmailService } from "../../../../src/service/infrastructure/emailService.ts";
// ── Helpers ──────────────────────────────────────────────────────────────────
const createMockEmailService = () =>
({
buildEmail: jest.fn().mockResolvedValue("<html>email</html>"),
sendEmail: jest.fn().mockResolvedValue("msg-123"),
}) as unknown as jest.Mocked<IEmailService>;
const createProvider = () => {
const logger = createMockLogger();
const emailService = createMockEmailService();
const provider = new EmailProvider(emailService, logger as any);
return { provider, logger, emailService };
};
testNotificationProviderContract("EmailProvider", {
create: () => createProvider().provider,
makeNotification: () => makeNotification(),
});
// ── Tests ────────────────────────────────────────────────────────────────────
describe("EmailProvider", () => {
describe("sendTestAlert", () => {
it("builds test email and sends it", async () => {
const { provider, emailService } = createProvider();
const result = await provider.sendTestAlert(makeNotification());
expect(result).toBe(true);
expect(emailService.buildEmail).toHaveBeenCalledWith("testEmailTemplate", { testName: "Monitoring System" });
expect(emailService.sendEmail).toHaveBeenCalledWith("https://hooks.example.com/webhook", "Test notification", "<html>email</html>");
});
it("returns false when address is missing", async () => {
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification({ address: "" }))).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Missing address" }));
});
it("returns false when buildEmail returns undefined", async () => {
const { provider, emailService, logger } = createProvider();
(emailService.buildEmail as jest.Mock).mockResolvedValue(undefined);
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Failed to build test email content" }));
});
it("returns false when sendEmail returns falsy", async () => {
const { provider, emailService, logger } = createProvider();
(emailService.sendEmail as jest.Mock).mockResolvedValue(false);
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Email test alert failed" }));
});
});
describe("sendMessage", () => {
it("builds email from message and sends it", async () => {
const { provider, emailService } = createProvider();
const result = await provider.sendMessage(makeNotification() as any, makeMessage());
expect(result).toBe(true);
expect(emailService.buildEmail).toHaveBeenCalledWith(
"unifiedNotificationTemplate",
expect.objectContaining({ title: "Monitor Down: Test Monitor" })
);
expect(emailService.sendEmail).toHaveBeenCalled();
});
it("returns false when address is missing", async () => {
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification({ address: "" }) as any, makeMessage())).toBe(false);
});
it("returns false when buildEmail returns undefined", async () => {
const { provider, emailService, logger } = createProvider();
(emailService.buildEmail as jest.Mock).mockResolvedValue(undefined);
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Failed to build email content" }));
});
it("returns false when sendEmail returns falsy", async () => {
const { provider, emailService, logger } = createProvider();
(emailService.sendEmail as jest.Mock).mockResolvedValue(undefined);
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Email notification failed" }));
});
it("builds correct subject for monitor_down", async () => {
const { provider, emailService } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ type: "monitor_down" }));
expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Monitor Test Monitor is down", expect.anything());
});
it("builds correct subject for monitor_up", async () => {
const { provider, emailService } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ type: "monitor_up" }));
expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Monitor Test Monitor is back up", expect.anything());
});
it("builds correct subject for threshold_breach", async () => {
const { provider, emailService } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ type: "threshold_breach" }));
expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Monitor Test Monitor threshold exceeded", expect.anything());
});
it("builds correct subject for threshold_resolved", async () => {
const { provider, emailService } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ type: "threshold_resolved" }));
expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Monitor Test Monitor thresholds resolved", expect.anything());
});
it("builds default subject for unknown type", async () => {
const { provider, emailService } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ type: "test" }));
expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Alert: Test Monitor", expect.anything());
});
it("passes incident url in context when present", async () => {
const { provider, emailService } = createProvider();
const msg = makeMessage();
msg.content.incident = { id: "inc-1", url: "https://app.example.com/incidents/inc-1", createdAt: new Date() };
await provider.sendMessage(makeNotification() as any, msg);
expect(emailService.buildEmail).toHaveBeenCalledWith(
"unifiedNotificationTemplate",
expect.objectContaining({ incidentUrl: "https://app.example.com/incidents/inc-1" })
);
});
it("maps all severity levels to colors", async () => {
const { provider, emailService } = createProvider();
const severityColors: Record<string, string> = { critical: "red", warning: "#f59e0b", info: "#3b82f6", success: "green" };
for (const [severity, color] of Object.entries(severityColors)) {
(emailService.buildEmail as jest.Mock).mockClear();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity: severity as any }));
expect(emailService.buildEmail).toHaveBeenCalledWith("unifiedNotificationTemplate", expect.objectContaining({ headerColor: color }));
}
});
it("uses default color for unknown severity", async () => {
const { provider, emailService } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity: "unknown" as any }));
expect(emailService.buildEmail).toHaveBeenCalledWith("unifiedNotificationTemplate", expect.objectContaining({ headerColor: "#3b82f6" }));
});
});
});
@@ -0,0 +1,144 @@
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts";
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
const mockGotPut = jest.fn().mockResolvedValue({});
jest.unstable_mockModule("got", () => ({ default: { put: mockGotPut } }));
jest.unstable_mockModule("crypto", () => ({ randomUUID: () => "test-uuid-1234" }));
const { MatrixProvider } = await import("../../../../src/service/infrastructure/notificationProviders/matrix.ts");
const createProvider = () => {
const logger = createMockLogger();
return { provider: new MatrixProvider(logger as any), logger };
};
testNotificationProviderContract("MatrixProvider", {
create: () => {
mockGotPut.mockResolvedValue({});
return createProvider().provider;
},
makeNotification: () => makeNotification(),
});
describe("MatrixProvider", () => {
beforeEach(() => mockGotPut.mockReset().mockResolvedValue({}));
describe("sendTestAlert", () => {
it("sends to Matrix API and returns true", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification())).toBe(true);
expect(mockGotPut).toHaveBeenCalledWith(
expect.stringContaining("matrix.example.com/_matrix/client/v3/rooms/!room:example.com/send/m.room.message/test-uuid-1234"),
expect.objectContaining({
headers: expect.objectContaining({ Authorization: "Bearer token-abc" }),
})
);
});
it("returns false when homeserverUrl is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ homeserverUrl: undefined }))).toBe(false);
});
it("returns false when accessToken is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ accessToken: undefined }))).toBe(false);
});
it("returns false when roomId is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ roomId: undefined }))).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPut.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("handles non-Error thrown values", async () => {
mockGotPut.mockRejectedValue({ message: "custom obj" });
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined }));
});
});
describe("sendMessage", () => {
it("sends formatted HTML and plain text", async () => {
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(true);
const body = mockGotPut.mock.calls[0][1].json;
expect(body.msgtype).toBe("m.text");
expect(body.format).toBe("org.matrix.custom.html");
expect(body.body).toContain("Monitor Down");
expect(body.formatted_body).toContain("<h2");
});
it("returns false when homeserverUrl is missing", async () => {
expect(await createProvider().provider.sendMessage(makeNotification({ homeserverUrl: undefined }) as any, makeMessage())).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPut.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("includes threshold breaches", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds());
const body = mockGotPut.mock.calls[0][1].json;
expect(body.body).toContain("CPU");
expect(body.formatted_body).toContain("CPU");
});
it("includes incident link", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithIncident());
const body = mockGotPut.mock.calls[0][1].json;
expect(body.body).toContain("View Incident");
expect(body.formatted_body).toContain("View Incident");
});
it("omits optional sections when not present", async () => {
const { provider } = createProvider();
const msg = makeMessage();
msg.content.thresholds = undefined;
msg.content.details = undefined;
msg.content.incident = undefined;
await provider.sendMessage(makeNotification() as any, msg);
const body = mockGotPut.mock.calls[0][1].json;
expect(body.body).not.toContain("Threshold");
expect(body.body).not.toContain("Additional");
expect(body.body).not.toContain("View Incident");
});
it("escapes HTML special characters", async () => {
const { provider } = createProvider();
const msg = makeMessage();
msg.monitor.name = '<script>alert("xss")</script>';
await provider.sendMessage(makeNotification() as any, msg);
const html = mockGotPut.mock.calls[0][1].json.formatted_body;
expect(html).not.toContain("<script>");
expect(html).toContain("&lt;script&gt;");
});
it("maps all severity levels to colors", async () => {
const { provider } = createProvider();
for (const severity of ["critical", "warning", "success", "info"] as const) {
mockGotPut.mockClear();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity }));
const html = mockGotPut.mock.calls[0][1].json.formatted_body;
expect(html).toContain("color:");
}
});
it("uses default color for unknown severity", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity: "unknown" as any }));
const html = mockGotPut.mock.calls[0][1].json.formatted_body;
expect(html).toContain("#808080");
});
});
});
@@ -0,0 +1,34 @@
import { describe, expect, it, jest } from "@jest/globals";
import { buildTestEmail, getTestMessage } from "../../../../src/service/infrastructure/notificationProviders/utils.ts";
import type { IEmailService } from "../../../../src/service/infrastructure/emailService.ts";
describe("notification utils", () => {
describe("getTestMessage", () => {
it("returns a non-empty string", () => {
expect(getTestMessage()).toBe("This is a test notification from Checkmate");
});
});
describe("buildTestEmail", () => {
it("calls emailService.buildEmail with test template and context", async () => {
const emailService = {
buildEmail: jest.fn().mockResolvedValue("<html>test</html>"),
} as unknown as IEmailService;
const result = await buildTestEmail(emailService);
expect(emailService.buildEmail).toHaveBeenCalledWith("testEmailTemplate", { testName: "Monitoring System" });
expect(result).toBe("<html>test</html>");
});
it("returns undefined when buildEmail returns undefined", async () => {
const emailService = {
buildEmail: jest.fn().mockResolvedValue(undefined),
} as unknown as IEmailService;
const result = await buildTestEmail(emailService);
expect(result).toBeUndefined();
});
});
});
@@ -0,0 +1,132 @@
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { makeNotification, makeMessage, makeMessageWithThresholds } from "../../../helpers/notificationMessage.ts";
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
const mockGotPost = jest.fn().mockResolvedValue({});
jest.unstable_mockModule("got", () => ({ default: { post: mockGotPost } }));
const { PagerDutyProvider } = await import("../../../../src/service/infrastructure/notificationProviders/pagerduty.ts");
const createProvider = () => {
const logger = createMockLogger();
return { provider: new PagerDutyProvider(logger as any), logger };
};
testNotificationProviderContract("PagerDutyProvider", {
create: () => {
mockGotPost.mockResolvedValue({});
return createProvider().provider;
},
makeNotification: () => makeNotification(),
});
describe("PagerDutyProvider", () => {
beforeEach(() => mockGotPost.mockReset().mockResolvedValue({}));
describe("sendTestAlert", () => {
it("sends to PagerDuty API and returns true", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification())).toBe(true);
expect(mockGotPost).toHaveBeenCalledWith(
"https://events.pagerduty.com/v2/enqueue",
expect.objectContaining({
json: expect.objectContaining({ routing_key: "https://hooks.example.com/webhook", event_action: "trigger" }),
})
);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("handles non-Error thrown values", async () => {
mockGotPost.mockRejectedValue(null);
const { provider } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
});
});
describe("sendMessage", () => {
it("sends payload and returns true", async () => {
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(true);
const payload = mockGotPost.mock.calls[0][1].json;
expect(payload.routing_key).toBe("https://hooks.example.com/webhook");
expect(payload.event_action).toBe("trigger");
expect(payload.dedup_key).toBe("checkmate-mon-1");
});
it("returns false when address is missing", async () => {
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification({ address: "" }) as any, makeMessage())).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("handles non-Error thrown values in sendMessage", async () => {
mockGotPost.mockRejectedValue(null);
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
});
it("uses 'resolve' event_action for monitor_up", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ type: "monitor_up" }));
expect(mockGotPost.mock.calls[0][1].json.event_action).toBe("resolve");
});
it("uses 'resolve' event_action for threshold_resolved", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ type: "threshold_resolved" }));
expect(mockGotPost.mock.calls[0][1].json.event_action).toBe("resolve");
});
it("includes threshold info in summary and custom_details", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds());
const payload = mockGotPost.mock.calls[0][1].json;
expect(payload.payload.summary).toContain("CPU");
expect(payload.payload.custom_details.threshold_breaches).toBeDefined();
});
it("includes details in custom_details", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage());
expect(mockGotPost.mock.calls[0][1].json.payload.custom_details.details).toBeDefined();
});
it("maps all severity levels", async () => {
const { provider } = createProvider();
for (const severity of ["critical", "warning", "info", "success"] as const) {
mockGotPost.mockClear();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity }));
expect(mockGotPost.mock.calls[0][1].json.payload.severity).toBeTruthy();
}
});
it("uses 'error' for unknown severity", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity: "unknown" as any }));
expect(mockGotPost.mock.calls[0][1].json.payload.severity).toBe("error");
});
it("omits threshold and details from custom_details when not present", async () => {
const { provider } = createProvider();
const msg = makeMessage();
msg.content.thresholds = undefined;
msg.content.details = undefined;
await provider.sendMessage(makeNotification() as any, msg);
const cd = mockGotPost.mock.calls[0][1].json.payload.custom_details;
expect(cd.threshold_breaches).toBeUndefined();
expect(cd.details).toBeUndefined();
});
});
});
@@ -0,0 +1,127 @@
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts";
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
const mockGotPost = jest.fn().mockResolvedValue({});
jest.unstable_mockModule("got", () => ({ default: { post: mockGotPost }, HTTPError: class extends Error {} }));
const { SlackProvider } = await import("../../../../src/service/infrastructure/notificationProviders/slack.ts");
const createProvider = () => {
const logger = createMockLogger();
return { provider: new SlackProvider(logger as any), logger };
};
testNotificationProviderContract("SlackProvider", {
create: () => {
mockGotPost.mockResolvedValue({});
return createProvider().provider;
},
makeNotification: () => makeNotification(),
});
describe("SlackProvider", () => {
beforeEach(() => mockGotPost.mockReset().mockResolvedValue({}));
describe("sendTestAlert", () => {
it("returns true on success", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification())).toBe(true);
});
it("returns false when address is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ address: "" }))).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("handles undefined thrown values", async () => {
mockGotPost.mockRejectedValue(undefined);
const { provider } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
});
});
describe("sendMessage", () => {
it("sends Block Kit payload and returns true", async () => {
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(true);
const payload = mockGotPost.mock.calls[0][1].json;
expect(payload.blocks).toBeDefined();
expect(payload.attachments).toBeDefined();
});
it("returns false when address is missing", async () => {
expect(await createProvider().provider.sendMessage(makeNotification({ address: "" }) as any, makeMessage())).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("handles undefined thrown values in sendMessage", async () => {
mockGotPost.mockRejectedValue(undefined);
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
});
it("includes threshold section when thresholds present", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds());
const blocks = mockGotPost.mock.calls[0][1].json.blocks;
const text = JSON.stringify(blocks);
expect(text).toContain("Threshold Breaches");
});
it("includes incident button when incident present", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithIncident());
const blocks = mockGotPost.mock.calls[0][1].json.blocks;
expect(blocks.some((b: any) => b.type === "actions")).toBe(true);
});
it("includes details section when details are present", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage());
const blocks = mockGotPost.mock.calls[0][1].json.blocks;
const text = JSON.stringify(blocks);
expect(text).toContain("Additional Information");
});
it("omits threshold, details, and incident sections when not present", async () => {
const { provider } = createProvider();
const msg = makeMessage();
msg.content.thresholds = undefined;
msg.content.details = undefined;
msg.content.incident = undefined;
await provider.sendMessage(makeNotification() as any, msg);
const blocks = mockGotPost.mock.calls[0][1].json.blocks;
const text = JSON.stringify(blocks);
expect(text).not.toContain("Additional Information");
expect(blocks.some((b: any) => b.type === "actions")).toBe(false);
});
it("maps all severity levels to colors", async () => {
const { provider } = createProvider();
for (const severity of ["critical", "warning", "success", "info"] as const) {
mockGotPost.mockClear();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity }));
expect(mockGotPost.mock.calls[0][1].json.attachments[0].color).toBeTruthy();
}
});
it("uses default color for unknown severity", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessage({ severity: "unknown" as any }));
expect(mockGotPost.mock.calls[0][1].json.attachments[0].color).toBe("#808080");
});
});
});
@@ -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",
@@ -248,5 +248,48 @@ describe("TeamsProvider", () => {
})
);
});
it("handles non-Error thrown values in sendMessage", async () => {
mockPost.mockRejectedValue(null);
const result = await provider.sendMessage(createNotification(), createMessage());
expect(result).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined }));
});
it("includes details section in Adaptive Card when details are present", async () => {
const notification = createNotification();
const message = createMessage({
content: {
title: "Test",
summary: "Summary",
details: ["URL: https://example.com", "Status: Down"],
timestamp: new Date(),
},
});
await provider.sendMessage(notification, message);
const [, options] = mockPost.mock.calls[0] as [string, { json: any }];
const card = options.json.attachments[0].content;
const text = JSON.stringify(card.body);
expect(text).toContain("Additional Information");
expect(text).toContain("URL: https://example.com");
});
it("uses default color for unknown severity", async () => {
const notification = createNotification();
const message = createMessage({ severity: "unknown" as any });
await provider.sendMessage(notification, message);
const [, options] = mockPost.mock.calls[0] as [string, { json: any }];
const card = options.json.attachments[0].content;
expect(card.body[0].color).toBe("default");
});
});
describe("sendTestAlert error handling", () => {
it("handles non-Error thrown values", async () => {
mockPost.mockRejectedValue(null);
const result = await provider.sendTestAlert(createNotification());
expect(result).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined }));
});
});
});
@@ -0,0 +1,115 @@
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts";
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
const mockGotPost = jest.fn().mockResolvedValue({});
jest.unstable_mockModule("got", () => ({ default: { post: mockGotPost } }));
const { TelegramProvider } = await import("../../../../src/service/infrastructure/notificationProviders/telegram.ts");
const createProvider = () => {
const logger = createMockLogger();
return { provider: new TelegramProvider(logger as any), logger };
};
testNotificationProviderContract("TelegramProvider", {
create: () => {
mockGotPost.mockResolvedValue({});
return createProvider().provider;
},
makeNotification: () => makeNotification(),
});
describe("TelegramProvider", () => {
beforeEach(() => mockGotPost.mockReset().mockResolvedValue({}));
describe("sendTestAlert", () => {
it("sends to Telegram API and returns true", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification())).toBe(true);
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringContaining("api.telegram.org/bottoken-abc/sendMessage"),
expect.objectContaining({ json: expect.objectContaining({ chat_id: "https://hooks.example.com/webhook" }) })
);
});
it("returns false when address is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ address: "" }))).toBe(false);
});
it("returns false when accessToken is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ accessToken: undefined }))).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendTestAlert", details: { error: "fail" } }));
});
it("handles non-Error thrown values in sendTestAlert", async () => {
mockGotPost.mockRejectedValue("string error");
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined, details: { error: "unknown error" } }));
});
});
describe("sendMessage", () => {
it("sends HTML-formatted text and returns true", async () => {
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(true);
const json = mockGotPost.mock.calls[0][1].json;
expect(json.parse_mode).toBe("HTML");
expect(json.text).toContain("<b>Monitor Down");
});
it("returns false when address is missing", async () => {
expect(await createProvider().provider.sendMessage(makeNotification({ address: "" }) as any, makeMessage())).toBe(false);
});
it("returns false when accessToken is missing", async () => {
expect(await createProvider().provider.sendMessage(makeNotification({ accessToken: undefined }) as any, makeMessage())).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendMessage" }));
});
it("handles non-Error thrown values in sendMessage", async () => {
mockGotPost.mockRejectedValue(42);
const { provider, logger } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ details: { error: "unknown error" } }));
});
it("includes thresholds in text", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds());
expect(mockGotPost.mock.calls[0][1].json.text).toContain("CPU");
});
it("includes incident link in text", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithIncident());
expect(mockGotPost.mock.calls[0][1].json.text).toContain("View Incident");
});
it("omits optional sections when not present", async () => {
const { provider } = createProvider();
const msg = makeMessage();
msg.content.thresholds = undefined;
msg.content.details = undefined;
msg.content.incident = undefined;
await provider.sendMessage(makeNotification() as any, msg);
const text = mockGotPost.mock.calls[0][1].json.text;
expect(text).not.toContain("Threshold");
expect(text).not.toContain("Additional");
expect(text).not.toContain("View Incident");
});
});
});
@@ -0,0 +1,102 @@
import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts";
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
const mockGotPost = jest.fn().mockResolvedValue({});
jest.unstable_mockModule("got", () => ({ default: { post: mockGotPost } }));
const { WebhookProvider } = await import("../../../../src/service/infrastructure/notificationProviders/webhook.ts");
const createProvider = () => {
const logger = createMockLogger();
return { provider: new WebhookProvider(logger as any), logger };
};
testNotificationProviderContract("WebhookProvider", {
create: () => {
mockGotPost.mockResolvedValue({});
return createProvider().provider;
},
makeNotification: () => makeNotification(),
});
describe("WebhookProvider", () => {
beforeEach(() => mockGotPost.mockReset().mockResolvedValue({}));
describe("sendTestAlert", () => {
it("returns true on success", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification())).toBe(true);
});
it("returns false when address is missing", async () => {
expect(await createProvider().provider.sendTestAlert(makeNotification({ address: "" }))).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("handles non-Error thrown values", async () => {
mockGotPost.mockRejectedValue(null);
const { provider } = createProvider();
expect(await provider.sendTestAlert(makeNotification())).toBe(false);
});
});
describe("sendMessage", () => {
it("sends payload with text and structured data", async () => {
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(true);
const payload = mockGotPost.mock.calls[0][1].json;
expect(payload.text).toContain("Monitor Down");
expect(payload.severity).toBe("critical");
expect(payload.monitor.id).toBe("mon-1");
});
it("returns false when address is missing", async () => {
expect(await createProvider().provider.sendMessage(makeNotification({ address: "" }) as any, makeMessage())).toBe(false);
});
it("returns false and logs on error", async () => {
mockGotPost.mockRejectedValue(new Error("fail"));
const { provider, logger } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
expect(logger.warn).toHaveBeenCalled();
});
it("handles non-Error thrown values in sendMessage", async () => {
mockGotPost.mockRejectedValue(null);
const { provider } = createProvider();
expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false);
});
it("includes threshold breaches in text", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds());
expect(mockGotPost.mock.calls[0][1].json.text).toContain("CPU");
});
it("includes incident link in text", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification() as any, makeMessageWithIncident());
expect(mockGotPost.mock.calls[0][1].json.text).toContain("View Incident");
});
it("omits threshold and incident sections when not present", async () => {
const { provider } = createProvider();
const msg = makeMessage();
msg.content.thresholds = undefined;
msg.content.details = undefined;
msg.content.incident = undefined;
await provider.sendMessage(makeNotification() as any, msg);
const text = mockGotPost.mock.calls[0][1].json.text;
expect(text).not.toContain("Threshold");
expect(text).not.toContain("Additional Information");
expect(text).not.toContain("View Incident");
});
});
});