diff --git a/src/sentry/integrations/github/integration.py b/src/sentry/integrations/github/integration.py index 3a243bc05d4952..1bf90a7d286882 100644 --- a/src/sentry/integrations/github/integration.py +++ b/src/sentry/integrations/github/integration.py @@ -451,6 +451,8 @@ def sync_assignee_outbound( # Strip the @ from the username github_username = external_actor.external_name.lstrip("@") + # lowercase the username + github_username = github_username.lower() # Only update GitHub if we have a username to assign or if we're explicitly deassigning if github_username or not assign: diff --git a/src/sentry/integrations/utils/sync.py b/src/sentry/integrations/utils/sync.py index c4163386aa0505..4ff93355efff0e 100644 --- a/src/sentry/integrations/utils/sync.py +++ b/src/sentry/integrations/utils/sync.py @@ -139,7 +139,7 @@ def sync_group_assignee_inbound_by_external_actor( external_actors = ExternalActor.objects.filter( provider=EXTERNAL_PROVIDERS_REVERSE[ExternalProviderEnum(integration.provider)].value, - external_name=external_user_name, + external_name__iexact=external_user_name, integration_id=integration.id, user_id__isnull=False, ).values_list("user_id", flat=True) diff --git a/tests/sentry/integrations/github/test_integration.py b/tests/sentry/integrations/github/test_integration.py index fd98df4dd96789..dcab6a9889e792 100644 --- a/tests/sentry/integrations/github/test_integration.py +++ b/tests/sentry/integrations/github/test_integration.py @@ -1791,6 +1791,31 @@ def test_sync_assignee_outbound(self) -> None: assert request.url == "https://api.github.com/repos/Test-Organization/foo/issues/123" assert orjson.loads(request.body) == {"assignees": ["octocat"]} + @responses.activate + def test_sync_assignee_outbound_case_insensitive(self) -> None: + """Test assigning a GitHub issue to a user with linked GitHub account""" + + user, installation, external_issue, _, _ = self._setup_assignee_sync_test( + external_name="@JohnDoe" + ) + + responses.add( + responses.PATCH, + "https://api.github.com/repos/Test-Organization/foo/issues/123", + json={"assignees": ["johndoe"]}, + status=200, + ) + + responses.calls.reset() + + with assume_test_silo_mode(SiloMode.REGION): + installation.sync_assignee_outbound(external_issue, user, assign=True) + + assert len(responses.calls) == 1 + request = responses.calls[0].request + assert request.url == "https://api.github.com/repos/Test-Organization/foo/issues/123" + assert orjson.loads(request.body) == {"assignees": ["johndoe"]} + @responses.activate def test_sync_assignee_outbound_unassign(self) -> None: """Test unassigning a GitHub issue""" diff --git a/tests/sentry/integrations/utils/test_sync.py b/tests/sentry/integrations/utils/test_sync.py index 843329e1005cba..686993c8b2103b 100644 --- a/tests/sentry/integrations/utils/test_sync.py +++ b/tests/sentry/integrations/utils/test_sync.py @@ -345,6 +345,41 @@ def test_assignment_with_external_actor( assert updated_assignee.email == "test@example.com" mock_record_event.assert_called_with(EventLifecycleOutcome.SUCCESS, None, False, None) + @mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") + def test_assignment_with_external_actor_case_insensitive( + self, + mock_record_event: mock.MagicMock, + ) -> None: + """Test assigning a group to a user via external actor.""" + assert self.group.get_assignee() is None + + external_issue = self.create_integration_external_issue( + group=self.group, + key="JIRA-123", + integration=self.example_integration, + ) + + # Create external user mapping + self.create_external_user( + user=self.test_user, + external_name="@JohnDoe", + provider=ExternalProviders.GITHUB.value, + integration=self.example_integration, + ) + + sync_group_assignee_inbound_by_external_actor( + integration=self.example_integration, + external_user_name="@johndoe", + external_issue_key=external_issue.key, + assign=True, + ) + + updated_assignee = self.group.get_assignee() + assert updated_assignee is not None + assert updated_assignee.id == self.test_user.id + assert updated_assignee.email == "test@example.com" + mock_record_event.assert_called_with(EventLifecycleOutcome.SUCCESS, None, False, None) + @mock.patch("sentry.integrations.utils.sync.where_should_sync") @mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") def test_assign_with_multiple_groups(