Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/sentry/seer/endpoints/project_seer_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sentry.api.bases.project import ProjectEndpoint, ProjectEventPermission
from sentry.api.serializers.rest_framework import CamelSnakeSerializer
from sentry.models.project import Project
from sentry.models.repository import Repository
from sentry.ratelimits.config import RateLimitConfig
from sentry.seer.autofix.utils import get_autofix_repos_from_project_code_mappings
from sentry.seer.models import PreferenceResponse, SeerProjectPreference
Expand Down Expand Up @@ -108,6 +109,16 @@ def post(self, request: Request, project: Project) -> Response:
serializer = ProjectSeerPreferencesSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

for repo_data in serializer.validated_data.get("repositories", []):
repo_exists = Repository.objects.filter(
organization_id=project.organization.id,
provider=repo_data.get("provider"),
external_id=repo_data.get("external_id"),
).exists()

if not repo_exists:
return Response({"detail": "Invalid repository"}, status=400)

path = "/v1/project-preference/set"
body = orjson.dumps(
{
Expand Down
94 changes: 94 additions & 0 deletions tests/sentry/seer/endpoints/test_project_seer_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.conf import settings
from django.urls import reverse

from sentry.models.repository import Repository
from sentry.seer.models import PreferenceResponse, SeerProjectPreference, SeerRepoDefinition
from sentry.testutils.cases import APITestCase

Expand All @@ -27,6 +28,13 @@ def setUp(self) -> None:
"project_id_or_slug": self.project.slug,
},
)
# Create a repository that matches the test data for POST tests
self.repository = Repository.objects.create(
organization_id=self.org.id,
name="getsentry/sentry",
provider="github",
external_id="123456",
)
self.repo_definition = SeerRepoDefinition(
integration_id="111",
provider="github",
Expand Down Expand Up @@ -569,3 +577,89 @@ def test_post_handoff_without_auto_create_pr_defaults_to_false(
preference = body_dict["preference"]
assert "automation_handoff" in preference
assert preference["automation_handoff"]["auto_create_pr"] is False

@patch("sentry.seer.endpoints.project_seer_preferences.requests.post")
def test_post_validates_repository_exists_in_organization(self, mock_post: MagicMock) -> None:
"""Test that POST validates repository exists in the organization"""
Repository.objects.create(
organization_id=self.org.id,
name="getsentry/sentry",
provider="integrations:github",
external_id="valid-external-id",
)

mock_response = Mock()
mock_response.status_code = 200
mock_post.return_value = mock_response

request_data = {
"repositories": [
{
"organization_id": self.org.id,
"integration_id": "111",
"provider": "integrations:github",
"owner": "getsentry",
"name": "sentry",
"external_id": "valid-external-id",
}
],
}

response = self.client.post(self.url, data=request_data)

assert response.status_code == 204
mock_post.assert_called_once()

@patch("sentry.seer.endpoints.project_seer_preferences.requests.post")
def test_post_rejects_repository_not_in_organization(self, mock_post: MagicMock) -> None:
"""Test that POST fails when repository doesn't exist in the organization"""
request_data = {
"repositories": [
{
"organization_id": self.org.id,
"integration_id": "111",
"provider": "integrations:github",
"owner": "getsentry",
"name": "sentry",
"external_id": "nonexistent-repo-id",
}
],
}

response = self.client.post(self.url, data=request_data)

assert response.status_code == 400
assert response.data["detail"] == "Invalid repository"
mock_post.assert_not_called()

@patch("sentry.seer.endpoints.project_seer_preferences.requests.post")
def test_post_rejects_repository_from_different_organization(
self, mock_post: MagicMock
) -> None:
"""Test that POST fails when repository exists but belongs to a different organization"""
other_org = self.create_organization(owner=self.user)
Repository.objects.create(
organization_id=other_org.id,
name="other-org/repo",
provider="integrations:github",
external_id="other-org-repo-id",
)

request_data = {
"repositories": [
{
"organization_id": self.org.id,
"integration_id": "111",
"provider": "integrations:github",
"owner": "other-org",
"name": "repo",
"external_id": "other-org-repo-id",
}
],
}

response = self.client.post(self.url, data=request_data)

assert response.status_code == 400
assert response.data["detail"] == "Invalid repository"
mock_post.assert_not_called()
Loading