66
77import jwt
88import responses
9+ from jwt import DecodeError , ExpiredSignatureError , InvalidSignatureError
910from rest_framework import status
1011
1112from sentry .constants import ObjectStatus
1617 AtlassianConnectValidationError ,
1718 get_query_hash ,
1819)
19- from sentry .testutils .asserts import assert_count_of_metric , assert_halt_metric
20+ from sentry .testutils .asserts import (
21+ assert_count_of_metric ,
22+ assert_failure_metric ,
23+ assert_halt_metric ,
24+ )
2025from sentry .testutils .cases import APITestCase
2126from sentry .testutils .silo import control_silo_test
2227from sentry .utils .http import absolute_uri
@@ -49,9 +54,6 @@ def _jwt_token(
4954 headers = {** (headers or {}), "alg" : jira_signing_algorithm },
5055 )
5156
52- def jwt_token_secret (self ):
53- return self ._jwt_token ("HS256" , self .shared_secret )
54-
5557 def jwt_token_cdn (self ):
5658 return self ._jwt_token ("RS256" , RS256_KEY , headers = {"kid" : self .kid })
5759
@@ -110,18 +112,83 @@ def test_no_claims(self, mock_authenticate_asymmetric_jwt: MagicMock) -> None:
110112 status_code = status .HTTP_409_CONFLICT ,
111113 )
112114
115+ @patch (
116+ "sentry.integrations.jira.webhooks.installed.authenticate_asymmetric_jwt" ,
117+ side_effect = ExpiredSignatureError (),
118+ )
113119 @patch ("sentry.integrations.utils.metrics.EventLifecycle.record_event" )
114- @patch ("sentry_sdk.set_tag" )
115- def test_with_shared_secret (self , mock_set_tag : MagicMock , mock_record_event ) -> None :
116- self .get_success_response (
120+ @responses .activate
121+ def test_expired_signature (
122+ self , mock_record_event : MagicMock , mock_authenticate_asymmetric_jwt : MagicMock
123+ ) -> None :
124+ self .add_response ()
125+
126+ self .get_error_response (
117127 ** self .body (),
118- extra_headers = dict (HTTP_AUTHORIZATION = "JWT " + self .jwt_token_secret ()),
128+ extra_headers = dict (HTTP_AUTHORIZATION = "JWT " + self .jwt_token_cdn ()),
129+ status_code = status .HTTP_400_BAD_REQUEST ,
130+ )
131+ # SLO metric asserts
132+ # ENSURE_CONTROL_SILO (success) -> VERIFY_INSTALLATION (failure) -> GET_CONTROL_RESPONSE (success)
133+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .STARTED , 3 )
134+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .FAILURE , 1 )
135+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .SUCCESS , 2 )
136+ assert_failure_metric (
137+ mock_record_event ,
138+ ExpiredSignatureError (),
119139 )
120- integration = Integration .objects .get (provider = "jira" , external_id = self .external_id )
121140
122- mock_set_tag .assert_any_call ("integration_id" , integration .id )
123- assert integration .status == ObjectStatus .ACTIVE
124- mock_record_event .assert_called_with (EventLifecycleOutcome .SUCCESS , None , False , None )
141+ @patch (
142+ "sentry.integrations.jira.webhooks.installed.authenticate_asymmetric_jwt" ,
143+ side_effect = InvalidSignatureError (),
144+ )
145+ @patch ("sentry.integrations.utils.metrics.EventLifecycle.record_event" )
146+ @responses .activate
147+ def test_invalid_signature (
148+ self , mock_record_event : MagicMock , mock_authenticate_asymmetric_jwt : MagicMock
149+ ) -> None :
150+ self .add_response ()
151+
152+ self .get_error_response (
153+ ** self .body (),
154+ extra_headers = dict (HTTP_AUTHORIZATION = "JWT " + self .jwt_token_cdn ()),
155+ status_code = status .HTTP_400_BAD_REQUEST ,
156+ )
157+ # SLO metric asserts
158+ # ENSURE_CONTROL_SILO (success) -> VERIFY_INSTALLATION (halt) -> GET_CONTROL_RESPONSE (success)
159+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .STARTED , 3 )
160+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .HALTED , 1 )
161+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .SUCCESS , 2 )
162+ assert_halt_metric (
163+ mock_record_event ,
164+ "JWT contained invalid signature" ,
165+ )
166+
167+ @patch (
168+ "sentry.integrations.jira.webhooks.installed.authenticate_asymmetric_jwt" ,
169+ side_effect = DecodeError (),
170+ )
171+ @patch ("sentry.integrations.utils.metrics.EventLifecycle.record_event" )
172+ @responses .activate
173+ def test_decode_error (
174+ self , mock_record_event : MagicMock , mock_authenticate_asymmetric_jwt : MagicMock
175+ ) -> None :
176+ self .add_response ()
177+
178+ self .get_error_response (
179+ ** self .body (),
180+ extra_headers = dict (HTTP_AUTHORIZATION = "JWT " + self .jwt_token_cdn ()),
181+ status_code = status .HTTP_400_BAD_REQUEST ,
182+ )
183+ # SLO metric asserts
184+ # ENSURE_CONTROL_SILO (success) -> VERIFY_INSTALLATION (halt) -> GET_CONTROL_RESPONSE (success)
185+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .STARTED , 3 )
186+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .HALTED , 1 )
187+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .SUCCESS , 2 )
188+ assert_halt_metric (
189+ mock_record_event ,
190+ "Could not decode JWT token" ,
191+ )
125192
126193 @patch ("sentry_sdk.set_tag" )
127194 @responses .activate
@@ -138,7 +205,34 @@ def test_with_key_id(self, mock_set_tag: MagicMock) -> None:
138205 assert integration .status == ObjectStatus .ACTIVE
139206
140207 @patch ("sentry.integrations.utils.metrics.EventLifecycle.record_event" )
208+ def test_without_key_id (self , mock_record_event : MagicMock ) -> None :
209+ self .get_error_response (
210+ ** self .body (),
211+ extra_headers = dict (
212+ HTTP_AUTHORIZATION = "JWT " + self ._jwt_token ("RS256" , RS256_KEY , headers = {})
213+ ),
214+ status_code = status .HTTP_400_BAD_REQUEST ,
215+ )
216+ # SLO metric asserts
217+ # ENSURE_CONTROL_SILO (success) -> VERIFY_INSTALLATION (halt) -> GET_CONTROL_RESPONSE (success)
218+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .STARTED , 3 )
219+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .HALTED , 1 )
220+ assert_count_of_metric (mock_record_event , EventLifecycleOutcome .SUCCESS , 2 )
221+ assert_halt_metric (
222+ mock_record_event ,
223+ "Missing key_id (kid)" ,
224+ )
225+
226+ @patch ("sentry.integrations.utils.metrics.EventLifecycle.record_event" )
227+ @responses .activate
141228 def test_with_invalid_key_id (self , mock_record_event : MagicMock ) -> None :
229+ responses .add (
230+ responses .GET ,
231+ "https://connect-install-keys.atlassian.com/fake-kid" ,
232+ body = b"Not Found" ,
233+ status = 404 ,
234+ )
235+
142236 self .get_error_response (
143237 ** self .body (),
144238 extra_headers = dict (
0 commit comments