Files
Checkmate/server/test/unit/providers/teamsProvider.test.ts
T

253 lines
8.2 KiB
TypeScript

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";
const mockPost = jest.fn();
jest.unstable_mockModule("got", () => ({
default: { post: mockPost },
HTTPError: class HTTPError extends Error {},
}));
const { TeamsProvider } = await import("../../../src/service/infrastructure/notificationProviders/teams.ts");
const createNotification = (overrides?: Partial<Notification>): Notification => ({
id: "notif-1",
userId: "user-1",
teamId: "team-1",
type: "teams",
notificationName: "Teams Alert",
address: "https://xxxxx.webhook.office.com/webhookb2/test",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
});
const createMessage = (overrides?: Partial<NotificationMessage>): NotificationMessage => ({
type: "monitor_down",
severity: "critical",
monitor: {
id: "mon-1",
name: "API Server",
url: "https://api.example.com",
type: "http",
status: "down",
},
content: {
title: "🔴 Monitor Down",
summary: "API Server is not responding",
details: ["Response timeout after 30s"],
thresholds: [],
incident: {
id: "inc-1",
url: "https://app.example.com/incidents/inc-1",
createdAt: new Date(),
},
timestamp: new Date("2026-01-15T12:00:00Z"),
},
clientHost: "https://app.example.com",
metadata: {
teamId: "team-1",
notificationReason: "monitor_down",
},
...overrides,
});
describe("TeamsProvider", () => {
let provider: InstanceType<typeof TeamsProvider>;
let logger: ReturnType<typeof createMockLogger>;
beforeEach(() => {
logger = createMockLogger();
provider = new TeamsProvider(logger);
mockPost.mockReset();
mockPost.mockResolvedValue({});
});
describe("sendTestAlert", () => {
it("returns false when address is missing", async () => {
const notification = createNotification({ address: undefined });
const result = await provider.sendTestAlert(notification);
expect(result).toBe(false);
expect(mockPost).not.toHaveBeenCalled();
});
it("sends an Adaptive Card test message and returns true on success", async () => {
const notification = createNotification();
const result = await provider.sendTestAlert(notification);
expect(result).toBe(true);
expect(mockPost).toHaveBeenCalledTimes(1);
const [url, options] = mockPost.mock.calls[0] as [string, { json: any; headers: any }];
expect(url).toBe(notification.address);
expect(options.headers["Content-Type"]).toBe("application/json");
const payload = options.json;
expect(payload.type).toBe("message");
expect(payload.attachments).toHaveLength(1);
expect(payload.attachments[0].contentType).toBe("application/vnd.microsoft.card.adaptive");
const card = payload.attachments[0].content;
expect(card.type).toBe("AdaptiveCard");
expect(card.version).toBe("1.4");
expect(card.body[0].type).toBe("TextBlock");
expect(card.body[0].text).toBe("This is a test notification from Checkmate");
});
it("returns false and logs warning on HTTP error", async () => {
mockPost.mockRejectedValue(new Error("Network error"));
const notification = createNotification();
const result = await provider.sendTestAlert(notification);
expect(result).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: "Teams test alert failed",
service: "TeamsProvider",
})
);
});
});
describe("sendMessage", () => {
it("returns false when address is missing", async () => {
const notification = createNotification({ address: undefined });
const result = await provider.sendMessage(notification, createMessage());
expect(result).toBe(false);
expect(mockPost).not.toHaveBeenCalled();
});
it("sends a well-formed Adaptive Card and returns true", async () => {
const notification = createNotification();
const message = createMessage();
const result = await provider.sendMessage(notification, message);
expect(result).toBe(true);
expect(mockPost).toHaveBeenCalledTimes(1);
const [url, options] = mockPost.mock.calls[0] as [string, { json: any }];
expect(url).toBe(notification.address);
const payload = options.json;
expect(payload.type).toBe("message");
const card = payload.attachments[0].content;
expect(card.type).toBe("AdaptiveCard");
expect(card.version).toBe("1.4");
const textBlocks = card.body.filter((b: any) => b.type === "TextBlock");
const factSets = card.body.filter((b: any) => b.type === "FactSet");
expect(textBlocks[0].text).toBe(message.content.title);
expect(textBlocks[0].color).toBe("attention");
expect(textBlocks[1].text).toBe(message.content.summary);
expect(factSets).toHaveLength(1);
const facts = factSets[0].facts;
expect(facts).toEqual(
expect.arrayContaining([
expect.objectContaining({ title: "Name", value: "API Server" }),
expect.objectContaining({ title: "URL", value: "https://api.example.com" }),
expect.objectContaining({ title: "Status", value: "down" }),
])
);
expect(card.actions).toHaveLength(1);
expect(card.actions[0].type).toBe("Action.OpenUrl");
expect(card.actions[0].title).toBe("View Incident");
expect(card.actions[0].url).toContain("/incidents/inc-1");
});
it("omits actions when no incident is present", async () => {
const notification = createNotification();
const message = createMessage({
content: {
title: "Test",
summary: "Summary",
timestamp: new Date(),
},
});
const result = await provider.sendMessage(notification, message);
expect(result).toBe(true);
const [, options] = mockPost.mock.calls[0] as [string, { json: any }];
const card = options.json.attachments[0].content;
expect(card.actions).toBeUndefined();
});
it("includes threshold breaches when present", async () => {
const notification = createNotification();
const message = createMessage({
type: "threshold_breach",
severity: "warning",
content: {
title: "Threshold Breach",
summary: "CPU usage exceeded",
thresholds: [
{
metric: "cpu",
currentValue: 95,
threshold: 80,
unit: "%",
formattedValue: "95%",
},
],
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;
// Title should use "warning" color
expect(card.body[0].color).toBe("warning");
// Find threshold text blocks
const thresholdHeader = card.body.find((b: any) => b.type === "TextBlock" && b.text === "**Threshold Breaches**");
expect(thresholdHeader).toBeDefined();
const cpuBlock = card.body.find((b: any) => b.type === "TextBlock" && b.text?.includes("**CPU**"));
expect(cpuBlock).toBeDefined();
expect(cpuBlock.text).toContain("95%");
expect(cpuBlock.text).toContain("threshold: 80%");
});
it("maps severity to correct Adaptive Card colors", async () => {
const notification = createNotification();
const severityMap = [
{ severity: "critical", expected: "attention" },
{ severity: "warning", expected: "warning" },
{ severity: "success", expected: "good" },
{ severity: "info", expected: "accent" },
] as const;
for (const { severity, expected } of severityMap) {
mockPost.mockReset();
mockPost.mockResolvedValue({});
const message = createMessage({ severity });
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(expected);
}
});
it("returns false and logs warning on HTTP error", async () => {
mockPost.mockRejectedValue(new Error("502 Bad Gateway"));
const notification = createNotification();
const result = await provider.sendMessage(notification, createMessage());
expect(result).toBe(false);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
message: "Teams alert failed via sendMessage",
service: "TeamsProvider",
})
);
});
});
});