Skip to content

Commit f167a8b

Browse files
committed
Use shared HTTP client for Discord webhooks
1 parent 02b55ba commit f167a8b

2 files changed

Lines changed: 34 additions & 45 deletions

File tree

packages/services/api/src/modules/alerts/providers/adapters/discord.spec.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ const logger = {
1414
const appBaseUrl = 'app-base-url';
1515
const webhookUrl = 'webhook-url';
1616

17+
function createAdapter() {
18+
const httpClient = {
19+
post: vi.fn().mockResolvedValue(undefined),
20+
};
21+
const adapter = new DiscordCommunicationAdapter(logger as any, httpClient as any, appBaseUrl);
22+
23+
return { adapter, httpClient };
24+
}
25+
1726
describe('DiscordCommunicationAdapter', () => {
1827
beforeEach(() => {
1928
vi.restoreAllMocks();
@@ -111,7 +120,7 @@ describe('DiscordCommunicationAdapter', () => {
111120
} as AlertChannel,
112121
} as SchemaChangeNotificationInput;
113122

114-
const adapter = new DiscordCommunicationAdapter(logger as any, appBaseUrl);
123+
const { adapter } = createAdapter();
115124
const sendDiscordMessageSpy = vi.spyOn(adapter, 'sendDiscordMessage');
116125

117126
await adapter.sendSchemaChangeNotification(input);
@@ -148,7 +157,7 @@ describe('DiscordCommunicationAdapter', () => {
148157
},
149158
channel: {},
150159
} as SchemaChangeNotificationInput;
151-
const adapter = new DiscordCommunicationAdapter(logger as any, appBaseUrl);
160+
const { adapter } = createAdapter();
152161
const sendDiscordMessageSpy = vi.spyOn(adapter, 'sendDiscordMessage');
153162

154163
await adapter.sendSchemaChangeNotification(input);
@@ -178,7 +187,7 @@ describe('DiscordCommunicationAdapter', () => {
178187
webhookEndpoint: webhookUrl,
179188
},
180189
} as ChannelConfirmationInput;
181-
const adapter = new DiscordCommunicationAdapter(logger as any, appBaseUrl);
190+
const { adapter } = createAdapter();
182191
const sendDiscordMessageSpy = vi.spyOn(adapter, 'sendDiscordMessage');
183192

184193
await adapter.sendChannelConfirmation(input);
@@ -211,19 +220,9 @@ describe('DiscordCommunicationAdapter', () => {
211220
});
212221

213222
describe('sendDiscordMessage', () => {
214-
const adapter = new DiscordCommunicationAdapter(logger as any, appBaseUrl);
215-
216-
beforeEach(() => {
217-
// @ts-expect-error mocking fetch
218-
global.fetch = vi.fn(() =>
219-
Promise.resolve({
220-
ok: true,
221-
statusText: 'OK',
222-
}),
223-
);
224-
});
225-
226223
it('sends a Discord webhook payload with embeds', async () => {
224+
const { adapter, httpClient } = createAdapter();
225+
227226
await adapter.sendDiscordMessage('http://example.com/webhook', {
228227
embeds: [
229228
{
@@ -234,14 +233,13 @@ describe('DiscordCommunicationAdapter', () => {
234233
],
235234
});
236235

237-
expect(fetch).toHaveBeenCalledWith(
236+
expect(httpClient.post).toHaveBeenCalledWith(
238237
'http://example.com/webhook',
239238
expect.objectContaining({
240-
method: 'POST',
241239
headers: {
242240
'Content-Type': 'application/json',
243241
},
244-
body: JSON.stringify({
242+
json: {
245243
username: 'GraphQL Hive',
246244
embeds: [
247245
{
@@ -251,12 +249,17 @@ describe('DiscordCommunicationAdapter', () => {
251249
},
252250
],
253251
allowed_mentions: { parse: [] },
254-
}),
252+
},
253+
context: {
254+
logger: expect.any(Object),
255+
},
255256
}),
256257
);
257258
});
258259

259260
it('truncates embed fields to Discord limits', async () => {
261+
const { adapter, httpClient } = createAdapter();
262+
260263
await adapter.sendDiscordMessage('http://example.com/webhook', {
261264
embeds: [
262265
{
@@ -272,9 +275,7 @@ describe('DiscordCommunicationAdapter', () => {
272275
],
273276
});
274277

275-
const body = JSON.parse(
276-
(fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0][1].body as string,
277-
);
278+
const body = httpClient.post.mock.calls[0][1].json;
278279

279280
expect(body.embeds[0].title).toHaveLength(256);
280281
expect(body.embeds[0].description).toHaveLength(4096);
@@ -283,8 +284,8 @@ describe('DiscordCommunicationAdapter', () => {
283284
});
284285

285286
it('handles failed send operation', async () => {
286-
// @ts-expect-error types obviously don't account for the fact this is mocked
287-
fetch.mockImplementationOnce(() => Promise.resolve({ ok: false, statusText: 'Bad Request' }));
287+
const { adapter, httpClient } = createAdapter();
288+
httpClient.post.mockRejectedValueOnce(new Error('Failed to send Discord message: Bad Request'));
288289

289290
await expect(
290291
adapter.sendDiscordMessage('http://example.com/webhook', {

packages/services/api/src/modules/alerts/providers/adapters/discord.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Inject, Injectable } from 'graphql-modules';
22
import { CriticalityLevel } from '@graphql-inspector/core';
33
import { SchemaChangeType } from '@hive/storage';
4+
import { HttpClient } from '../../../shared/providers/http-client';
45
import { Logger } from '../../../shared/providers/logger';
56
import { WEB_APP_URL } from '../../../shared/providers/tokens';
67
import {
@@ -51,6 +52,7 @@ export class DiscordCommunicationAdapter implements CommunicationAdapter {
5152

5253
constructor(
5354
logger: Logger,
55+
private httpClient: HttpClient,
5456
@Inject(WEB_APP_URL) private appBaseUrl: string,
5557
) {
5658
this.logger = logger.child({ service: 'DiscordCommunicationAdapter' });
@@ -135,13 +137,7 @@ export class DiscordCommunicationAdapter implements CommunicationAdapter {
135137
],
136138
});
137139
} catch (error) {
138-
const errorText =
139-
error instanceof Error
140-
? error.toString()
141-
: typeof error === 'string'
142-
? error
143-
: JSON.stringify(error);
144-
this.logger.error(`Failed to send Discord notification (error=%s)`, errorText);
140+
this.logger.error('Failed to send Discord notification (error=%o)', error);
145141
}
146142
}
147143

@@ -180,13 +176,7 @@ export class DiscordCommunicationAdapter implements CommunicationAdapter {
180176
],
181177
});
182178
} catch (error) {
183-
const errorText =
184-
error instanceof Error
185-
? error.toString()
186-
: typeof error === 'string'
187-
? error
188-
: JSON.stringify(error);
189-
this.logger.error(`Failed to send Discord notification`, errorText);
179+
this.logger.error('Failed to send Discord notification (error=%o)', error);
190180
}
191181
}
192182

@@ -202,17 +192,15 @@ export class DiscordCommunicationAdapter implements CommunicationAdapter {
202192
embeds: payload.embeds?.slice(0, DISCORD_MAX_EMBEDS).map(limitEmbed),
203193
};
204194

205-
const response = await fetch(webhookUrl, {
206-
method: 'POST',
195+
await this.httpClient.post(webhookUrl, {
207196
headers: {
208197
'Content-Type': 'application/json',
209198
},
210-
body: JSON.stringify(normalizedPayload),
199+
json: normalizedPayload,
200+
context: {
201+
logger: this.logger,
202+
},
211203
});
212-
213-
if (!response.ok) {
214-
throw new Error(`Failed to send Discord message: ${response.statusText}`);
215-
}
216204
}
217205
}
218206

0 commit comments

Comments
 (0)