@@ -2,9 +2,11 @@ import 'reflect-metadata';
2
2
import { Request , Response } from 'express' ;
3
3
import { WebhookController } from '@/controllers/webhook.controller' ;
4
4
import { sendSlackMessage } from '@/modules/slack/slack.notifier' ;
5
+ import { verifySignature } from '@/utils/verify.util' ;
5
6
6
7
// Mock dependencies
7
8
jest . mock ( '@/modules/slack/slack.notifier' ) ;
9
+ jest . mock ( '@/utils/verify.util' ) ;
8
10
9
11
// logger 모킹
10
12
jest . mock ( '@/configs/logger.config' , ( ) => ( {
@@ -18,6 +20,7 @@ describe('WebhookController', () => {
18
20
let mockResponse : Partial < Response > ;
19
21
let nextFunction : jest . Mock ;
20
22
let mockSendSlackMessage : jest . MockedFunction < typeof sendSlackMessage > ;
23
+ let mockVerifySignature : jest . MockedFunction < typeof verifySignature > ;
21
24
22
25
beforeEach ( ( ) => {
23
26
// WebhookController 인스턴스 생성
@@ -36,6 +39,10 @@ describe('WebhookController', () => {
36
39
37
40
nextFunction = jest . fn ( ) ;
38
41
mockSendSlackMessage = sendSlackMessage as jest . MockedFunction < typeof sendSlackMessage > ;
42
+ mockVerifySignature = verifySignature as jest . MockedFunction < typeof verifySignature > ;
43
+
44
+ // 기본적으로 시그니처 검증이 성공하도록 설정
45
+ mockVerifySignature . mockReturnValue ( true ) ;
39
46
} ) ;
40
47
41
48
afterEach ( ( ) => {
@@ -308,4 +315,108 @@ describe('WebhookController', () => {
308
315
expect ( mockSendSlackMessage ) . toHaveBeenCalledWith ( expectedMessage ) ;
309
316
} ) ;
310
317
} ) ;
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
+ } ) ;
311
422
} ) ;
0 commit comments