Skip to content

Commit 0116921

Browse files
committed
feature: 테스트 코드 추가
1 parent 60c416c commit 0116921

File tree

2 files changed

+269
-0
lines changed

2 files changed

+269
-0
lines changed

src/controllers/__test__/webhook.controller.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import 'reflect-metadata';
22
import { Request, Response } from 'express';
33
import { WebhookController } from '@/controllers/webhook.controller';
44
import { sendSlackMessage } from '@/modules/slack/slack.notifier';
5+
import { verifySignature } from '@/utils/verify.util';
56

67
// Mock dependencies
78
jest.mock('@/modules/slack/slack.notifier');
9+
jest.mock('@/utils/verify.util');
810

911
// logger 모킹
1012
jest.mock('@/configs/logger.config', () => ({
@@ -18,6 +20,7 @@ describe('WebhookController', () => {
1820
let mockResponse: Partial<Response>;
1921
let nextFunction: jest.Mock;
2022
let mockSendSlackMessage: jest.MockedFunction<typeof sendSlackMessage>;
23+
let mockVerifySignature: jest.MockedFunction<typeof verifySignature>;
2124

2225
beforeEach(() => {
2326
// WebhookController 인스턴스 생성
@@ -36,6 +39,10 @@ describe('WebhookController', () => {
3639

3740
nextFunction = jest.fn();
3841
mockSendSlackMessage = sendSlackMessage as jest.MockedFunction<typeof sendSlackMessage>;
42+
mockVerifySignature = verifySignature as jest.MockedFunction<typeof verifySignature>;
43+
44+
// 기본적으로 시그니처 검증이 성공하도록 설정
45+
mockVerifySignature.mockReturnValue(true);
3946
});
4047

4148
afterEach(() => {
@@ -308,4 +315,108 @@ describe('WebhookController', () => {
308315
expect(mockSendSlackMessage).toHaveBeenCalledWith(expectedMessage);
309316
});
310317
});
318+
319+
describe('Signature Verification', () => {
320+
const mockSentryData = {
321+
action: 'created',
322+
data: {
323+
issue: {
324+
id: 'test-issue-123',
325+
title: '시그니처 테스트 오류',
326+
culprit: 'TestFile.js:10',
327+
status: 'unresolved',
328+
count: "1",
329+
userCount: 1,
330+
firstSeen: '2024-01-01T12:00:00.000Z',
331+
permalink: 'https://velog-dashboardv2.sentry.io/issues/test-issue-123/',
332+
project: {
333+
id: 'project-123',
334+
name: 'Velog Dashboard',
335+
slug: 'velog-dashboard'
336+
}
337+
}
338+
}
339+
};
340+
341+
it('유효한 시그니처로 웹훅 처리에 성공해야 한다', async () => {
342+
mockRequest.body = mockSentryData;
343+
mockRequest.headers = {
344+
'sentry-hook-signature': 'valid-signature'
345+
};
346+
mockVerifySignature.mockReturnValue(true);
347+
mockSendSlackMessage.mockResolvedValue();
348+
349+
await webhookController.handleSentryWebhook(
350+
mockRequest as Request,
351+
mockResponse as Response,
352+
nextFunction
353+
);
354+
355+
expect(mockVerifySignature).toHaveBeenCalledWith(mockRequest);
356+
expect(mockSendSlackMessage).toHaveBeenCalled();
357+
expect(mockResponse.status).toHaveBeenCalledWith(200);
358+
});
359+
360+
it('잘못된 시그니처로 400 에러를 반환해야 한다', async () => {
361+
mockRequest.body = mockSentryData;
362+
mockRequest.headers = {
363+
'sentry-hook-signature': 'invalid-signature'
364+
};
365+
mockVerifySignature.mockReturnValue(false);
366+
367+
await webhookController.handleSentryWebhook(
368+
mockRequest as Request,
369+
mockResponse as Response,
370+
nextFunction
371+
);
372+
373+
expect(mockVerifySignature).toHaveBeenCalledWith(mockRequest);
374+
expect(mockSendSlackMessage).not.toHaveBeenCalled();
375+
expect(mockResponse.status).toHaveBeenCalledWith(400);
376+
expect(mockResponse.json).toHaveBeenCalledWith({
377+
success: true,
378+
message: 'Sentry 웹훅 처리에 실패했습니다',
379+
data: {},
380+
error: null
381+
});
382+
});
383+
384+
it('시그니처 헤더가 누락된 경우 400 에러를 반환해야 한다', async () => {
385+
mockRequest.body = mockSentryData;
386+
mockRequest.headers = {}; // 시그니처 헤더 누락
387+
mockVerifySignature.mockReturnValue(false);
388+
389+
await webhookController.handleSentryWebhook(
390+
mockRequest as Request,
391+
mockResponse as Response,
392+
nextFunction
393+
);
394+
395+
expect(mockVerifySignature).toHaveBeenCalledWith(mockRequest);
396+
expect(mockSendSlackMessage).not.toHaveBeenCalled();
397+
expect(mockResponse.status).toHaveBeenCalledWith(400);
398+
});
399+
400+
it('시그니처 검증 중 예외 발생 시 에러를 전달해야 한다', async () => {
401+
mockRequest.body = mockSentryData;
402+
mockRequest.headers = {
403+
'sentry-hook-signature': 'some-signature'
404+
};
405+
const verificationError = new Error('SENTRY_CLIENT_SECRET is not defined');
406+
mockVerifySignature.mockImplementation(() => {
407+
throw verificationError;
408+
});
409+
410+
await webhookController.handleSentryWebhook(
411+
mockRequest as Request,
412+
mockResponse as Response,
413+
nextFunction
414+
);
415+
416+
expect(mockVerifySignature).toHaveBeenCalledWith(mockRequest);
417+
expect(nextFunction).toHaveBeenCalledWith(verificationError);
418+
expect(mockSendSlackMessage).not.toHaveBeenCalled();
419+
expect(mockResponse.json).not.toHaveBeenCalled();
420+
});
421+
});
311422
});
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { Request } from 'express';
2+
import { verifySignature } from '../verify.util';
3+
import crypto from 'crypto';
4+
5+
// 환경 변수 모킹
6+
const mockEnv = {
7+
SENTRY_CLIENT_SECRET: 'test-secret-key'
8+
};
9+
10+
describe('verifySignature', () => {
11+
let mockRequest: Partial<Request>;
12+
let originalEnv: NodeJS.ProcessEnv;
13+
14+
beforeAll(() => {
15+
originalEnv = process.env;
16+
});
17+
18+
beforeEach(() => {
19+
process.env = { ...originalEnv, ...mockEnv };
20+
mockRequest = {
21+
body: {},
22+
headers: {}
23+
};
24+
});
25+
26+
afterAll(() => {
27+
process.env = originalEnv;
28+
});
29+
30+
afterEach(() => {
31+
jest.clearAllMocks();
32+
});
33+
34+
describe('성공 케이스', () => {
35+
it('유효한 시그니처로 검증에 성공해야 한다', () => {
36+
const testBody = { action: 'created', data: { issue: { id: 'test' } } };
37+
const bodyString = JSON.stringify(testBody);
38+
const expectedSignature = crypto
39+
.createHmac('sha256', mockEnv.SENTRY_CLIENT_SECRET)
40+
.update(bodyString)
41+
.digest('hex');
42+
43+
mockRequest.body = testBody;
44+
mockRequest.headers = {
45+
'sentry-hook-signature': expectedSignature
46+
};
47+
48+
const result = verifySignature(mockRequest as Request);
49+
50+
expect(result).toBe(true);
51+
});
52+
53+
it('복잡한 객체 body로 검증에 성공해야 한다', () => {
54+
const testBody = {
55+
action: 'created',
56+
data: {
57+
issue: {
58+
id: 'complex-issue-123',
59+
title: 'Complex Error with Special Characters: áéíóú',
60+
culprit: 'components/[email protected]:42',
61+
status: 'unresolved',
62+
count: "15",
63+
userCount: 8,
64+
firstSeen: '2024-01-01T12:00:00.000Z'
65+
}
66+
}
67+
};
68+
69+
const bodyString = JSON.stringify(testBody);
70+
const expectedSignature = crypto
71+
.createHmac('sha256', mockEnv.SENTRY_CLIENT_SECRET)
72+
.update(bodyString)
73+
.digest('hex');
74+
75+
mockRequest.body = testBody;
76+
mockRequest.headers = {
77+
'sentry-hook-signature': expectedSignature
78+
};
79+
80+
const result = verifySignature(mockRequest as Request);
81+
82+
expect(result).toBe(true);
83+
});
84+
});
85+
86+
describe('실패 케이스', () => {
87+
it('잘못된 시그니처로 검증에 실패해야 한다', () => {
88+
const testBody = { action: 'created', data: { issue: { id: 'test' } } };
89+
90+
mockRequest.body = testBody;
91+
mockRequest.headers = {
92+
'sentry-hook-signature': 'invalid-signature'
93+
};
94+
95+
const result = verifySignature(mockRequest as Request);
96+
97+
expect(result).toBe(false);
98+
});
99+
100+
it('시그니처 헤더가 누락된 경우 false를 반환해야 한다', () => {
101+
const testBody = { action: 'created', data: { issue: { id: 'test' } } };
102+
103+
mockRequest.body = testBody;
104+
mockRequest.headers = {}; // 시그니처 헤더 누락
105+
106+
const result = verifySignature(mockRequest as Request);
107+
108+
expect(result).toBe(false);
109+
});
110+
111+
it('빈 시그니처로 검증에 실패해야 한다', () => {
112+
const testBody = { action: 'created', data: { issue: { id: 'test' } } };
113+
114+
mockRequest.body = testBody;
115+
mockRequest.headers = {
116+
'sentry-hook-signature': ''
117+
};
118+
119+
const result = verifySignature(mockRequest as Request);
120+
121+
expect(result).toBe(false);
122+
});
123+
124+
it('다른 secret으로 생성된 시그니처로 검증에 실패해야 한다', () => {
125+
const testBody = { action: 'created', data: { issue: { id: 'test' } } };
126+
const bodyString = JSON.stringify(testBody);
127+
const wrongSignature = crypto
128+
.createHmac('sha256', 'wrong-secret')
129+
.update(bodyString)
130+
.digest('hex');
131+
132+
mockRequest.body = testBody;
133+
mockRequest.headers = {
134+
'sentry-hook-signature': wrongSignature
135+
};
136+
137+
const result = verifySignature(mockRequest as Request);
138+
139+
expect(result).toBe(false);
140+
});
141+
});
142+
143+
describe('에러 케이스', () => {
144+
it('SENTRY_CLIENT_SECRET이 없는 경우 에러를 발생시켜야 한다', () => {
145+
delete process.env.SENTRY_CLIENT_SECRET;
146+
147+
const testBody = { action: 'created', data: { issue: { id: 'test' } } };
148+
mockRequest.body = testBody;
149+
mockRequest.headers = {
150+
'sentry-hook-signature': 'some-signature'
151+
};
152+
153+
expect(() => {
154+
verifySignature(mockRequest as Request);
155+
}).toThrow('SENTRY_CLIENT_SECRET is not defined');
156+
});
157+
});
158+
});

0 commit comments

Comments
 (0)