add notificaiton provider tests
This commit is contained in:
1 parent
63efef65c7
commit
a0f73e4e8d
24 files changed
+1116
-47
No files matched your search
+1
-1
@@ -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");
|
||||
});
|
||||
});
|
||||
};
|
||||
+2
-2
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+5
-5
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+5
-5
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+3
-3
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+4
-4
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+4
-4
@@ -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;
|
||||
|
||||
+6
-6
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+3
-3
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
+4
-4
@@ -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
-1
@@ -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", () => {
|
||||
+4
-4
@@ -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("<script>");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
+47
-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",
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user