Files
Checkmate/server/test/unit/providers/notifications/rocketChatProvider.test.ts
T
Marcos Paulo Diniz d4eaad6170 fix(notifications): route Rocket.Chat links by monitor type
Build title_link from the monitor type (hardware -> infrastructure,
pagespeed -> pagespeed, uptime types -> uptime) and omit it when
clientHost is not configured, instead of always linking to the
infrastructure page.
2026-08-13 11:56:28 -03:00

274 lines
9.4 KiB
TypeScript

import { describe, expect, it, jest } from "@jest/globals";
import type { MonitorType } from "../../../../src/domain/monitors/monitor.type.ts";
import type { NotificationSeverity } from "../../../../src/domain/notifications/notification.type.ts";
import { createMockLogger } from "../../../helpers/createMockLogger.ts";
import { makeMessage, makeMessageWithIncident, makeMessageWithThresholds, makeNotification } from "../../../helpers/notificationMessage.ts";
import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts";
const mockGotPost = jest.fn().mockResolvedValue({});
jest.unstable_mockModule("got", () => ({ default: { post: mockGotPost } }));
const { RocketChatProvider } = await import("../../../../src/domain/notifications/providers/rocketChat.ts");
const createProvider = () => {
const logger = createMockLogger();
const provider = new RocketChatProvider(logger as any);
return { provider, logger };
};
testNotificationProviderContract("RocketChatProvider", {
create: () => {
mockGotPost.mockResolvedValue({});
return createProvider().provider;
},
makeNotification: () => makeNotification({ type: "rocket_chat" }),
});
describe("RocketChatProvider", () => {
beforeEach(() => mockGotPost.mockReset().mockResolvedValue({}));
it("sends a rich test alert without webhook overrides", async () => {
const { provider } = createProvider();
const result = await provider.sendTestAlert(makeNotification({ type: "rocket_chat" }));
expect(result).toBe(true);
expect(mockGotPost).toHaveBeenCalledWith(
"https://hooks.example.com/webhook",
expect.objectContaining({
json: {
text: "This is a test notification from Checkmate",
attachments: [
{
color: "#0000FF",
title: "Checkmate test notification",
fields: [
{ title: "Channel", value: "Rocket.Chat", short: true },
{ title: "Status", value: "Test notification", short: true },
],
},
],
},
timeout: { request: 30000 },
retry: { limit: 0 },
})
);
const payload = mockGotPost.mock.calls[0][1].json;
expect(payload).not.toHaveProperty("alias");
expect(payload).not.toHaveProperty("avatar");
expect(payload).not.toHaveProperty("emoji");
expect(payload).not.toHaveProperty("channel");
});
it("returns false when the test webhook URL is missing", async () => {
const { provider } = createProvider();
const result = await provider.sendTestAlert(makeNotification({ type: "rocket_chat", address: "" }));
expect(result).toBe(false);
expect(mockGotPost).not.toHaveBeenCalled();
});
it("returns false and logs when test delivery fails", async () => {
mockGotPost.mockRejectedValue(new Error("network failure"));
const { provider, logger } = createProvider();
const result = await provider.sendTestAlert(makeNotification({ type: "rocket_chat" }));
expect(result).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rocket.Chat test alert failed",
service: "RocketChatProvider",
method: "sendTestAlert",
})
);
});
it("sends a rich monitor attachment without webhook overrides", async () => {
const { provider } = createProvider();
const result = await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessage());
expect(result).toBe(true);
expect(mockGotPost).toHaveBeenCalledWith(
"https://hooks.example.com/webhook",
expect.objectContaining({
timeout: { request: 30000 },
retry: { limit: 0 },
json: {
text: 'Monitor "Test Monitor" is currently down.',
attachments: [
{
color: "#FF0000",
title: "Monitor Down: Test Monitor",
title_link: "https://app.example.com/uptime/mon-1",
fields: [
{ title: "Monitor", value: "Test Monitor", short: true },
{ title: "Type", value: "http", short: true },
{ title: "Status", value: "down", short: true },
{ title: "URL", value: "https://example.com", short: false },
{
title: "Details",
value: "- URL: https://example.com\n- Status: Down",
short: false,
},
],
ts: "2025-01-01T00:00:00.000Z",
},
],
},
})
);
const payload = mockGotPost.mock.calls[0][1].json;
expect(payload).not.toHaveProperty("alias");
expect(payload).not.toHaveProperty("avatar");
expect(payload).not.toHaveProperty("emoji");
expect(payload).not.toHaveProperty("channel");
});
it.each([
["http", "https://app.example.com/uptime/mon-1"],
["ping", "https://app.example.com/uptime/mon-1"],
["pagespeed", "https://app.example.com/pagespeed/mon-1"],
["hardware", "https://app.example.com/infrastructure/mon-1"],
])("links %s monitors to %s", async (type, expectedLink) => {
const { provider } = createProvider();
await provider.sendMessage(
makeNotification({ type: "rocket_chat" }),
makeMessage({ monitor: { id: "mon-1", name: "Test Monitor", url: "https://example.com", type: type as MonitorType, status: "down" } })
);
const payload = mockGotPost.mock.calls[0][1].json;
expect(payload.attachments[0].title_link).toBe(expectedLink);
});
it.each([["Host not defined"], [""], [" "]])("omits title_link when clientHost is %j", async (clientHost) => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessage({ clientHost }));
const payload = mockGotPost.mock.calls[0][1].json;
expect(payload.attachments[0]).not.toHaveProperty("title_link");
});
it.each([
["critical", "#FF0000"],
["warning", "#FFA500"],
["success", "#00FF00"],
["info", "#0000FF"],
["unknown", "#808080"],
])("maps %s severity to %s", async (severity, color) => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessage({ severity: severity as NotificationSeverity }));
const payload = mockGotPost.mock.calls[0][1].json;
expect(payload.attachments[0].color).toBe(color);
});
it("appends threshold fields before details", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessageWithThresholds());
const fields = mockGotPost.mock.calls[0][1].json.attachments[0].fields;
expect(fields).toEqual([
{ title: "Monitor", value: "Infra Server", short: true },
{ title: "Type", value: "hardware", short: true },
{ title: "Status", value: "up", short: true },
{ title: "URL", value: "https://infra.example.com", short: false },
{ title: "CPU", value: "90.0% (threshold: 80%)", short: true },
{ title: "MEMORY", value: "85.0% (threshold: 70%)", short: true },
{ title: "Details", value: "- URL: https://infra.example.com", short: false },
]);
});
it("appends the incident URL after details", async () => {
const { provider } = createProvider();
await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessageWithIncident());
const fields = mockGotPost.mock.calls[0][1].json.attachments[0].fields;
expect(fields).toEqual([
{ title: "Monitor", value: "Test Monitor", short: true },
{ title: "Type", value: "http", short: true },
{ title: "Status", value: "down", short: true },
{ title: "URL", value: "https://example.com", short: false },
{ title: "Details", value: "- URL: https://example.com\n- Status: Down", short: false },
{ title: "Incident", value: "https://app.example.com/incidents/inc-1", short: false },
]);
});
it("omits empty optional fields", async () => {
const { provider } = createProvider();
const message = makeMessage();
await provider.sendMessage(
makeNotification({ type: "rocket_chat" }),
makeMessage({
content: {
...message.content,
details: [],
thresholds: [],
incident: undefined,
},
})
);
const fields = mockGotPost.mock.calls[0][1].json.attachments[0].fields;
expect(fields).toEqual([
{ title: "Monitor", value: "Test Monitor", short: true },
{ title: "Type", value: "http", short: true },
{ title: "Status", value: "down", short: true },
{ title: "URL", value: "https://example.com", short: false },
]);
});
it("omits the incident field when its URL is empty", async () => {
const { provider } = createProvider();
const message = makeMessageWithIncident();
await provider.sendMessage(
makeNotification({ type: "rocket_chat" }),
makeMessage({
content: {
...message.content,
incident: { ...message.content.incident!, url: "" },
},
})
);
const fields = mockGotPost.mock.calls[0][1].json.attachments[0].fields;
expect(fields).not.toContainEqual(expect.objectContaining({ title: "Incident" }));
});
it("returns false when the message webhook URL is missing", async () => {
const { provider } = createProvider();
const result = await provider.sendMessage(makeNotification({ type: "rocket_chat", address: "" }), makeMessage());
expect(result).toBe(false);
expect(mockGotPost).not.toHaveBeenCalled();
});
it("returns false and logs when message delivery fails", async () => {
mockGotPost.mockRejectedValue(new Error("network failure"));
const { provider, logger } = createProvider();
const result = await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessage());
expect(result).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: "Rocket.Chat notification failed",
service: "RocketChatProvider",
method: "sendMessage",
})
);
});
});