Skip to content

Commit 635b18f

Browse files
committed
refactor: 코드 및 기능 단순화
당장 복잡한 기능이 필요할 것 같지 않아 단순한 코드로 변경
1 parent 9d11a69 commit 635b18f

File tree

12 files changed

+367
-1198
lines changed

12 files changed

+367
-1198
lines changed
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import 'reflect-metadata';
2+
import { Request, Response } from 'express';
3+
import { WebhookController } from '@/controllers/webhook.controller';
4+
import { sendSlackMessage } from '@/modules/slack/slack.notifier';
5+
import { SentryWebhookData } from '@/types';
6+
7+
// Mock dependencies
8+
jest.mock('@/modules/slack/slack.notifier');
9+
10+
// logger 모킹
11+
jest.mock('@/configs/logger.config', () => ({
12+
error: jest.fn(),
13+
info: jest.fn(),
14+
}));
15+
16+
describe('WebhookController', () => {
17+
let webhookController: WebhookController;
18+
let mockRequest: Partial<Request>;
19+
let mockResponse: Partial<Response>;
20+
let nextFunction: jest.Mock;
21+
let mockSendSlackMessage: jest.MockedFunction<typeof sendSlackMessage>;
22+
23+
beforeEach(() => {
24+
// WebhookController 인스턴스 생성
25+
webhookController = new WebhookController();
26+
27+
// Request, Response, NextFunction 모킹
28+
mockRequest = {
29+
body: {},
30+
headers: {},
31+
};
32+
33+
mockResponse = {
34+
json: jest.fn().mockReturnThis(),
35+
status: jest.fn().mockReturnThis(),
36+
};
37+
38+
nextFunction = jest.fn();
39+
mockSendSlackMessage = sendSlackMessage as jest.MockedFunction<typeof sendSlackMessage>;
40+
});
41+
42+
afterEach(() => {
43+
jest.clearAllMocks();
44+
});
45+
46+
describe('handleSentryWebhook', () => {
47+
const mockSentryData: SentryWebhookData = {
48+
action: 'created',
49+
data: {
50+
issue: {
51+
id: 'test-issue-123',
52+
title: '테스트 오류입니다',
53+
culprit: 'TestFile.js:10',
54+
status: 'unresolved',
55+
count: 5,
56+
userCount: 3,
57+
firstSeen: '2024-01-01T12:00:00.000Z',
58+
permalink: 'https://velog-dashboardv2.sentry.io/issues/test-issue-123/',
59+
project: {
60+
id: 'project-123',
61+
name: 'Velog Dashboard',
62+
slug: 'velog-dashboard'
63+
}
64+
}
65+
}
66+
};
67+
68+
it('유효한 Sentry 웹훅 데이터로 처리에 성공해야 한다', async () => {
69+
mockRequest.body = mockSentryData;
70+
mockSendSlackMessage.mockResolvedValue();
71+
72+
await webhookController.handleSentryWebhook(
73+
mockRequest as Request,
74+
mockResponse as Response,
75+
nextFunction
76+
);
77+
78+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
79+
expect.stringContaining('🚨 *새로운 오류가 발생했습니다*')
80+
);
81+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
82+
expect.stringContaining('🔴 *제목:* 테스트 오류입니다')
83+
);
84+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
85+
expect.stringContaining('📍 *위치:* TestFile.js:10')
86+
);
87+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
88+
expect.stringContaining('🔗 *상세 보기:* https://velog-dashboardv2.sentry.io/issues/test-issue-123/')
89+
);
90+
expect(mockResponse.status).toHaveBeenCalledWith(200);
91+
expect(mockResponse.json).toHaveBeenCalledWith({
92+
success: true,
93+
message: 'Sentry 웹훅 처리에 성공하였습니다.',
94+
data: {},
95+
error: null
96+
});
97+
});
98+
99+
it('permalink가 없는 경우 기본 URL 패턴을 사용해야 한다', async () => {
100+
const dataWithoutPermalink = {
101+
...mockSentryData,
102+
data: {
103+
...mockSentryData.data,
104+
issue: {
105+
...mockSentryData.data.issue,
106+
permalink: undefined
107+
}
108+
}
109+
};
110+
mockRequest.body = dataWithoutPermalink;
111+
mockSendSlackMessage.mockResolvedValue();
112+
113+
await webhookController.handleSentryWebhook(
114+
mockRequest as Request,
115+
mockResponse as Response,
116+
nextFunction
117+
);
118+
119+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
120+
expect.stringContaining('🔗 *상세 보기:* https://velog-dashboardv2.sentry.io/issues/test-issue-123/')
121+
);
122+
});
123+
124+
it('resolved 액션에 대해 올바른 메시지를 생성해야 한다', async () => {
125+
const resolvedData = {
126+
...mockSentryData,
127+
action: 'resolved' as const,
128+
data: {
129+
...mockSentryData.data,
130+
issue: {
131+
...mockSentryData.data.issue,
132+
status: 'resolved' as const
133+
}
134+
}
135+
};
136+
mockRequest.body = resolvedData;
137+
mockSendSlackMessage.mockResolvedValue();
138+
139+
await webhookController.handleSentryWebhook(
140+
mockRequest as Request,
141+
mockResponse as Response,
142+
nextFunction
143+
);
144+
145+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
146+
expect.stringContaining('🚨 *오류가 해결되었습니다*')
147+
);
148+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
149+
expect.stringContaining('✅ *제목:*')
150+
);
151+
});
152+
153+
it('ignored 액션에 대해 올바른 메시지를 생성해야 한다', async () => {
154+
const ignoredData = {
155+
...mockSentryData,
156+
action: 'ignored' as const,
157+
data: {
158+
...mockSentryData.data,
159+
issue: {
160+
...mockSentryData.data.issue,
161+
status: 'ignored' as const
162+
}
163+
}
164+
};
165+
mockRequest.body = ignoredData;
166+
mockSendSlackMessage.mockResolvedValue();
167+
168+
await webhookController.handleSentryWebhook(
169+
mockRequest as Request,
170+
mockResponse as Response,
171+
nextFunction
172+
);
173+
174+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
175+
expect.stringContaining('🚨 *오류가 무시되었습니다*')
176+
);
177+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
178+
expect.stringContaining('🔇 *제목:*')
179+
);
180+
});
181+
182+
it('알 수 없는 액션에 대해 기본 메시지를 생성해야 한다', async () => {
183+
const unknownActionData = {
184+
...mockSentryData,
185+
action: 'unknown_action' as 'created'
186+
};
187+
mockRequest.body = unknownActionData;
188+
mockSendSlackMessage.mockResolvedValue();
189+
190+
await webhookController.handleSentryWebhook(
191+
mockRequest as Request,
192+
mockResponse as Response,
193+
nextFunction
194+
);
195+
196+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
197+
expect.stringContaining('오류 이벤트: unknown_action')
198+
);
199+
});
200+
201+
it('알 수 없는 상태에 대해 기본 이모지를 사용해야 한다', async () => {
202+
const unknownStatusData = {
203+
...mockSentryData,
204+
data: {
205+
...mockSentryData.data,
206+
issue: {
207+
...mockSentryData.data.issue,
208+
status: 'unknown_status' as 'unresolved'
209+
}
210+
}
211+
};
212+
mockRequest.body = unknownStatusData;
213+
mockSendSlackMessage.mockResolvedValue();
214+
215+
await webhookController.handleSentryWebhook(
216+
mockRequest as Request,
217+
mockResponse as Response,
218+
nextFunction
219+
);
220+
221+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
222+
expect.stringContaining('❓ *제목:*')
223+
);
224+
});
225+
226+
it('Slack 메시지 전송 실패 시 에러를 전달해야 한다', async () => {
227+
mockRequest.body = mockSentryData;
228+
const slackError = new Error('Slack 전송 실패');
229+
mockSendSlackMessage.mockRejectedValue(slackError);
230+
231+
await webhookController.handleSentryWebhook(
232+
mockRequest as Request,
233+
mockResponse as Response,
234+
nextFunction
235+
);
236+
237+
expect(nextFunction).toHaveBeenCalledWith(slackError);
238+
expect(mockResponse.json).not.toHaveBeenCalled();
239+
});
240+
241+
it('빈 body로 요청 시에도 처리해야 한다', async () => {
242+
mockRequest.body = {};
243+
mockSendSlackMessage.mockResolvedValue();
244+
245+
await webhookController.handleSentryWebhook(
246+
mockRequest as Request,
247+
mockResponse as Response,
248+
nextFunction
249+
);
250+
251+
// undefined 값들에 대해서도 처리되어야 함
252+
expect(mockSendSlackMessage).toHaveBeenCalled();
253+
expect(mockResponse.status).toHaveBeenCalledWith(200);
254+
});
255+
256+
it('필수 필드가 없는 경우에도 처리해야 한다', async () => {
257+
const incompleteData = {
258+
action: 'created',
259+
data: {
260+
issue: {
261+
id: 'test-123'
262+
// title, culprit 등 누락
263+
}
264+
}
265+
};
266+
mockRequest.body = incompleteData;
267+
mockSendSlackMessage.mockResolvedValue();
268+
269+
await webhookController.handleSentryWebhook(
270+
mockRequest as Request,
271+
mockResponse as Response,
272+
nextFunction
273+
);
274+
275+
expect(mockSendSlackMessage).toHaveBeenCalledWith(
276+
expect.stringContaining('🔗 *상세 보기:* https://velog-dashboardv2.sentry.io/issues/test-123/')
277+
);
278+
expect(mockResponse.status).toHaveBeenCalledWith(200);
279+
});
280+
});
281+
282+
describe('formatSentryMessage (private method integration test)', () => {
283+
it('완전한 Sentry 데이터로 올바른 형식의 메시지를 생성해야 한다', async () => {
284+
const completeData: SentryWebhookData = {
285+
action: 'created',
286+
data: {
287+
issue: {
288+
id: 'issue-456',
289+
title: 'TypeError: Cannot read property of undefined',
290+
culprit: 'components/UserProfile.tsx:25',
291+
status: 'unresolved',
292+
count: 12,
293+
userCount: 8,
294+
firstSeen: '2024-01-15T14:30:00.000Z',
295+
permalink: 'https://velog-dashboardv2.sentry.io/issues/issue-456/',
296+
project: {
297+
id: 'proj-789',
298+
name: 'Velog Dashboard V2',
299+
slug: 'velog-dashboard-v2'
300+
}
301+
}
302+
}
303+
};
304+
305+
mockRequest.body = completeData;
306+
mockSendSlackMessage.mockResolvedValue();
307+
308+
await webhookController.handleSentryWebhook(
309+
mockRequest as Request,
310+
mockResponse as Response,
311+
nextFunction
312+
);
313+
314+
const expectedMessage = `🚨 *새로운 오류가 발생했습니다*
315+
316+
🔴 *제목:* TypeError: Cannot read property of undefined
317+
318+
📍 *위치:* components/UserProfile.tsx:25
319+
320+
🔗 *상세 보기:* https://velog-dashboardv2.sentry.io/issues/issue-456/`;
321+
322+
expect(mockSendSlackMessage).toHaveBeenCalledWith(expectedMessage);
323+
});
324+
});
325+
});

0 commit comments

Comments
 (0)