123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- from __future__ import annotations
- from unittest.mock import MagicMock, patch
- import responses
- from django.test.utils import override_settings
- from rest_framework import status
- from rest_framework.exceptions import MethodNotAllowed
- from rest_framework.response import Response
- from fixtures.integrations.stub_service import StubService
- from sentry.integrations.jira.webhooks.base import JiraTokenError, JiraWebhookBase
- from sentry.integrations.mixins import IssueSyncMixin
- from sentry.integrations.utils import AtlassianConnectValidationError
- from sentry.services.hybrid_cloud.integration.serial import serialize_integration
- from sentry.services.hybrid_cloud.organization.serial import serialize_rpc_organization
- from sentry.shared_integrations.exceptions import ApiError
- from sentry.testutils.cases import APITestCase, TestCase
- from sentry.testutils.silo import region_silo_test
- TOKEN = "JWT anexampletoken"
- @region_silo_test
- class JiraIssueUpdatedWebhookTest(APITestCase):
- endpoint = "sentry-extensions-jira-issue-updated"
- method = "post"
- def setUp(self):
- super().setUp()
- integration, _ = self.create_provider_integration_for(
- organization=self.organization,
- user=self.user,
- provider="jira",
- name="Example Jira",
- metadata={
- "oauth_client_id": "oauth-client-id",
- "shared_secret": "a-super-secret-key-from-atlassian",
- "base_url": "https://example.atlassian.net",
- "domain_name": "example.atlassian.net",
- },
- )
- # Ensure this is region safe, and doesn't require the ORM integration model
- self.integration = serialize_integration(integration=integration)
- @patch("sentry.integrations.jira.utils.api.sync_group_assignee_inbound")
- def test_simple_assign(self, mock_sync_group_assignee_inbound):
- with patch(
- "sentry.integrations.jira.webhooks.issue_updated.get_integration_from_jwt",
- return_value=self.integration,
- ):
- data = StubService.get_stub_data("jira", "edit_issue_assignee_payload.json")
- self.get_success_response(**data, extra_headers=dict(HTTP_AUTHORIZATION=TOKEN))
- mock_sync_group_assignee_inbound.assert_called_with(
- self.integration, "jess@sentry.io", "APP-123", assign=True
- )
- @override_settings(JIRA_USE_EMAIL_SCOPE=True)
- @patch("sentry.integrations.jira.utils.api.sync_group_assignee_inbound")
- @responses.activate
- def test_assign_use_email_api(self, mock_sync_group_assignee_inbound):
- responses.add(
- responses.GET,
- "https://example.atlassian.net/rest/api/3/user/email",
- json={"accountId": "deadbeef123", "email": self.user.email},
- )
- with patch(
- "sentry.integrations.jira.webhooks.issue_updated.get_integration_from_jwt",
- return_value=self.integration,
- ):
- data = StubService.get_stub_data("jira", "edit_issue_assignee_payload.json")
- data["issue"]["fields"]["assignee"]["emailAddress"] = ""
- self.get_success_response(**data, extra_headers=dict(HTTP_AUTHORIZATION=TOKEN))
- assert mock_sync_group_assignee_inbound.called
- assert len(responses.calls) == 1
- @override_settings(JIRA_USE_EMAIL_SCOPE=True)
- @responses.activate
- def test_assign_use_email_api_error(self):
- responses.add(
- responses.GET,
- "https://example.atlassian.net/rest/api/3/user/email",
- status=500,
- )
- with patch(
- "sentry.integrations.jira.webhooks.issue_updated.get_integration_from_jwt",
- return_value=self.integration,
- ):
- data = StubService.get_stub_data("jira", "edit_issue_assignee_payload.json")
- data["issue"]["fields"]["assignee"]["emailAddress"] = ""
- response = self.get_success_response(
- **data, extra_headers=dict(HTTP_AUTHORIZATION=TOKEN)
- )
- assert "error_message" in response.data
- @patch("sentry.integrations.jira.utils.api.sync_group_assignee_inbound")
- def test_assign_missing_email(self, mock_sync_group_assignee_inbound):
- with patch(
- "sentry.integrations.jira.webhooks.issue_updated.get_integration_from_jwt",
- return_value=self.integration,
- ):
- data = StubService.get_stub_data("jira", "edit_issue_assignee_payload.json")
- data["issue"]["fields"]["assignee"]["emailAddress"] = ""
- self.get_success_response(**data, extra_headers=dict(HTTP_AUTHORIZATION=TOKEN))
- assert not mock_sync_group_assignee_inbound.called
- @patch("sentry.integrations.jira.utils.api.sync_group_assignee_inbound")
- def test_simple_deassign(self, mock_sync_group_assignee_inbound):
- with patch(
- "sentry.integrations.jira.webhooks.issue_updated.get_integration_from_jwt",
- return_value=self.integration,
- ):
- data = StubService.get_stub_data("jira", "edit_issue_no_assignee_payload.json")
- self.get_success_response(**data, extra_headers=dict(HTTP_AUTHORIZATION=TOKEN))
- mock_sync_group_assignee_inbound.assert_called_with(
- self.integration, None, "APP-123", assign=False
- )
- @patch("sentry.integrations.jira.utils.api.sync_group_assignee_inbound")
- def test_simple_deassign_assignee_missing(self, mock_sync_group_assignee_inbound):
- with patch(
- "sentry.integrations.jira.webhooks.issue_updated.get_integration_from_jwt",
- return_value=self.integration,
- ):
- data = StubService.get_stub_data("jira", "edit_issue_assignee_missing_payload.json")
- self.get_success_response(**data, extra_headers=dict(HTTP_AUTHORIZATION=TOKEN))
- mock_sync_group_assignee_inbound.assert_called_with(
- self.integration, None, "APP-123", assign=False
- )
- @patch.object(IssueSyncMixin, "sync_status_inbound")
- def test_simple_status_sync_inbound(self, mock_sync_status_inbound):
- with patch(
- "sentry.integrations.jira.webhooks.issue_updated.get_integration_from_jwt",
- return_value=self.integration,
- ) as mock_get_integration_from_jwt:
- data = StubService.get_stub_data("jira", "edit_issue_status_payload.json")
- self.get_success_response(**data, extra_headers=dict(HTTP_AUTHORIZATION=TOKEN))
- mock_get_integration_from_jwt.assert_called_with(
- token="anexampletoken",
- path="/extensions/jira/issue-updated/",
- provider="jira",
- query_params={},
- method="POST",
- )
- mock_sync_status_inbound.assert_called_with(
- "APP-123",
- {
- "changelog": {
- "from": "10101",
- "field": "status",
- "fromString": "Done",
- "to": "3",
- "toString": "In Progress",
- "fieldtype": "jira",
- "fieldId": "status",
- },
- "issue": {
- "fields": {"project": {"id": "10000", "key": "APP"}},
- "key": "APP-123",
- },
- },
- )
- @patch("sentry_sdk.set_tag")
- @patch("sentry.integrations.utils.scope.bind_organization_context")
- def test_adds_context_data(self, mock_bind_org_context: MagicMock, mock_set_tag: MagicMock):
- with patch(
- "sentry.integrations.jira.webhooks.issue_updated.get_integration_from_jwt",
- return_value=self.integration,
- ):
- data = StubService.get_stub_data("jira", "edit_issue_assignee_payload.json")
- self.get_success_response(**data, extra_headers=dict(HTTP_AUTHORIZATION=TOKEN))
- mock_set_tag.assert_called_with("integration_id", self.integration.id)
- mock_bind_org_context.assert_called_with(serialize_rpc_organization(self.organization))
- def test_missing_changelog(self):
- with patch(
- "sentry.integrations.jira.webhooks.issue_updated.get_integration_from_jwt",
- return_value=self.integration,
- ):
- data = StubService.get_stub_data("jira", "changelog_missing.json")
- self.get_success_response(**data, extra_headers=dict(HTTP_AUTHORIZATION=TOKEN))
- class MockErroringJiraEndpoint(JiraWebhookBase):
- permission_classes = ()
- dummy_exception = Exception("whoops")
- # In order to be able to use `as_view`'s `initkwargs` (in other words, in order to be able to
- # pass kwargs to `as_view` and have `as_view` pass them onto the `__init__` method below), any
- # kwarg we'd like to pass must already be an attibute of the class
- error = BaseException("unreachable")
- def __init__(self, error: Exception = dummy_exception, *args, **kwargs):
- # We allow the error to be passed in so that we have access to it in the test for use
- # in equality checks
- self.error = error
- super().__init__(*args, **kwargs)
- def get(self, request):
- raise self.error
- class JiraWebhookBaseTest(TestCase):
- @patch("sentry.utils.sdk.capture_exception")
- def test_bad_request_errors(self, mock_capture_exception: MagicMock):
- for error_type in [AtlassianConnectValidationError, JiraTokenError]:
- mock_endpoint = MockErroringJiraEndpoint.as_view(error=error_type())
- request = self.make_request(method="GET")
- response = mock_endpoint(request)
- assert response.status_code == status.HTTP_409_CONFLICT
- # This kind of error shouldn't be sent to Sentry
- assert mock_capture_exception.call_count == 0
- @patch("sentry.integrations.jira.webhooks.base.logger")
- @patch("sentry.utils.sdk.capture_exception")
- def test_atlassian_pen_testing_bot(
- self, mock_capture_exception: MagicMock, mock_logger: MagicMock
- ):
- mock_endpoint = MockErroringJiraEndpoint.as_view(error=MethodNotAllowed("GET"))
- request = self.make_request(method="GET")
- request.META[
- "HTTP_USER_AGENT"
- ] = "CSRT (github.com/atlassian-labs/connect-security-req-tester)"
- response = mock_endpoint(request)
- assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
- assert (
- mock_logger.info.call_args.args[0]
- == "Atlassian Connect Security Request Tester tried disallowed method"
- )
- # This kind of error shouldn't be sent to Sentry
- assert mock_capture_exception.call_count == 0
- @patch("sentry.api.base.Endpoint.handle_exception", return_value=Response())
- def test_APIError_host_and_path_added_as_tags(self, mock_super_handle_exception: MagicMock):
- handler_error = ApiError("", url="http://maiseycharlie.jira.com/rest/api/3/dogs/tricks")
- mock_endpoint = MockErroringJiraEndpoint.as_view(error=handler_error)
- request = self.make_request(method="GET")
- mock_endpoint(request)
- # signature is super().handle_exception(request, error, handler_context, scope)
- assert (
- mock_super_handle_exception.call_args.args[3]._tags["jira.host"]
- == "maiseycharlie.jira.com"
- )
- assert (
- mock_super_handle_exception.call_args.args[3]._tags["jira.endpoint"]
- == "/rest/api/3/dogs/tricks"
- )
- @patch("sentry.api.base.Endpoint.handle_exception", return_value=Response())
- def test_handles_xml_as_error_message(self, mock_super_handle_exception: MagicMock):
- """Moves the XML to `handler_context` and replaces it with a human-friendly message"""
- xml_string = '<?xml version="1.0"?><status><code>500</code><message>PSQLException: too many connections</message></status>'
- handler_error = ApiError(
- xml_string, url="http://maiseycharlie.jira.com/rest/api/3/dogs/tricks"
- )
- mock_endpoint = MockErroringJiraEndpoint.as_view(error=handler_error)
- request = self.make_request(method="GET")
- mock_endpoint(request)
- # signature is super().handle_exception(request, error, handler_context, scope)
- assert mock_super_handle_exception.call_args.args[1] == handler_error
- assert str(handler_error) == "Unknown error when requesting /rest/api/3/dogs/tricks"
- assert mock_super_handle_exception.call_args.args[2]["xml_response"] == xml_string
- @patch("sentry.api.base.Endpoint.handle_exception", return_value=Response())
- def test_handles_html_as_error_message(self, mock_super_handle_exception: MagicMock):
- """Moves the HTML to `handler_context` and replaces it with a human-friendly message"""
- html_strings = [
- # These aren't valid HTML (because they're cut off) but the `ApiError` constructor does
- # that, too, if the error text is long enough (though after more characters than this)
- '<!DOCTYPE html><html><head><title>Oops</title></head><body><div id="page"><div'
- '<html lang="en"><head><title>Oops</title></head><body><div id="page"><div'
- ]
- for html_string in html_strings:
- handler_error = ApiError(
- html_string, url="http://maiseycharlie.jira.com/rest/api/3/dogs/tricks"
- )
- mock_endpoint = MockErroringJiraEndpoint.as_view(error=handler_error)
- request = self.make_request(method="GET")
- mock_endpoint(request)
- # signature is super().handle_exception(request, error, handler_context, scope)
- assert mock_super_handle_exception.call_args.args[1] == handler_error
- assert str(handler_error) == "Unknown error when requesting /rest/api/3/dogs/tricks"
- assert mock_super_handle_exception.call_args.args[2]["html_response"] == html_string
- @patch("sentry.api.base.Endpoint.handle_exception", return_value=Response())
- def test_replacement_error_messages(self, mock_super_handle_exception: MagicMock):
- replacement_messages_by_code = {
- 429: "Rate limit hit when requesting /rest/api/3/dogs/tricks",
- 401: "Unauthorized request to /rest/api/3/dogs/tricks",
- 502: "Bad gateway when connecting to /rest/api/3/dogs/tricks",
- 504: "Gateway timeout when connecting to /rest/api/3/dogs/tricks",
- }
- for code, new_message in replacement_messages_by_code.items():
- handler_error = ApiError(
- "<!DOCTYPE html><html>Some HTML here</html>",
- url="http://maiseycharlie.jira.com/rest/api/3/dogs/tricks",
- code=code,
- )
- mock_endpoint = MockErroringJiraEndpoint.as_view(error=handler_error)
- request = self.make_request(method="GET")
- mock_endpoint(request)
- # signature is super().handle_exception(request, error, handler_context, scope)
- assert mock_super_handle_exception.call_args.args[1] == handler_error
- assert str(handler_error) == new_message
- @patch("sentry.integrations.jira.webhooks.base.logger")
- @patch("sentry.api.base.Endpoint.handle_exception", return_value=Response())
- def test_unexpected_jira_errors(
- self, mock_super_handle_exception: MagicMock, mock_logger: MagicMock
- ):
- unknown_errors = [
- (
- Exception(
- "not a known error",
- ),
- "not a known error",
- ),
- (
- ApiError(
- "<!DOCTYPE html><html>Some HTML here</html>",
- url="http://maiseycharlie.jira.com/rest/api/3/dogs/tricks",
- code=403,
- ),
- "Unknown error when requesting /rest/api/3/dogs/tricks",
- ),
- ]
- for unknown_error, expected_error_message in unknown_errors:
- mock_endpoint = MockErroringJiraEndpoint.as_view(error=unknown_error)
- request = self.make_request(method="GET")
- mock_endpoint(request)
- assert mock_super_handle_exception.call_args.args[1] == unknown_error
- assert str(unknown_error) == expected_error_message
- assert mock_logger.error.call_args.args[0] == "Unclear JIRA exception"
|