@@ -571,6 +571,125 @@ describe('HTTPReceiver', () => {
571571 } ) ;
572572 } ) ;
573573
574+ describe ( 'invalidRequestSignatureHandler' , ( ) => {
575+ it ( 'should call the custom handler when signature verification fails' , async ( ) => {
576+ const spy = sinon . spy ( ) ;
577+ const fakeParseAndVerify = sinon . fake . rejects ( new Error ( 'Signature mismatch' ) ) ;
578+ const fakeBuildNoBodyResponse = sinon . fake ( ) ;
579+
580+ const overridesWithFakeVerify = mergeOverrides ( overrides , {
581+ './HTTPModuleFunctions' : {
582+ parseAndVerifyHTTPRequest : fakeParseAndVerify ,
583+ parseHTTPRequestBody : sinon . fake ( ) ,
584+ buildNoBodyResponse : fakeBuildNoBodyResponse ,
585+ '@noCallThru' : true ,
586+ } ,
587+ } ) ;
588+
589+ const HTTPReceiver = importHTTPReceiver ( overridesWithFakeVerify ) ;
590+ const receiver = new HTTPReceiver ( {
591+ signingSecret : 'secret' ,
592+ logger : noopLogger ,
593+ invalidRequestSignatureHandler : spy ,
594+ } ) ;
595+ assert . isNotNull ( receiver ) ;
596+
597+ const fakeReq = sinon . createStubInstance ( IncomingMessage ) as unknown as IncomingMessage ;
598+ fakeReq . url = '/slack/events' ;
599+ fakeReq . method = 'POST' ;
600+ fakeReq . headers = {
601+ 'x-slack-signature' : 'v0=bad' ,
602+ 'x-slack-request-timestamp' : '1234567890' ,
603+ } ;
604+ ( fakeReq as IncomingMessage & { rawBody ?: string } ) . rawBody = '{"token":"test"}' ;
605+
606+ const fakeRes = sinon . createStubInstance ( ServerResponse ) as unknown as ServerResponse ;
607+
608+ receiver . requestListener ( fakeReq , fakeRes ) ;
609+
610+ // Wait for the async closure inside handleIncomingEvent to settle
611+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
612+
613+ assert ( spy . calledOnce , 'invalidRequestSignatureHandler should be called once' ) ;
614+ const args = spy . firstCall . args [ 0 ] ;
615+ assert . equal ( args . rawBody , '{"token":"test"}' ) ;
616+ assert . equal ( args . signature , 'v0=bad' ) ;
617+ assert . equal ( args . ts , 1234567890 ) ;
618+ } ) ;
619+
620+ it ( 'should use the default noop handler when no custom handler is provided' , async ( ) => {
621+ const fakeParseAndVerify = sinon . fake . rejects ( new Error ( 'Signature mismatch' ) ) ;
622+ const fakeBuildNoBodyResponse = sinon . fake ( ) ;
623+
624+ const overridesWithFakeVerify = mergeOverrides ( overrides , {
625+ './HTTPModuleFunctions' : {
626+ parseAndVerifyHTTPRequest : fakeParseAndVerify ,
627+ parseHTTPRequestBody : sinon . fake ( ) ,
628+ buildNoBodyResponse : fakeBuildNoBodyResponse ,
629+ '@noCallThru' : true ,
630+ } ,
631+ } ) ;
632+
633+ const HTTPReceiver = importHTTPReceiver ( overridesWithFakeVerify ) ;
634+ const receiver = new HTTPReceiver ( {
635+ signingSecret : 'secret' ,
636+ logger : noopLogger ,
637+ } ) ;
638+
639+ const fakeReq = sinon . createStubInstance ( IncomingMessage ) as unknown as IncomingMessage ;
640+ fakeReq . url = '/slack/events' ;
641+ fakeReq . method = 'POST' ;
642+ fakeReq . headers = { } ;
643+
644+ const fakeRes = sinon . createStubInstance ( ServerResponse ) as unknown as ServerResponse ;
645+
646+ // Should not throw even without a custom handler
647+ receiver . requestListener ( fakeReq , fakeRes ) ;
648+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
649+
650+ sinon . assert . calledOnce ( fakeBuildNoBodyResponse ) ;
651+ sinon . assert . calledWith ( fakeBuildNoBodyResponse , fakeRes , 401 ) ;
652+ } ) ;
653+
654+ it ( 'should pass undefined for signature and ts when headers are missing' , async ( ) => {
655+ const spy = sinon . spy ( ) ;
656+ const fakeParseAndVerify = sinon . fake . rejects ( new Error ( 'Signature mismatch' ) ) ;
657+ const fakeBuildNoBodyResponse = sinon . fake ( ) ;
658+
659+ const overridesWithFakeVerify = mergeOverrides ( overrides , {
660+ './HTTPModuleFunctions' : {
661+ parseAndVerifyHTTPRequest : fakeParseAndVerify ,
662+ parseHTTPRequestBody : sinon . fake ( ) ,
663+ buildNoBodyResponse : fakeBuildNoBodyResponse ,
664+ '@noCallThru' : true ,
665+ } ,
666+ } ) ;
667+
668+ const HTTPReceiver = importHTTPReceiver ( overridesWithFakeVerify ) ;
669+ const receiver = new HTTPReceiver ( {
670+ signingSecret : 'secret' ,
671+ logger : noopLogger ,
672+ invalidRequestSignatureHandler : spy ,
673+ } ) ;
674+
675+ const fakeReq = sinon . createStubInstance ( IncomingMessage ) as unknown as IncomingMessage ;
676+ fakeReq . url = '/slack/events' ;
677+ fakeReq . method = 'POST' ;
678+ fakeReq . headers = { } ;
679+
680+ const fakeRes = sinon . createStubInstance ( ServerResponse ) as unknown as ServerResponse ;
681+
682+ receiver . requestListener ( fakeReq , fakeRes ) ;
683+ await new Promise ( ( resolve ) => setTimeout ( resolve , 50 ) ) ;
684+
685+ assert ( spy . calledOnce ) ;
686+ const args = spy . firstCall . args [ 0 ] ;
687+ assert . equal ( args . rawBody , '' ) ;
688+ assert . isUndefined ( args . signature ) ;
689+ assert . isUndefined ( args . ts ) ;
690+ } ) ;
691+ } ) ;
692+
574693 it ( "should throw if request doesn't match install path, redirect URI path, or custom routes" , async ( ) => {
575694 const installProviderStub = sinon . createStubInstance ( InstallProvider ) ;
576695 const HTTPReceiver = importHTTPReceiver ( overrides ) ;
0 commit comments