Files
Checkmate/server/test/teamsProvider.test.ts

268 lines
8.4 KiB
TypeScript

import { describe, expect, it, jest, beforeEach } from "@jest/globals";
import { TeamsProvider } from "../src/service/infrastructure/notificationProviders/teams.ts";
import type { Notification } from "../src/types/notification.ts";
import type { NotificationMessage } from "../src/types/notificationMessage.ts";
// Mock got
jest.unstable_mockModule("got", () => ({
default: { post: jest.fn() },
}));
const createLogger = () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
});
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: TeamsProvider;
let logger: ReturnType<typeof createLogger>;
let gotPost: jest.Mock;
beforeEach(async () => {
logger = createLogger();
provider = new TeamsProvider(logger);
const got = await import("got");
gotPost = got.default.post as jest.Mock;
gotPost.mockReset();
gotPost.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(gotPost).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(gotPost).toHaveBeenCalledTimes(1);
const [url, options] = gotPost.mock.calls[0] as [string, { json: any; headers: any }];
expect(url).toBe(notification.address);
expect(options.headers["Content-Type"]).toBe("application/json");
// Verify Teams webhook envelope
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");
// Verify Adaptive Card structure
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).toContain("test notification");
});
it("returns false and logs warning on HTTP error", async () => {
gotPost.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(gotPost).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(gotPost).toHaveBeenCalledTimes(1);
const [url, options] = gotPost.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");
// Verify body contains expected elements
const textBlocks = card.body.filter((b: any) => b.type === "TextBlock");
const factSets = card.body.filter((b: any) => b.type === "FactSet");
// Title block
expect(textBlocks[0].text).toBe(message.content.title);
expect(textBlocks[0].color).toBe("attention"); // critical -> attention
// Summary block
expect(textBlocks[1].text).toBe(message.content.summary);
// FactSet with monitor details
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" }),
])
);
// Incident action button
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("/infrastructure/mon-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] = gotPost.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] = gotPost.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) {
gotPost.mockReset();
gotPost.mockResolvedValue({});
const message = createMessage({ severity });
await provider.sendMessage(notification, message);
const [, options] = gotPost.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 () => {
gotPost.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",
})
);
});
});
});