diff --git a/src/sentry/integrations/base.py b/src/sentry/integrations/base.py index 5f9ebe1d376c70..ba6c2aa1ea2758 100644 --- a/src/sentry/integrations/base.py +++ b/src/sentry/integrations/base.py @@ -527,7 +527,7 @@ def message_from_error(self, exc: Exception) -> str: elif isinstance(exc, UnsupportedResponseType): return ERR_UNSUPPORTED_RESPONSE_TYPE.format(content_type=exc.content_type) elif isinstance(exc, ApiError): - if exc.json: + if exc.json and isinstance(exc.json, dict): msg = self.error_message_from_json(exc.json) or "unknown error" else: msg = "unknown error" diff --git a/src/sentry/integrations/gitlab/integration.py b/src/sentry/integrations/gitlab/integration.py index ab6608d764bd77..2ca39e53ffd865 100644 --- a/src/sentry/integrations/gitlab/integration.py +++ b/src/sentry/integrations/gitlab/integration.py @@ -186,6 +186,14 @@ def get_repositories( # Note: gitlab projects are the same things as repos everywhere else group = self.get_group_id() resp = self.get_client().search_projects(group, query) + # GitLab returns {"status": "error", ...} when the group is + # inaccessible. The pagination layer turns that dict into a list + # of its keys (e.g. ["status", "error"]), which would crash the + # list comprehension below with TypeError on str["id"]. + if resp and not isinstance(resp[0], dict): + raise IntegrationError( + "Expected list of projects from GitLab, got unexpected response" + ) instance = self.model.metadata["instance"] return [ { diff --git a/tests/sentry/integrations/source_code_management/test_sync_repos.py b/tests/sentry/integrations/source_code_management/test_sync_repos.py index 8f5c3498ce60da..57cda27f154b89 100644 --- a/tests/sentry/integrations/source_code_management/test_sync_repos.py +++ b/tests/sentry/integrations/source_code_management/test_sync_repos.py @@ -10,6 +10,7 @@ from sentry.constants import ObjectStatus from sentry.integrations.github.integration import GitHubIntegrationProvider from sentry.integrations.github_enterprise.integration import GitHubEnterpriseIntegrationProvider +from sentry.integrations.gitlab.integration import GitlabIntegration from sentry.integrations.models.organization_integration import OrganizationIntegration from sentry.integrations.source_code_management.sync_repos import ( sync_repos_for_org, @@ -572,6 +573,40 @@ def test_creates_new_repos_for_gitlab(self) -> None: assert len(repos) == 2 assert repos[0].provider == "integrations:gitlab" + @responses.activate + def test_error_dict_response_raises_integration_error(self) -> None: + integration = self.create_provider_integration( + provider="gitlab", + name="Example Gitlab 2", + external_id="example2.gitlab.com:group-y", + metadata={ + "instance": "example.gitlab.com", + "base_url": "https://example.gitlab.com", + "domain_name": "example.gitlab.com/group-y", + "verify_ssl": False, + "group_id": 1, + "webhook_secret": "secret123", + }, + ) + identity = Identity.objects.create( + idp=self.create_identity_provider(type="gitlab", config={}), + user=self.user, + external_id="gitlab456", + data={"access_token": "123456789"}, + ) + integration.add_organization(self.organization, self.user, identity.id) + installation = integration.get_installation(organization_id=self.organization.id) + + responses.add( + responses.GET, + "https://example.gitlab.com/api/v4/groups/1/projects?search=&simple=True&include_subgroups=False&page=1&per_page=100&order_by=last_activity_at", + json={"status": "error", "error": "group not accessible"}, + ) + + assert isinstance(installation, GitlabIntegration) + with pytest.raises(IntegrationError, match="Expected list of projects"): + installation.get_repositories() + @control_silo_test class SyncReposForOrgBitbucketTestCase(TestCase): @@ -762,6 +797,21 @@ def _wrap_identity_not_valid(*args: object, **kwargs: object) -> None: with assume_test_silo_mode(SiloMode.CELL): assert Repository.objects.count() == 0 + def test_vsts_message_from_error_handles_string_json(self) -> None: + integration = self.create_provider_integration( + provider="vsts", + external_id="vsts-account-id-2", + name="MyVSTSAccount", + metadata={"domain_name": "https://myvstsaccount.visualstudio.com/"}, + ) + integration.add_organization(self.organization, self.user) + installation = integration.get_installation(organization_id=self.organization.id) + + exc = ApiForbiddenError("ADO OAuth tokens disabled") + exc.json = "ADO OAuth tokens disabled" # type: ignore[assignment] + msg = installation.message_from_error(exc) + assert "unknown error" in msg + @control_silo_test class SyncReposForOrgBrokenIdentityTestCase(TestCase):