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 => ({ 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 => ({ 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; 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", }) ); }); }); });