Openstatus
www.openstatus.dev
1import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2import { selectNotificationSchema } from "@openstatus/db/src/schema";
3import { COLORS } from "@openstatus/notification-base";
4import {
5 sendAlert,
6 sendDegraded,
7 sendRecovery,
8 sendTestSlackMessage,
9} from "./index";
10
11describe("Slack Notifications", () => {
12 // biome-ignore lint/suspicious/noExplicitAny: <explanation>
13 let fetchMock: any = undefined;
14
15 beforeEach(() => {
16 // @ts-expect-error
17 fetchMock = spyOn(global, "fetch").mockImplementation(() =>
18 Promise.resolve(new Response(null, { status: 200 })),
19 );
20 });
21
22 afterEach(() => {
23 if (fetchMock) {
24 fetchMock.mockRestore();
25 }
26 });
27
28 const createMockMonitor = () => ({
29 id: "monitor-1",
30 name: "API Health Check",
31 url: "https://api.example.com/health",
32 jobType: "http" as const,
33 periodicity: "5m" as const,
34 status: "active" as const,
35 createdAt: new Date(),
36 updatedAt: new Date(),
37 region: "us-east-1",
38 });
39
40 const createMockNotification = () => ({
41 id: 1,
42 name: "Slack Notification",
43 provider: "slack",
44 workspaceId: 1,
45 createdAt: new Date(),
46 updatedAt: new Date(),
47 data: '{"slack":"https://hooks.slack.com/services/url"}',
48 });
49
50 test("Send Alert", async () => {
51 const monitor = createMockMonitor();
52 const notification = selectNotificationSchema.parse(
53 createMockNotification(),
54 );
55
56 await sendAlert({
57 // @ts-expect-error
58 monitor,
59 notification,
60 statusCode: 500,
61 message: "Something went wrong",
62 cronTimestamp: Date.now(),
63 });
64
65 expect(fetchMock).toHaveBeenCalledTimes(1);
66 const callArgs = fetchMock.mock.calls[0];
67 expect(callArgs[0]).toBe("https://hooks.slack.com/services/url");
68 expect(callArgs[1].method).toBe("POST");
69
70 const body = JSON.parse(callArgs[1].body);
71 expect(body.attachments).toBeDefined();
72 expect(body.attachments[0].color).toBe(COLORS.red);
73 expect(body.attachments[0].blocks).toBeDefined();
74 expect(body.attachments[0].blocks.length).toBeGreaterThan(0);
75 expect(body.attachments[0].blocks[0].text.text).toContain("is failing");
76 });
77
78 test("Send Alert without statusCode", async () => {
79 const monitor = createMockMonitor();
80 const notification = selectNotificationSchema.parse(
81 createMockNotification(),
82 );
83
84 await sendAlert({
85 // @ts-expect-error
86 monitor,
87 notification,
88 message: "Connection timeout",
89 cronTimestamp: Date.now(),
90 });
91
92 expect(fetchMock).toHaveBeenCalledTimes(1);
93 const callArgs = fetchMock.mock.calls[0];
94 const body = JSON.parse(callArgs[1].body);
95 expect(body.attachments[0].color).toBe(COLORS.red);
96 expect(body.attachments[0].blocks[3].fields[0].text).toContain("Unknown");
97 });
98
99 test("Send Recovery", async () => {
100 const monitor = createMockMonitor();
101 const notification = selectNotificationSchema.parse(
102 createMockNotification(),
103 );
104
105 await sendRecovery({
106 // @ts-expect-error
107 monitor,
108 notification,
109 statusCode: 200,
110 message: "Service recovered",
111 cronTimestamp: Date.now(),
112 });
113
114 expect(fetchMock).toHaveBeenCalledTimes(1);
115 const callArgs = fetchMock.mock.calls[0];
116 const body = JSON.parse(callArgs[1].body);
117 expect(body.attachments).toBeDefined();
118 expect(body.attachments[0].color).toBe(COLORS.green);
119 expect(body.attachments[0].blocks[0].text.text).toContain("is recovered");
120 });
121
122 test("Send Degraded", async () => {
123 const monitor = createMockMonitor();
124 const notification = selectNotificationSchema.parse(
125 createMockNotification(),
126 );
127
128 await sendDegraded({
129 // @ts-expect-error
130 monitor,
131 notification,
132 statusCode: 503,
133 message: "Service degraded",
134 cronTimestamp: Date.now(),
135 });
136
137 expect(fetchMock).toHaveBeenCalledTimes(1);
138 const callArgs = fetchMock.mock.calls[0];
139 const body = JSON.parse(callArgs[1].body);
140 expect(body.attachments).toBeDefined();
141 expect(body.attachments[0].color).toBe(COLORS.yellow);
142 expect(body.attachments[0].blocks[0].text.text).toContain("is degraded");
143 });
144
145 test("Send Test Slack Message", async () => {
146 const webhookUrl = "https://hooks.slack.com/services/test/url";
147
148 await sendTestSlackMessage(webhookUrl);
149
150 expect(fetchMock).toHaveBeenCalledTimes(1);
151 const callArgs = fetchMock.mock.calls[0];
152 expect(callArgs[0]).toBe(webhookUrl);
153
154 const body = JSON.parse(callArgs[1].body);
155 expect(body.attachments[0].blocks[0].text.text).toContain(
156 "Test Notification",
157 );
158 });
159
160 test("Send Test Slack Message throws error on empty webhookUrl", async () => {
161 fetchMock.mockImplementation(() =>
162 Promise.reject(new Error("Network error")),
163 );
164
165 expect(sendTestSlackMessage("")).rejects.toThrow();
166 expect(fetchMock).toHaveBeenCalledTimes(0);
167 });
168
169 test("Handle fetch error gracefully", async () => {
170 fetchMock.mockImplementation(() =>
171 Promise.reject(new Error("Network error")),
172 );
173
174 const monitor = createMockMonitor();
175 const notification = selectNotificationSchema.parse(
176 createMockNotification(),
177 );
178
179 // Should not throw - function catches errors internally
180 expect(
181 sendAlert({
182 // @ts-expect-error
183 monitor,
184 notification,
185 statusCode: 500,
186 message: "Error",
187 cronTimestamp: Date.now(),
188 }),
189 ).rejects.toThrow();
190
191 expect(fetchMock).toHaveBeenCalledTimes(1);
192 });
193});