Openstatus www.openstatus.dev

fix: webhook test (#1614)

* fix: webhook test

* chore: test sendTest function

* update pnpm lock

* ci: apply automated fixes

---------

Co-authored-by: Thibault Le Ouay Ducasse <thibaultleouay@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

+140 -18
+3 -15
apps/dashboard/src/components/forms/notifications/form-webhook.tsx
··· 98 98 startTransition(async () => { 99 99 try { 100 100 const provider = form.getValues("provider"); 101 - const data = form.getValues("data"); 102 - const promise = config[provider].sendTest( 103 - data as unknown as { 104 - url: string; 105 - headers?: { key: string; value: string }[]; 106 - }, 107 - ); 108 - toast.promise(promise, { 101 + const data = form.getValues("data.endpoint"); 102 + toast.promise(config[provider].sendTest({ url: data }), { 109 103 loading: "Sending test...", 110 104 success: "Test sent", 111 - error: (error) => { 112 - if (error instanceof Error) { 113 - return error.message; 114 - } 115 - return "Failed to send test"; 116 - }, 105 + error: "Failed to send test", 117 106 }); 118 - await promise; 119 107 } catch (error) { 120 108 console.error(error); 121 109 }
+4
packages/notifications/webhook/package.json
··· 2 2 "name": "@openstatus/notification-webhook", 3 3 "version": "1.0.0", 4 4 "main": "src/index.ts", 5 + "scripts": { 6 + "test": "bun test" 7 + }, 5 8 "dependencies": { 6 9 "@openstatus/db": "workspace:*", 7 10 "@openstatus/utils": "workspace:*", ··· 10 13 "devDependencies": { 11 14 "@openstatus/tsconfig": "workspace:*", 12 15 "@types/node": "22.10.2", 16 + "bun-types": "1.3.1", 13 17 "typescript": "5.7.2" 14 18 } 15 19 }
+124
packages/notifications/webhook/src/index.test.ts
··· 1 + import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; 2 + import { sendTest } from "./index"; 3 + 4 + describe("Webhook sendTest", () => { 5 + // biome-ignore lint/suspicious/noExplicitAny: <explanation> 6 + let fetchMock: any = undefined; 7 + 8 + beforeEach(() => { 9 + fetchMock = spyOn(global, "fetch"); 10 + }); 11 + 12 + afterEach(() => { 13 + if (fetchMock) { 14 + fetchMock.mockClear(); 15 + fetchMock.mockRestore(); 16 + } 17 + }); 18 + 19 + test("should send test webhook successfully", async () => { 20 + const url = "https://example.com/webhook"; 21 + fetchMock.mockResolvedValue(new Response(null, { status: 200 })); 22 + 23 + const result = await sendTest({ url }); 24 + 25 + expect(result).toBe(true); 26 + expect(fetchMock).toHaveBeenCalledTimes(1); 27 + expect(fetchMock).toHaveBeenCalledWith( 28 + url, 29 + expect.objectContaining({ 30 + method: "post", 31 + body: expect.any(String), 32 + headers: undefined, 33 + }), 34 + ); 35 + 36 + const callArgs = fetchMock.mock.calls[0]; 37 + const body = JSON.parse(callArgs[1].body); 38 + expect(body).toMatchObject({ 39 + monitor: { 40 + id: 1, 41 + name: "test", 42 + url: "http://openstat.us", 43 + }, 44 + status: "recovered", 45 + statusCode: 200, 46 + latency: 1337, 47 + }); 48 + expect(body.cronTimestamp).toBeTypeOf("number"); 49 + }); 50 + 51 + test("should send test webhook with headers", async () => { 52 + const url = "https://example.com/webhook"; 53 + const headers = [ 54 + { key: "Authorization", value: "Bearer token123" }, 55 + { key: "Content-Type", value: "application/json" }, 56 + ]; 57 + fetchMock.mockResolvedValue(new Response(null, { status: 200 })); 58 + 59 + const result = await sendTest({ url, headers }); 60 + 61 + expect(result).toBe(true); 62 + expect(fetchMock).toHaveBeenCalledTimes(1); 63 + expect(fetchMock).toHaveBeenCalledWith( 64 + url, 65 + expect.objectContaining({ 66 + method: "post", 67 + body: expect.any(String), 68 + headers: { 69 + Authorization: "Bearer token123", 70 + "Content-Type": "application/json", 71 + }, 72 + }), 73 + ); 74 + }); 75 + 76 + test("should throw error when response is not ok", async () => { 77 + const url = "https://example.com/webhook"; 78 + fetchMock.mockResolvedValue(new Response(null, { status: 400 })); 79 + 80 + await expect(sendTest({ url })).rejects.toThrow("Failed to send test"); 81 + expect(fetchMock).toHaveBeenCalledTimes(1); 82 + }); 83 + 84 + test("should throw error when fetch fails", async () => { 85 + const url = "https://example.com/webhook"; 86 + const networkError = new Error("Network error"); 87 + fetchMock.mockRejectedValue(networkError); 88 + 89 + await expect(sendTest({ url })).rejects.toThrow("Failed to send test"); 90 + expect(fetchMock).toHaveBeenCalledTimes(1); 91 + }); 92 + 93 + test("should send test webhook with empty headers array", async () => { 94 + const url = "https://example.com/webhook"; 95 + const headers: { key: string; value: string }[] = []; 96 + fetchMock.mockResolvedValue(new Response(null, { status: 200 })); 97 + 98 + const result = await sendTest({ url, headers }); 99 + 100 + expect(result).toBe(true); 101 + expect(fetchMock).toHaveBeenCalledTimes(1); 102 + const callArgs = fetchMock.mock.calls[0]; 103 + // Empty headers array should result in empty object after transformHeaders 104 + expect(callArgs[1].headers).toEqual({}); 105 + }); 106 + 107 + test("should send test webhook with 500 status code", async () => { 108 + const url = "https://example.com/webhook"; 109 + fetchMock.mockResolvedValue(new Response(null, { status: 500 })); 110 + 111 + await expect(sendTest({ url })).rejects.toThrow("Failed to send test"); 112 + expect(fetchMock).toHaveBeenCalledTimes(1); 113 + }); 114 + 115 + test("should send test webhook with 201 status code (success)", async () => { 116 + const url = "https://example.com/webhook"; 117 + fetchMock.mockResolvedValue(new Response(null, { status: 201 })); 118 + 119 + const result = await sendTest({ url }); 120 + 121 + expect(result).toBe(true); 122 + expect(fetchMock).toHaveBeenCalledTimes(1); 123 + }); 124 + });
+6 -3
packages/notifications/webhook/src/index.ts
··· 153 153 latency: 1337, 154 154 }); 155 155 try { 156 - await fetch(url, { 156 + const response = await fetch(url, { 157 157 method: "post", 158 158 body: JSON.stringify(body), 159 159 headers: headers ? transformHeaders(headers) : undefined, 160 160 }); 161 + if (!response.ok) { 162 + throw new Error("Failed to send test"); 163 + } 164 + return true; 161 165 } catch (err) { 162 166 console.log(err); 163 - return false; 167 + throw new Error("Failed to send test"); 164 168 } 165 - return true; 166 169 };
+3
pnpm-lock.yaml
··· 1620 1620 '@types/node': 1621 1621 specifier: 22.10.2 1622 1622 version: 22.10.2 1623 + bun-types: 1624 + specifier: 1.3.1 1625 + version: 1.3.1(@types/react@19.2.2) 1623 1626 typescript: 1624 1627 specifier: 5.7.2 1625 1628 version: 5.7.2