From 14e25e0e327a3babcf589f9932e75cd744aa3eeb Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Thu, 25 Jun 2026 16:17:05 -0400 Subject: [PATCH 1/2] feat(scm-multi-platform-detection): Conditional use of new multi detector --- .../organization_repository_platforms.py | 34 +++-- .../test_organization_repository_platforms.py | 129 +++++++++++++++++- 2 files changed, 153 insertions(+), 10 deletions(-) diff --git a/src/sentry/integrations/api/endpoints/organization_repository_platforms.py b/src/sentry/integrations/api/endpoints/organization_repository_platforms.py index 96fc325208ed..82aeb0022992 100644 --- a/src/sentry/integrations/api/endpoints/organization_repository_platforms.py +++ b/src/sentry/integrations/api/endpoints/organization_repository_platforms.py @@ -1,7 +1,6 @@ from __future__ import annotations -import logging - +import sentry_sdk from rest_framework.request import Request from rest_framework.response import Response @@ -11,14 +10,22 @@ from sentry.api.base import cell_silo_endpoint from sentry.integrations.api.bases.organization_repository import OrganizationRepositoryEndpoint from sentry.integrations.github.client import GitHubApiClient +from sentry.integrations.github.multi_platform_detection import detect_platforms_multi from sentry.integrations.github.platform_detection import detect_platforms from sentry.integrations.services.integration import integration_service from sentry.integrations.types import IntegrationProviderSlug from sentry.models.organization import Organization from sentry.models.repository import Repository -from sentry.shared_integrations.exceptions import ApiError +from sentry.shared_integrations.exceptions import ApiConflictError, ApiError + -logger = logging.getLogger(__name__) +def _capture_detection_exception(type: str, is_multi: bool, repo_id: int, repo_name: str) -> None: + with sentry_sdk.new_scope() as scope: + scope.set_tag("scm_platform_detection", type) + scope.set_tag("is_multi", is_multi) + scope.set_tag("repo_id", repo_id) + scope.set_tag("repo_name", repo_name) + sentry_sdk.capture_exception() @cell_silo_endpoint @@ -60,13 +67,22 @@ def get(self, request: Request, organization: Organization, repo: Repository) -> client = GitHubApiClient(integration=integration, org_integration_id=org_integration.id) + is_multi = features.has( + "organizations:integrations-github-multi-platform-detection", + organization, + actor=request.user, + ) try: - platforms = detect_platforms(client=client, repo=repo.name) + if is_multi: + platforms = detect_platforms_multi(client, repo.name)["platforms"] + else: + platforms = detect_platforms(client=client, repo=repo.name) + except ApiConflictError: + # Empty / unprocessable repo (e.g. empty git tree) — multi path only. + _capture_detection_exception("empty_repo", is_multi, repo.id, repo.name) + return Response({"platforms": []}) except (ApiError, ValueError): - logger.exception( - "integrations.github.platform_detection_failed", - extra={"repo_id": repo.id, "repo_name": repo.name}, - ) + _capture_detection_exception("failed", is_multi, repo.id, repo.name) return Response({"detail": "Failed to detect platforms from GitHub."}, status=502) return Response({"platforms": platforms}) diff --git a/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms.py b/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms.py index 8a5b7cbaed09..a2c0f2a79be3 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms.py @@ -11,6 +11,8 @@ from sentry.testutils.cases import APITestCase FEATURE_FLAG = "organizations:integrations-github-platform-detection" +MULTI_FLAG = "organizations:integrations-github-multi-platform-detection" +ENDPOINT_MODULE = "sentry.integrations.api.endpoints.organization_repository_platforms" class OrganizationRepositoryPlatformsGetTest(APITestCase): @@ -194,9 +196,12 @@ def test_other_orgs_repo_not_accessible(self) -> None: response = self.get_response(self.organization.slug, other_repo.id) assert response.status_code == 404 + @mock.patch(f"{ENDPOINT_MODULE}.sentry_sdk") @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") @responses.activate - def test_github_api_error_returns_502(self, get_jwt: mock.MagicMock) -> None: + def test_github_api_error_returns_502( + self, get_jwt: mock.MagicMock, mock_sentry_sdk: mock.MagicMock + ) -> None: responses.add( method=responses.GET, url="https://api.github.com/repos/Test-Organization/foo/languages", @@ -208,3 +213,125 @@ def test_github_api_error_returns_502(self, get_jwt: mock.MagicMock) -> None: response = self.get_response(self.organization.slug, self.repo.id) assert response.status_code == 502 assert "Failed to detect" in response.data["detail"] + assert mock_sentry_sdk.capture_exception.called + scope = mock_sentry_sdk.new_scope.return_value.__enter__.return_value + scope.set_tag.assert_any_call("is_multi", False) + scope.set_tag.assert_any_call("repo_id", self.repo.id) + scope.set_tag.assert_any_call("repo_name", self.repo.name) + + +class OrganizationRepositoryPlatformsMultiGetTest(APITestCase): + """Tests for the multi-platform detector path (both flags enabled).""" + + endpoint = "sentry-api-0-organization-repository-platforms" + + def setUp(self) -> None: + super().setUp() + self.login_as(user=self.user) + + ten_days = timezone.now() + timedelta(days=10) + self.integration = self.create_integration( + organization=self.organization, + provider="github", + name="Github Test Org", + external_id="1", + metadata={ + "access_token": "12345token", + "expires_at": ten_days.strftime("%Y-%m-%dT%H:%M:%S"), + }, + ) + self.repo = Repository.objects.create( + organization_id=self.organization.id, + name="Test-Organization/foo", + url="https://github.com/Test-Organization/foo", + provider="integrations:github", + external_id="123", + integration_id=self.integration.id, + ) + + @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") + @responses.activate + def test_multi_detects_framework_and_language(self, get_jwt: mock.MagicMock) -> None: + # manage.py is a pure existence rule for python-django — no content read needed. + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/languages", + json={"Python": 50000}, + status=200, + ) + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/git/trees/HEAD", + json={ + "tree": [{"path": "manage.py", "type": "blob", "size": 100}], + "truncated": False, + }, + status=200, + ) + + with self.feature({FEATURE_FLAG: True, MULTI_FLAG: True}): + response = self.get_success_response( + self.organization.slug, self.repo.id, status_code=200 + ) + + platforms = {p["platform"]: p for p in response.data["platforms"]} + assert "python-django" in platforms + assert platforms["python-django"]["confidence"] == "high" + assert "python" in platforms + assert platforms["python"]["confidence"] == "medium" + + @mock.patch(f"{ENDPOINT_MODULE}.sentry_sdk") + @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") + @responses.activate + def test_multi_empty_repo_returns_empty_list( + self, get_jwt: mock.MagicMock, mock_sentry_sdk: mock.MagicMock + ) -> None: + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/languages", + json={"Python": 50000}, + status=200, + ) + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/git/trees/HEAD", + json={"message": "Git Repository is empty."}, + status=409, + ) + + with self.feature({FEATURE_FLAG: True, MULTI_FLAG: True}): + response = self.get_success_response( + self.organization.slug, self.repo.id, status_code=200 + ) + + assert response.data == {"platforms": []} + assert mock_sentry_sdk.capture_exception.called + scope = mock_sentry_sdk.new_scope.return_value.__enter__.return_value + scope.set_tag.assert_any_call("scm_platform_detection", "empty_repo") + scope.set_tag.assert_any_call("is_multi", True) + scope.set_tag.assert_any_call("repo_id", self.repo.id) + scope.set_tag.assert_any_call("repo_name", self.repo.name) + + @mock.patch(f"{ENDPOINT_MODULE}.sentry_sdk") + @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") + @responses.activate + def test_multi_github_api_error_returns_502( + self, get_jwt: mock.MagicMock, mock_sentry_sdk: mock.MagicMock + ) -> None: + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/languages", + json={"message": "Server Error"}, + status=500, + ) + + with self.feature({FEATURE_FLAG: True, MULTI_FLAG: True}): + response = self.get_response(self.organization.slug, self.repo.id) + + assert response.status_code == 502 + assert "Failed to detect" in response.data["detail"] + assert mock_sentry_sdk.capture_exception.called + scope = mock_sentry_sdk.new_scope.return_value.__enter__.return_value + scope.set_tag.assert_any_call("is_multi", True) + scope.set_tag.assert_any_call("repo_id", self.repo.id) + scope.set_tag.assert_any_call("repo_name", self.repo.name) From d2f644763e2cf006cbf2e6cbade823255e97a6c0 Mon Sep 17 00:00:00 2001 From: Abdullah Khan Date: Sat, 27 Jun 2026 17:50:33 -0400 Subject: [PATCH 2/2] feat(scm-onboarding-multi-platform-detection): Removing test endpoint and porting over tests --- src/sentry/api/urls.py | 8 - .../organization_repository_platforms_test.py | 87 ------ .../utils/api/knownSentryApiUrls.generated.ts | 1 - .../test_organization_repository_platforms.py | 103 +++++++ ..._organization_repository_platforms_test.py | 255 ------------------ 5 files changed, 103 insertions(+), 351 deletions(-) delete mode 100644 src/sentry/integrations/api/endpoints/organization_repository_platforms_test.py delete mode 100644 tests/sentry/integrations/api/endpoints/test_organization_repository_platforms_test.py diff --git a/src/sentry/api/urls.py b/src/sentry/api/urls.py index e2f782a5269e..509d0201e297 100644 --- a/src/sentry/api/urls.py +++ b/src/sentry/api/urls.py @@ -277,9 +277,6 @@ from sentry.integrations.api.endpoints.organization_repository_platforms import ( OrganizationRepositoryPlatformsEndpoint, ) -from sentry.integrations.api.endpoints.organization_repository_platforms_test import ( - OrganizationRepositoryPlatformsTestEndpoint, -) from sentry.integrations.api.endpoints.organization_repository_settings import ( OrganizationRepositorySettingsEndpoint, ) @@ -2199,11 +2196,6 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]: OrganizationRepositoryPlatformsEndpoint.as_view(), name="sentry-api-0-organization-repository-platforms", ), - re_path( - r"^(?P[^/]+)/repos/(?P[^/]+)/platforms-test/$", - OrganizationRepositoryPlatformsTestEndpoint.as_view(), - name="sentry-api-0-organization-repository-platforms-test", - ), re_path( r"^(?P[^/]+)/legacy-webhooks/$", OrganizationLegacyWebhooksEndpoint.as_view(), diff --git a/src/sentry/integrations/api/endpoints/organization_repository_platforms_test.py b/src/sentry/integrations/api/endpoints/organization_repository_platforms_test.py deleted file mode 100644 index ae3b3edd6d62..000000000000 --- a/src/sentry/integrations/api/endpoints/organization_repository_platforms_test.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -import sentry_sdk -from rest_framework.request import Request -from rest_framework.response import Response -from sentry_sdk import logger as sentry_logger - -from sentry import features -from sentry.api.api_owners import ApiOwner -from sentry.api.api_publish_status import ApiPublishStatus -from sentry.api.base import cell_silo_endpoint -from sentry.integrations.api.bases.organization_repository import ( - OrganizationRepositoryEndpoint, -) -from sentry.integrations.github.client import GitHubApiClient -from sentry.integrations.github.multi_platform_detection import detect_platforms_multi -from sentry.integrations.services.integration import integration_service -from sentry.integrations.types import IntegrationProviderSlug -from sentry.models.organization import Organization -from sentry.models.repository import Repository -from sentry.shared_integrations.exceptions import ApiConflictError, ApiError - - -@cell_silo_endpoint -class OrganizationRepositoryPlatformsTestEndpoint(OrganizationRepositoryEndpoint): - """Endpoint for the tree-based multi-platform detector. - - Will replace the existing `OrganizationRepositoryPlatformsEndpoint`. - """ - - owner = ApiOwner.INTEGRATION_PLATFORM - publish_status = { - "GET": ApiPublishStatus.PRIVATE, - } - - def get(self, request: Request, organization: Organization, repo: Repository) -> Response: - if not features.has( - "organizations:integrations-github-platform-detection", - organization, - actor=request.user, - ): - return Response(status=404) - - if ( - not repo.integration_id - or repo.provider != f"integrations:{IntegrationProviderSlug.GITHUB}" - ): - return Response( - {"detail": "Platform detection is only supported for GitHub repositories."}, - status=400, - ) - - integration = integration_service.get_integration(integration_id=repo.integration_id) - if integration is None: - return Response({"detail": "GitHub integration not found."}, status=400) - - org_integration = integration_service.get_organization_integration( - integration_id=repo.integration_id, organization_id=organization.id - ) - if org_integration is None: - return Response( - {"detail": "GitHub integration is not configured for this organization."}, - status=400, - ) - - client = GitHubApiClient(integration=integration, org_integration_id=org_integration.id) - - attributes = {"repo_id": repo.id, "repo_name": repo.name} - try: - result = detect_platforms_multi(client, repo.name) - except ApiConflictError: - # Empty / unprocessable repo (e.g. empty git tree). - sentry_logger.warning( - "github.platform_detection.multi.empty_repo", attributes=attributes - ) - with sentry_sdk.new_scope() as scope: - scope.set_tag("scm_platform_detection", "empty_repo") - sentry_sdk.capture_exception() - return Response({"platforms": []}) - except (ApiError, ValueError): - sentry_logger.error("github.platform_detection.multi.failed", attributes=attributes) - with sentry_sdk.new_scope() as scope: - scope.set_tag("scm_platform_detection", "failed") - sentry_sdk.capture_exception() - return Response({"detail": "Failed to detect platforms from GitHub."}, status=502) - - return Response({"platforms": result["platforms"]}) diff --git a/static/app/utils/api/knownSentryApiUrls.generated.ts b/static/app/utils/api/knownSentryApiUrls.generated.ts index 7e19fb81ce63..51a46db362b8 100644 --- a/static/app/utils/api/knownSentryApiUrls.generated.ts +++ b/static/app/utils/api/knownSentryApiUrls.generated.ts @@ -351,7 +351,6 @@ export type KnownSentryApiUrls = | '/organizations/$organizationIdOrSlug/repos/' | '/organizations/$organizationIdOrSlug/repos/$repoId/' | '/organizations/$organizationIdOrSlug/repos/$repoId/commits/' - | '/organizations/$organizationIdOrSlug/repos/$repoId/platforms-test/' | '/organizations/$organizationIdOrSlug/repos/$repoId/platforms/' | '/organizations/$organizationIdOrSlug/repos/settings/' | '/organizations/$organizationIdOrSlug/request-project-creation/' diff --git a/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms.py b/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms.py index a2c0f2a79be3..f705cf6ecbbc 100644 --- a/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms.py +++ b/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms.py @@ -335,3 +335,106 @@ def test_multi_github_api_error_returns_502( scope.set_tag.assert_any_call("is_multi", True) scope.set_tag.assert_any_call("repo_id", self.repo.id) scope.set_tag.assert_any_call("repo_name", self.repo.name) + + @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") + @responses.activate + def test_detects_multi_platforms(self, get_jwt: mock.MagicMock) -> None: + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/languages", + json={"Python": 50000}, + status=200, + ) + # Recursive git tree with no manifest files -> language only, no framework detection + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/git/trees/HEAD", + json={ + "sha": "abc", + "truncated": False, + "tree": [ + {"path": "src/app.py", "type": "blob", "size": 1234}, + {"path": "src", "type": "tree"}, + ], + }, + status=200, + ) + + with self.feature({FEATURE_FLAG: True, MULTI_FLAG: True}): + response = self.get_success_response( + self.organization.slug, self.repo.id, status_code=200 + ) + + assert response.data == { + "platforms": [ + { + "platform": "python", + "language": "Python", + "bytes": 50000, + "confidence": "medium", + "priority": 1, + }, + ] + } + + @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") + @responses.activate + def test_detects_multi_framework(self, get_jwt: mock.MagicMock) -> None: + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/languages", + json={"Python": 50000}, + status=200, + ) + # Recursive git tree containing requirements.txt so a content read fires + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/git/trees/HEAD", + json={ + "sha": "abc", + "truncated": False, + "tree": [ + {"path": "requirements.txt", "type": "blob", "size": 42}, + ], + }, + status=200, + ) + + requirements_content = b64encode(b"Django==4.2\ncelery>=5.0\n").decode() + responses.add( + method=responses.GET, + url="https://api.github.com/repos/Test-Organization/foo/contents/requirements.txt", + json={"content": requirements_content}, + status=200, + ) + + with self.feature({FEATURE_FLAG: True, MULTI_FLAG: True}): + response = self.get_success_response( + self.organization.slug, self.repo.id, status_code=200 + ) + + assert response.data == { + "platforms": [ + { + "platform": "python-django", + "language": "Python", + "bytes": 50000, + "confidence": "high", + "priority": 90, + }, + { + "platform": "python-celery", + "language": "Python", + "bytes": 50000, + "confidence": "high", + "priority": 40, + }, + { + "platform": "python", + "language": "Python", + "bytes": 50000, + "confidence": "medium", + "priority": 1, + }, + ] + } diff --git a/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms_test.py b/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms_test.py deleted file mode 100644 index fdfca9409ff5..000000000000 --- a/tests/sentry/integrations/api/endpoints/test_organization_repository_platforms_test.py +++ /dev/null @@ -1,255 +0,0 @@ -from __future__ import annotations - -from base64 import b64encode -from datetime import timedelta -from unittest import mock - -import responses -from django.utils import timezone - -from sentry.models.repository import Repository -from sentry.testutils.cases import APITestCase - -FEATURE_FLAG = "organizations:integrations-github-platform-detection" -ENDPOINT_MODULE = "sentry.integrations.api.endpoints.organization_repository_platforms_test" - - -class OrganizationRepositoryPlatformsTestGetTest(APITestCase): - endpoint = "sentry-api-0-organization-repository-platforms-test" - - def setUp(self) -> None: - super().setUp() - self.login_as(user=self.user) - - ten_days = timezone.now() + timedelta(days=10) - self.integration = self.create_integration( - organization=self.organization, - provider="github", - name="Github Test Org", - external_id="1", - metadata={ - "access_token": "12345token", - "expires_at": ten_days.strftime("%Y-%m-%dT%H:%M:%S"), - }, - ) - self.repo = Repository.objects.create( - organization_id=self.organization.id, - name="Test-Organization/foo", - url="https://github.com/Test-Organization/foo", - provider="integrations:github", - external_id="123", - integration_id=self.integration.id, - ) - - def test_feature_flag_required(self) -> None: - response = self.get_response(self.organization.slug, self.repo.id) - assert response.status_code == 404 - - def test_non_github_repo(self) -> None: - repo = Repository.objects.create( - organization_id=self.organization.id, - name="non-github-repo", - provider="integrations:bitbucket", - external_id="456", - ) - - with self.feature(FEATURE_FLAG): - response = self.get_response(self.organization.slug, repo.id) - assert response.status_code == 400 - assert "only supported for GitHub" in response.data["detail"] - - def test_github_enterprise_repo_rejected(self) -> None: - repo = Repository.objects.create( - organization_id=self.organization.id, - name="enterprise-repo", - provider="integrations:github_enterprise", - external_id="999", - integration_id=self.integration.id, - ) - - with self.feature(FEATURE_FLAG): - response = self.get_response(self.organization.slug, repo.id) - assert response.status_code == 400 - assert "only supported for GitHub" in response.data["detail"] - - def test_repo_without_integration(self) -> None: - repo = Repository.objects.create( - organization_id=self.organization.id, - name="orphan-repo", - provider="integrations:github", - external_id="789", - integration_id=None, - ) - - with self.feature(FEATURE_FLAG): - response = self.get_response(self.organization.slug, repo.id) - assert response.status_code == 400 - - def test_repo_not_found(self) -> None: - with self.feature(FEATURE_FLAG): - response = self.get_response(self.organization.slug, 99999) - assert response.status_code == 404 - - def test_other_orgs_repo_not_accessible(self) -> None: - other_org = self.create_organization(name="other-org") - other_repo = Repository.objects.create( - organization_id=other_org.id, - name="Test-Organization/secret", - provider="integrations:github", - external_id="secret", - integration_id=self.integration.id, - ) - - with self.feature(FEATURE_FLAG): - response = self.get_response(self.organization.slug, other_repo.id) - assert response.status_code == 404 - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_detects_platforms(self, get_jwt: mock.MagicMock) -> None: - responses.add( - method=responses.GET, - url="https://api.github.com/repos/Test-Organization/foo/languages", - json={"Python": 50000}, - status=200, - ) - # Recursive git tree with no manifest files -> no framework detection - responses.add( - method=responses.GET, - url="https://api.github.com/repos/Test-Organization/foo/git/trees/HEAD", - json={ - "sha": "abc", - "truncated": False, - "tree": [ - {"path": "src/app.py", "type": "blob", "size": 1234}, - {"path": "src", "type": "tree"}, - ], - }, - status=200, - ) - - with self.feature(FEATURE_FLAG): - response = self.get_success_response( - self.organization.slug, self.repo.id, status_code=200 - ) - - assert response.data == { - "platforms": [ - { - "platform": "python", - "language": "Python", - "bytes": 50000, - "confidence": "medium", - "priority": 1, - }, - ] - } - - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_detects_framework(self, get_jwt: mock.MagicMock) -> None: - responses.add( - method=responses.GET, - url="https://api.github.com/repos/Test-Organization/foo/languages", - json={"Python": 50000}, - status=200, - ) - # Recursive git tree containing requirements.txt so a content read fires - responses.add( - method=responses.GET, - url="https://api.github.com/repos/Test-Organization/foo/git/trees/HEAD", - json={ - "sha": "abc", - "truncated": False, - "tree": [ - {"path": "requirements.txt", "type": "blob", "size": 42}, - ], - }, - status=200, - ) - - requirements_content = b64encode(b"Django==4.2\ncelery>=5.0\n").decode() - responses.add( - method=responses.GET, - url="https://api.github.com/repos/Test-Organization/foo/contents/requirements.txt", - json={"content": requirements_content}, - status=200, - ) - - with self.feature(FEATURE_FLAG): - response = self.get_success_response( - self.organization.slug, self.repo.id, status_code=200 - ) - - assert response.data == { - "platforms": [ - { - "platform": "python-django", - "language": "Python", - "bytes": 50000, - "confidence": "high", - "priority": 90, - }, - { - "platform": "python-celery", - "language": "Python", - "bytes": 50000, - "confidence": "high", - "priority": 40, - }, - { - "platform": "python", - "language": "Python", - "bytes": 50000, - "confidence": "medium", - "priority": 1, - }, - ] - } - - @mock.patch(f"{ENDPOINT_MODULE}.sentry_sdk") - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_empty_repo_returns_empty_list( - self, get_jwt: mock.MagicMock, sentry_sdk: mock.MagicMock - ) -> None: - responses.add( - method=responses.GET, - url="https://api.github.com/repos/Test-Organization/foo/languages", - json={"Python": 50000}, - status=200, - ) - responses.add( - method=responses.GET, - url="https://api.github.com/repos/Test-Organization/foo/git/trees/HEAD", - json={"message": "Git Repository is empty."}, - status=409, - ) - - with self.feature(FEATURE_FLAG): - response = self.get_success_response( - self.organization.slug, self.repo.id, status_code=200 - ) - - assert response.data == {"platforms": []} - assert sentry_sdk.capture_exception.called - - @mock.patch(f"{ENDPOINT_MODULE}.sentry_sdk") - @mock.patch("sentry.integrations.github.client.get_jwt", return_value="jwt_token_1") - @responses.activate - def test_github_api_error_returns_502( - self, get_jwt: mock.MagicMock, sentry_sdk: mock.MagicMock - ) -> None: - responses.add( - method=responses.GET, - url="https://api.github.com/repos/Test-Organization/foo/languages", - json={"message": "Server Error"}, - status=500, - ) - - with self.feature(FEATURE_FLAG): - response = self.get_response(self.organization.slug, self.repo.id) - - assert response.status_code == 502 - assert "Failed to detect" in response.data["detail"] - assert sentry_sdk.capture_exception.called