@@ -2,9 +2,11 @@ import 'reflect-metadata';
22import { Request , Response } from 'express' ;
33import { WebhookController } from '@/controllers/webhook.controller' ;
44import { sendSlackMessage } from '@/modules/slack/slack.notifier' ;
5+ import { verifySignature } from '@/utils/verify.util' ;
56
67// Mock dependencies
78jest . mock ( '@/modules/slack/slack.notifier' ) ;
9+ jest . mock ( '@/utils/verify.util' ) ;
810
911// logger 모킹
1012jest . 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} ) ;
0 commit comments