From 750fc9e238e5ec38d49930a70e8b8b2ee8465aeb Mon Sep 17 00:00:00 2001 From: Marcos Paulo Diniz Date: Sun, 2 Aug 2026 01:59:11 -0300 Subject: [PATCH 1/7] feat(notifications): add Rocket.Chat provider Refs #3821 --- .../api/validation/notificationValidation.ts | 8 + server/src/config/services.shared.ts | 3 + .../notifications/notification.model.ts | 1 + .../domain/notifications/notification.type.ts | 1 + .../notifications/providers/rocketChat.ts | 88 +++++++++++ .../notifications/notificationType.test.ts | 15 ++ .../notifications/rocketChatProvider.test.ts | 143 ++++++++++++++++++ .../validation/notificationValidation.test.ts | 47 ++++++ 8 files changed, 306 insertions(+) create mode 100644 server/src/domain/notifications/providers/rocketChat.ts create mode 100644 server/test/unit/notifications/notificationType.test.ts create mode 100644 server/test/unit/providers/notifications/rocketChatProvider.test.ts create mode 100644 server/test/unit/validation/notificationValidation.test.ts diff --git a/server/src/api/validation/notificationValidation.ts b/server/src/api/validation/notificationValidation.ts index bd3cdd6c2..fea35e89d 100644 --- a/server/src/api/validation/notificationValidation.ts +++ b/server/src/api/validation/notificationValidation.ts @@ -23,6 +23,14 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ roomId: z.union([z.string(), z.literal("")]).optional(), accessToken: z.union([z.string(), z.literal("")]).optional(), }), + // Rocket.Chat notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("rocket_chat"), + address: z + .url({ message: "Please enter a valid Rocket.Chat webhook URL" }) + .refine((url) => /^https?:\/\//i.test(url), { message: "Rocket.Chat webhook URL must use HTTP or HTTPS" }), + }), // Slack notification z.object({ notificationName: z.string().min(1, "Notification name is required"), diff --git a/server/src/config/services.shared.ts b/server/src/config/services.shared.ts index 0f7813d8a..b278f6eab 100644 --- a/server/src/config/services.shared.ts +++ b/server/src/config/services.shared.ts @@ -23,6 +23,7 @@ import { ILogger } from "@/utils/logger.js"; // Notification providers import type { NotificationProviderRegistry } from "@/domain/notifications/notification.service.js"; import { WebhookProvider } from "@/domain/notifications/providers/webhook.js"; +import { RocketChatProvider } from "@/domain/notifications/providers/rocketChat.js"; import { SlackProvider } from "@/domain/notifications/providers/slack.js"; import { EmailProvider } from "@/domain/notifications/providers/email.js"; import { DiscordProvider } from "@/domain/notifications/providers/discord.js"; @@ -157,6 +158,7 @@ export const buildShared = async ({ }); const webhookProvider = new WebhookProvider(logger); + const rocketChatProvider = new RocketChatProvider(logger); const slackProvider = new SlackProvider(logger); const emailProvider = new EmailProvider(emailService, logger); const discordProvider = new DiscordProvider(logger); @@ -170,6 +172,7 @@ export const buildShared = async ({ const notificationProviders: NotificationProviderRegistry = { webhook: webhookProvider, + rocket_chat: rocketChatProvider, email: emailProvider, slack: slackProvider, discord: discordProvider, diff --git a/server/src/domain/notifications/notification.model.ts b/server/src/domain/notifications/notification.model.ts index 797b3e768..518a4fc3f 100755 --- a/server/src/domain/notifications/notification.model.ts +++ b/server/src/domain/notifications/notification.model.ts @@ -30,6 +30,7 @@ const NotificationSchema = new Schema( "slack", "discord", "webhook", + "rocket_chat", "pager_duty", "matrix", "teams", diff --git a/server/src/domain/notifications/notification.type.ts b/server/src/domain/notifications/notification.type.ts index 8ae617a88..7c4c9f75d 100644 --- a/server/src/domain/notifications/notification.type.ts +++ b/server/src/domain/notifications/notification.type.ts @@ -3,6 +3,7 @@ export const NotificationChannels = [ "slack", "discord", "webhook", + "rocket_chat", "pager_duty", "matrix", "teams", diff --git a/server/src/domain/notifications/providers/rocketChat.ts b/server/src/domain/notifications/providers/rocketChat.ts new file mode 100644 index 000000000..aa6852d74 --- /dev/null +++ b/server/src/domain/notifications/providers/rocketChat.ts @@ -0,0 +1,88 @@ +const SERVICE_NAME = "RocketChatProvider"; +import type { Notification, NotificationMessage } from "@/domain/notifications/notification.type.js"; +import { NotificationProvider } from "@/domain/notifications/providers/INotificationProvider.js"; +import { getTestMessage } from "@/domain/notifications/providers/utils.js"; +import got from "got"; + +export class RocketChatProvider extends NotificationProvider { + async sendTestAlert(notification: Partial): Promise { + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + ...this.gotRequestOptions(), + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Rocket.Chat test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); + return false; + } + } + + async sendMessage(notification: Notification, message: NotificationMessage): Promise { + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: this.buildText(message) }, + headers: { + "Content-Type": "application/json", + }, + ...this.gotRequestOptions(), + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Rocket.Chat notification failed", + service: SERVICE_NAME, + method: "sendMessage", + stack: err?.stack, + }); + return false; + } + } + + private buildText(message: NotificationMessage): string { + const lines = [ + message.content.title, + message.content.summary, + "", + `Monitor: ${message.monitor.name}`, + `Type: ${message.monitor.type}`, + `Status: ${message.monitor.status}`, + `URL: ${message.monitor.url}`, + ]; + + if (message.content.details?.length) { + lines.push("", "Details:", ...message.content.details.map((detail) => `- ${detail}`)); + } + if (message.content.thresholds?.length) { + lines.push( + "", + "Thresholds:", + ...message.content.thresholds.map( + (threshold) => `- ${threshold.metric.toUpperCase()}: ${threshold.formattedValue} (threshold: ${threshold.threshold}${threshold.unit})` + ) + ); + } + + lines.push("", `Incident: ${message.clientHost}/infrastructure/${message.monitor.id}`); + + return lines.join("\n"); + } +} diff --git a/server/test/unit/notifications/notificationType.test.ts b/server/test/unit/notifications/notificationType.test.ts new file mode 100644 index 000000000..0029fbd46 --- /dev/null +++ b/server/test/unit/notifications/notificationType.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "@jest/globals"; +import { NotificationModel } from "@/domain/notifications/notification.model.js"; +import { NotificationChannels } from "@/domain/notifications/notification.type.js"; + +describe("NotificationChannels", () => { + it("includes Rocket.Chat", () => { + expect(NotificationChannels).toContain("rocket_chat"); + }); + + it("allows Rocket.Chat to be persisted", () => { + const typePath = NotificationModel.schema.path("type") as unknown as { enumValues: string[] }; + + expect(typePath.enumValues).toContain("rocket_chat"); + }); +}); diff --git a/server/test/unit/providers/notifications/rocketChatProvider.test.ts b/server/test/unit/providers/notifications/rocketChatProvider.test.ts new file mode 100644 index 000000000..8ceb60bee --- /dev/null +++ b/server/test/unit/providers/notifications/rocketChatProvider.test.ts @@ -0,0 +1,143 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { createMockLogger } from "../../../helpers/createMockLogger.ts"; +import { makeMessage, 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 text-only test alert", 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" }, + timeout: { request: 30000 }, + retry: { limit: 0 }, + }) + ); + }); + + 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 monitor details as a text-only message", 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 Down: Test Monitor", + 'Monitor "Test Monitor" is currently down.', + "", + "Monitor: Test Monitor", + "Type: http", + "Status: down", + "URL: https://example.com", + "", + "Details:", + "- URL: https://example.com", + "- Status: Down", + "", + "Incident: https://app.example.com/infrastructure/mon-1", + ].join("\n"), + }, + }) + ); + }); + + 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("includes threshold breaches in the text", async () => { + const { provider } = createProvider(); + + await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessageWithThresholds()); + + const payload = mockGotPost.mock.calls[0][1].json; + expect(payload.text).toContain("Thresholds:\n- CPU: 90.0% (threshold: 80%)\n- MEMORY: 85.0% (threshold: 70%)"); + }); + + it("includes a monitor-scoped incident link without synthetic incident data", async () => { + const { provider } = createProvider(); + + await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessage()); + + const payload = mockGotPost.mock.calls[0][1].json; + expect(payload.text).toContain("Incident: https://app.example.com/infrastructure/mon-1"); + }); + + 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", + }) + ); + }); +}); diff --git a/server/test/unit/validation/notificationValidation.test.ts b/server/test/unit/validation/notificationValidation.test.ts new file mode 100644 index 000000000..5cb2c1d59 --- /dev/null +++ b/server/test/unit/validation/notificationValidation.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "@jest/globals"; +import { createNotificationBodyValidation } from "@/api/validation/notificationValidation.js"; + +describe("notification validation", () => { + it("accepts a Rocket.Chat incoming webhook", () => { + const result = createNotificationBodyValidation.safeParse({ + notificationName: "Rocket.Chat alerts", + type: "rocket_chat", + address: "https://chat.example.com/hooks/integration-id/token", + }); + + expect(result.success).toBe(true); + }); + + it("accepts a self-hosted Rocket.Chat webhook over HTTP", () => { + const result = createNotificationBodyValidation.safeParse({ + notificationName: "Local Rocket.Chat alerts", + type: "rocket_chat", + address: "http://localhost:3000/hooks/integration-id/token", + }); + + expect(result.success).toBe(true); + }); + + it("rejects an invalid Rocket.Chat incoming webhook URL", () => { + const result = createNotificationBodyValidation.safeParse({ + notificationName: "Rocket.Chat alerts", + type: "rocket_chat", + address: "not-a-url", + }); + + expect(result.success).toBe(false); + }); + + it.each(["ftp://chat.example.com/hooks/integration-id/token", "mailto:admin@example.com", "data:text/plain,hello", "javascript:alert(1)"])( + "rejects a Rocket.Chat webhook using an unsupported protocol: %s", + (address) => { + const result = createNotificationBodyValidation.safeParse({ + notificationName: "Rocket.Chat alerts", + type: "rocket_chat", + address, + }); + + expect(result.success).toBe(false); + } + ); +}); From 5774cb0e06dd69f52d47086041a589e9a8a89401 Mon Sep 17 00:00:00 2001 From: Marcos Paulo Diniz Date: Sun, 2 Aug 2026 01:59:34 -0300 Subject: [PATCH 2/7] feat(client): expose Rocket.Chat channel Refs #3821 --- client/src/Hooks/useNotificationForm.ts | 7 +++++++ .../Notifications/components/NotificationsTable.tsx | 8 +++++++- client/src/Pages/Notifications/create/index.tsx | 6 +++++- client/src/Types/Notification.ts | 1 + client/src/Validation/notifications.ts | 13 +++++++++++++ client/src/locales/en.json | 1 + 6 files changed, 34 insertions(+), 2 deletions(-) diff --git a/client/src/Hooks/useNotificationForm.ts b/client/src/Hooks/useNotificationForm.ts index 0051ddafd..31ab30150 100644 --- a/client/src/Hooks/useNotificationForm.ts +++ b/client/src/Hooks/useNotificationForm.ts @@ -46,6 +46,13 @@ function buildDefaults(data: Notification | null): NotificationFormData { address: data.address || "", }; } + if (data?.type === "rocket_chat") { + return { + type: "rocket_chat", + notificationName: data.notificationName || "", + address: data.address || "", + }; + } if (data?.type === "pager_duty") { return { type: "pager_duty", diff --git a/client/src/Pages/Notifications/components/NotificationsTable.tsx b/client/src/Pages/Notifications/components/NotificationsTable.tsx index a7a8170f6..0b41c3be0 100644 --- a/client/src/Pages/Notifications/components/NotificationsTable.tsx +++ b/client/src/Pages/Notifications/components/NotificationsTable.tsx @@ -65,7 +65,13 @@ export const NotificationsTable = ({ id: "type", content: t("common.table.headers.type"), render: (row) => { - return {row?.type}; + return ( + + {row?.type === "rocket_chat" + ? t("pages.notifications.form.type.rocketChat") + : row?.type} + + ); }, }, { diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx index 84d264616..955add20f 100644 --- a/client/src/Pages/Notifications/create/index.tsx +++ b/client/src/Pages/Notifications/create/index.tsx @@ -139,7 +139,11 @@ const NotificationsCreatePage = () => { key={type} value={type} > - {type} + + {type === "rocket_chat" + ? t("pages.notifications.form.type.rocketChat") + : type} + ))} diff --git a/client/src/Types/Notification.ts b/client/src/Types/Notification.ts index 0e389f9e2..a118c4a3d 100644 --- a/client/src/Types/Notification.ts +++ b/client/src/Types/Notification.ts @@ -3,6 +3,7 @@ export const NotificationChannels = [ "slack", "discord", "webhook", + "rocket_chat", "pager_duty", "matrix", "teams", diff --git a/client/src/Validation/notifications.ts b/client/src/Validation/notifications.ts index 6526fa0dd..3058d3f66 100644 --- a/client/src/Validation/notifications.ts +++ b/client/src/Validation/notifications.ts @@ -30,6 +30,18 @@ const webhookSchema = baseSchema.extend({ address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"), }); +const rocketChatSchema = baseSchema.extend({ + type: z.literal("rocket_chat"), + address: z + .string() + .min(1, "Webhook URL is required") + .url("Please enter a valid URL") + .refine( + (url) => /^https?:\/\//i.test(url), + "Rocket.Chat webhook URL must use HTTP or HTTPS" + ), +}); + const pagerDutySchema = baseSchema.extend({ type: z.literal("pager_duty"), address: z.string().min(1, "Integration key is required"), @@ -81,6 +93,7 @@ export const notificationSchema = z.discriminatedUnion("type", [ slackSchema, discordSchema, webhookSchema, + rocketChatSchema, pagerDutySchema, matrixSchema, teamsSchema, diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 955e9727f..87984495f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1164,6 +1164,7 @@ "type": { "description": "Select the type of notification channel to create.", "optionType": "Type", + "rocketChat": "Rocket.Chat", "title": "Channel type" }, "telegram": { From 2e19b85a2d57d84fc39e0df33bac59726452f9d9 Mon Sep 17 00:00:00 2001 From: Marcos Paulo Diniz Date: Sun, 2 Aug 2026 01:59:58 -0300 Subject: [PATCH 3/7] docs(notifications): document Rocket.Chat Refs #3821 --- README.md | 2 +- server/openapi.json | 29 ++++++++++++++++++++++++++- server/openapi/index.ts | 3 ++- server/openapi/routes/notification.ts | 8 ++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bcbd557d4..139df2372 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you! - Selective disk monitoring with mountpoint selection - Incidents at a glance - Status pages with 4 beautiful themes -- E-mail, Webhooks, Discord, Slack, PagerDuty, Matrix, Microsoft Teams, Telegram, Pushover, Twilio (SMS) notifications +- E-mail, Webhooks, Discord, Slack, PagerDuty, Matrix, Rocket.Chat, Microsoft Teams, Telegram, Pushover, Twilio (SMS) notifications - Scheduled maintenance - JSON query monitoring - Multi-language support for Arabic, Chinese (Simplified), Chinese (Traditional, Taiwan), Czech, English, Finnish, French, German, Japanese, Portuguese (Brazil), Russian, Spanish, Thai, Turkish, Ukrainian, and Vietnamese diff --git a/server/openapi.json b/server/openapi.json index 28a22f394..627354c55 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -34,7 +34,7 @@ }, { "name": "notifications", - "description": "Notification channels (email, webhook, Slack, Discord, PagerDuty, Matrix, Teams, Telegram, Pushover, Twilio) and test alerts." + "description": "Notification channels (email, webhook, Slack, Discord, PagerDuty, Matrix, Rocket.Chat, Teams, Telegram, Pushover, Twilio) and test alerts." }, { "name": "maintenance-window", @@ -879,6 +879,9 @@ { "$ref": "#/components/schemas/WebhookNotification" }, + { + "$ref": "#/components/schemas/RocketChatNotification" + }, { "$ref": "#/components/schemas/SlackNotification" }, @@ -912,6 +915,7 @@ "mapping": { "email": "#/components/schemas/EmailNotification", "webhook": "#/components/schemas/WebhookNotification", + "rocket_chat": "#/components/schemas/RocketChatNotification", "slack": "#/components/schemas/SlackNotification", "discord": "#/components/schemas/DiscordNotification", "pager_duty": "#/components/schemas/PagerDutyNotification", @@ -1036,6 +1040,29 @@ "address": "https://example.com/hooks/checkmate" } }, + "RocketChatNotification": { + "type": "object", + "properties": { + "notificationName": { + "type": "string", + "minLength": 1 + }, + "type": { + "type": "string", + "enum": ["rocket_chat"] + }, + "address": { + "type": "string", + "format": "uri" + } + }, + "required": ["notificationName", "type", "address"], + "example": { + "notificationName": "Rocket.Chat alerts", + "type": "rocket_chat", + "address": "https://chat.example.com/hooks/integration-id/token" + } + }, "SlackNotification": { "type": "object", "properties": { diff --git a/server/openapi/index.ts b/server/openapi/index.ts index ef3e0b1d0..c96cec233 100644 --- a/server/openapi/index.ts +++ b/server/openapi/index.ts @@ -39,7 +39,8 @@ export function getOpenApiSpec(): JsonObject { { name: "incidents", description: "Downtime incidents derived from monitor state changes, including filtering and CSV export." }, { name: "notifications", - description: "Notification channels (email, webhook, Slack, Discord, PagerDuty, Matrix, Teams, Telegram, Pushover, Twilio) and test alerts.", + description: + "Notification channels (email, webhook, Slack, Discord, PagerDuty, Matrix, Rocket.Chat, Teams, Telegram, Pushover, Twilio) and test alerts.", }, { name: "maintenance-window", description: "Scheduled maintenance windows during which monitors do not generate incidents or alerts." }, { name: "status-page", description: "Public status pages, their configuration, and the monitors they expose." }, diff --git a/server/openapi/routes/notification.ts b/server/openapi/routes/notification.ts index f1c70ca6d..7da45adfb 100644 --- a/server/openapi/routes/notification.ts +++ b/server/openapi/routes/notification.ts @@ -23,6 +23,14 @@ const notificationVariantMeta: Record Date: Wed, 5 Aug 2026 08:33:29 -0300 Subject: [PATCH 4/7] fix(notifications): guard incident link Refs #3821 --- .../notifications/providers/rocketChat.ts | 4 +++- .../notifications/rocketChatProvider.test.ts | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/server/src/domain/notifications/providers/rocketChat.ts b/server/src/domain/notifications/providers/rocketChat.ts index aa6852d74..345eec8c3 100644 --- a/server/src/domain/notifications/providers/rocketChat.ts +++ b/server/src/domain/notifications/providers/rocketChat.ts @@ -81,7 +81,9 @@ export class RocketChatProvider extends NotificationProvider { ); } - lines.push("", `Incident: ${message.clientHost}/infrastructure/${message.monitor.id}`); + if (message.content.incident) { + lines.push("", `Incident: ${message.clientHost}/infrastructure/${message.monitor.id}`); + } return lines.join("\n"); } diff --git a/server/test/unit/providers/notifications/rocketChatProvider.test.ts b/server/test/unit/providers/notifications/rocketChatProvider.test.ts index 8ceb60bee..ac9ffc4c1 100644 --- a/server/test/unit/providers/notifications/rocketChatProvider.test.ts +++ b/server/test/unit/providers/notifications/rocketChatProvider.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, jest } from "@jest/globals"; import { createMockLogger } from "../../../helpers/createMockLogger.ts"; -import { makeMessage, makeMessageWithThresholds, makeNotification } from "../../../helpers/notificationMessage.ts"; +import { makeMessage, makeMessageWithIncident, makeMessageWithThresholds, makeNotification } from "../../../helpers/notificationMessage.ts"; import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts"; const mockGotPost = jest.fn().mockResolvedValue({}); @@ -90,8 +90,6 @@ describe("RocketChatProvider", () => { "Details:", "- URL: https://example.com", "- Status: Down", - "", - "Incident: https://app.example.com/infrastructure/mon-1", ].join("\n"), }, }) @@ -116,15 +114,31 @@ describe("RocketChatProvider", () => { expect(payload.text).toContain("Thresholds:\n- CPU: 90.0% (threshold: 80%)\n- MEMORY: 85.0% (threshold: 70%)"); }); - it("includes a monitor-scoped incident link without synthetic incident data", async () => { + it("includes a monitor-scoped link when incident data is present", async () => { const { provider } = createProvider(); - await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessage()); + await provider.sendMessage(makeNotification({ type: "rocket_chat" }), makeMessageWithIncident()); const payload = mockGotPost.mock.calls[0][1].json; expect(payload.text).toContain("Incident: https://app.example.com/infrastructure/mon-1"); }); + it("omits the incident link when incident data is absent", async () => { + const { provider } = createProvider(); + + await provider.sendMessage( + makeNotification({ type: "rocket_chat" }), + makeMessage({ + type: "monitor_up", + severity: "success", + monitor: { id: "mon-1", name: "Test Monitor", url: "https://example.com", type: "http", status: "up" }, + }) + ); + + const payload = mockGotPost.mock.calls[0][1].json; + expect(payload.text).not.toContain("Incident:"); + }); + it("returns false and logs when message delivery fails", async () => { mockGotPost.mockRejectedValue(new Error("network failure")); const { provider, logger } = createProvider(); From 466d356ad1cffbb3a08662c360db7cb4b5c0b67e Mon Sep 17 00:00:00 2001 From: Marcos Paulo Diniz Date: Wed, 5 Aug 2026 08:34:12 -0300 Subject: [PATCH 5/7] refactor(validation): use Zod URL protocols Refs #3821 --- client/src/Validation/notifications.ts | 6 +----- server/src/api/validation/notificationValidation.ts | 7 ++++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/client/src/Validation/notifications.ts b/client/src/Validation/notifications.ts index 3058d3f66..f7490d526 100644 --- a/client/src/Validation/notifications.ts +++ b/client/src/Validation/notifications.ts @@ -35,11 +35,7 @@ const rocketChatSchema = baseSchema.extend({ address: z .string() .min(1, "Webhook URL is required") - .url("Please enter a valid URL") - .refine( - (url) => /^https?:\/\//i.test(url), - "Rocket.Chat webhook URL must use HTTP or HTTPS" - ), + .url({ protocol: /^https?$/, message: "Please enter a valid URL" }), }); const pagerDutySchema = baseSchema.extend({ diff --git a/server/src/api/validation/notificationValidation.ts b/server/src/api/validation/notificationValidation.ts index fea35e89d..e883b9006 100644 --- a/server/src/api/validation/notificationValidation.ts +++ b/server/src/api/validation/notificationValidation.ts @@ -27,9 +27,10 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("rocket_chat"), - address: z - .url({ message: "Please enter a valid Rocket.Chat webhook URL" }) - .refine((url) => /^https?:\/\//i.test(url), { message: "Rocket.Chat webhook URL must use HTTP or HTTPS" }), + address: z.url({ + protocol: /^https?$/, + message: "Please enter a valid Rocket.Chat webhook URL", + }), }), // Slack notification z.object({ From 1c291bb89338eba28e05c2e8f1bdad43d530b556 Mon Sep 17 00:00:00 2001 From: Marcos Paulo Diniz Date: Wed, 5 Aug 2026 08:35:56 -0300 Subject: [PATCH 6/7] refactor(client): use raw channel types Refs #3821 --- .../Pages/Notifications/components/NotificationsTable.tsx | 8 +------- client/src/Pages/Notifications/create/index.tsx | 6 +----- client/src/locales/en.json | 1 - 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/client/src/Pages/Notifications/components/NotificationsTable.tsx b/client/src/Pages/Notifications/components/NotificationsTable.tsx index 0b41c3be0..a7a8170f6 100644 --- a/client/src/Pages/Notifications/components/NotificationsTable.tsx +++ b/client/src/Pages/Notifications/components/NotificationsTable.tsx @@ -65,13 +65,7 @@ export const NotificationsTable = ({ id: "type", content: t("common.table.headers.type"), render: (row) => { - return ( - - {row?.type === "rocket_chat" - ? t("pages.notifications.form.type.rocketChat") - : row?.type} - - ); + return {row?.type}; }, }, { diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx index 955add20f..84d264616 100644 --- a/client/src/Pages/Notifications/create/index.tsx +++ b/client/src/Pages/Notifications/create/index.tsx @@ -139,11 +139,7 @@ const NotificationsCreatePage = () => { key={type} value={type} > - - {type === "rocket_chat" - ? t("pages.notifications.form.type.rocketChat") - : type} - + {type} ))} diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 87984495f..955e9727f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1164,7 +1164,6 @@ "type": { "description": "Select the type of notification channel to create.", "optionType": "Type", - "rocketChat": "Rocket.Chat", "title": "Channel type" }, "telegram": { From 94f1f7e576313533ec001d91ca728e561974287c Mon Sep 17 00:00:00 2001 From: Glazzze <2050408913@qq.com> Date: Thu, 6 Aug 2026 11:41:01 +0800 Subject: [PATCH 7/7] docs(readme): clarify MongoDB requirement for single-image deployment --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 139df2372..f055f6f39 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,9 @@ Usage instructions can be found [here](https://checkmate.so/docs). ## Installation -The quickest way to run Checkmate is the reference Docker Compose file, which runs the all-in-one image (`ghcr.io/bluewave-labs/checkmate`) plus MongoDB: +The quickest way to run Checkmate is the reference Docker Compose file. It starts two services: the all-in-one Checkmate application image (`ghcr.io/bluewave-labs/checkmate`) and a separate MongoDB service. + +> **What “all-in-one” means:** the Checkmate application is packaged in a single image; MongoDB is not embedded in that image and remains required. The reference Compose file starts MongoDB for you. For custom deployments, configure `DB_CONNECTION_STRING` to use an external MongoDB instance. ```bash curl -O https://raw.githubusercontent.com/bluewave-labs/checkmate/master/docker/docker-compose.yaml