From 8fdc497f22ee9f82d45c95a17655a872b2d21ca5 Mon Sep 17 00:00:00 2001 From: Michelle Fu Date: Tue, 29 Jul 2025 15:19:21 -0700 Subject: [PATCH 1/3] API updates for frontend --- .../endpoints/auth_merge_user_accounts.py | 14 ++++---- src/sentry/users/api/serializers/user.py | 12 +++---- .../test_auth_merge_user_accounts.py | 36 +++++++++---------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py b/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py index d9eb83aa4b6f51..126f439fe95f08 100644 --- a/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py +++ b/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py @@ -14,9 +14,9 @@ class AuthMergeUserAccountsValidator(serializers.Serializer): - verification_code = serializers.CharField(required=True) - ids_to_merge = serializers.ListField(child=serializers.IntegerField(), required=True) - ids_to_delete = serializers.ListField(child=serializers.IntegerField(), required=True) + verificationCode = serializers.CharField(required=True) + idsToMerge = serializers.ListField(child=serializers.IntegerField(), required=True) + idsToDelete = serializers.ListField(child=serializers.IntegerField(), required=True) @control_silo_endpoint @@ -62,14 +62,14 @@ def post(self, request: Request) -> Response: verification_code = UserMergeVerificationCode.objects.filter( user_id=primary_user.id ).first() - if verification_code is None or verification_code.token != result["verification_code"]: + if verification_code is None or verification_code.token != result["verificationCode"]: return Response( status=403, data={"error": "Incorrect verification code"}, ) - ids_to_merge = result["ids_to_merge"] - ids_to_delete = result["ids_to_delete"] + ids_to_merge = result["idsToMerge"] + ids_to_delete = result["idsToDelete"] if not set(ids_to_merge).isdisjoint(set(ids_to_delete)): return Response( status=400, @@ -104,4 +104,4 @@ def post(self, request: Request) -> Response: user.merge_to(primary_user) user.delete() - return Response("Successfully merged user accounts.") + return Response(serialize([primary_user], request.user, UserSerializerWithOrgMemberships())) diff --git a/src/sentry/users/api/serializers/user.py b/src/sentry/users/api/serializers/user.py index 6abeea3552402a..b06060aa6b9bf1 100644 --- a/src/sentry/users/api/serializers/user.py +++ b/src/sentry/users/api/serializers/user.py @@ -415,19 +415,19 @@ def get_attrs( memberships = OrganizationMemberMapping.objects.filter( user_id__in={u.id for u in item_list} ).values_list("user_id", "organization_id", named=True) - active_org_id_to_name = dict( + active_org_id_to_slug = dict( OrganizationMapping.objects.filter( organization_id__in={m.organization_id for m in memberships}, status=OrganizationStatus.ACTIVE, - ).values_list("organization_id", "name") + ).values_list("organization_id", "slug") ) - active_organization_ids = active_org_id_to_name.keys() + active_organization_ids = active_org_id_to_slug.keys() - user_org_memberships: DefaultDict[int, list[str]] = defaultdict(list) + user_org_memberships: DefaultDict[int, set[str]] = defaultdict(set) for membership in memberships: if membership.organization_id in active_organization_ids: - user_org_memberships[membership.user_id].append( - active_org_id_to_name[membership.organization_id] + user_org_memberships[membership.user_id].add( + active_org_id_to_slug[membership.organization_id] ) for item in item_list: attrs[item]["organizations"] = user_org_memberships[item.id] diff --git a/tests/sentry/auth_v2/endpoints/test_auth_merge_user_accounts.py b/tests/sentry/auth_v2/endpoints/test_auth_merge_user_accounts.py index a375248c4fdf55..757493f8438b8b 100644 --- a/tests/sentry/auth_v2/endpoints/test_auth_merge_user_accounts.py +++ b/tests/sentry/auth_v2/endpoints/test_auth_merge_user_accounts.py @@ -72,9 +72,9 @@ def setUp(self) -> None: def test_simple(self) -> None: data = { - "ids_to_merge": [self.user2.id], - "ids_to_delete": [self.user3.id], - "verification_code": self.verification_code.token, + "idsToMerge": [self.user2.id], + "idsToDelete": [self.user3.id], + "verificationCode": self.verification_code.token, } self.get_success_response(**data) @@ -83,9 +83,9 @@ def test_simple(self) -> None: def test_incorrect_code(self) -> None: data = { - "ids_to_merge": [self.user2.id], - "ids_to_delete": [self.user3.id], - "verification_code": "hello", + "idsToMerge": [self.user2.id], + "idsToDelete": [self.user3.id], + "verificationCode": "hello", } response = self.get_error_response(**data) assert response.status_code == 403 @@ -93,9 +93,9 @@ def test_incorrect_code(self) -> None: def test_merge_unrelated_account(self) -> None: data = { - "ids_to_merge": [self.unrelated_user.id], - "ids_to_delete": [self.user2.id, self.user3.id], - "verification_code": self.verification_code.token, + "idsToMerge": [self.unrelated_user.id], + "idsToDelete": [self.user2.id, self.user3.id], + "verificationCode": self.verification_code.token, } response = self.get_error_response(**data) assert response.status_code == 403 @@ -105,9 +105,9 @@ def test_merge_unrelated_account(self) -> None: def test_related_and_unrelated_accounts(self) -> None: data = { - "ids_to_merge": [self.user2.id, self.unrelated_user.id], - "ids_to_delete": [self.user3.id], - "verification_code": self.verification_code.token, + "idsToMerge": [self.user2.id, self.unrelated_user.id], + "idsToDelete": [self.user3.id], + "verificationCode": self.verification_code.token, } response = self.get_error_response(**data) assert response.status_code == 403 @@ -117,9 +117,9 @@ def test_related_and_unrelated_accounts(self) -> None: def test_pass_current_user_id(self) -> None: data = { - "ids_to_merge": [self.user1.id], - "ids_to_delete": [self.user2.id, self.user3.id], - "verification_code": self.verification_code.token, + "idsToMerge": [self.user1.id], + "idsToDelete": [self.user2.id, self.user3.id], + "verificationCode": self.verification_code.token, } response = self.get_error_response(**data) assert response.status_code == 400 @@ -129,9 +129,9 @@ def test_pass_current_user_id(self) -> None: def test_not_disjoint(self) -> None: data = { - "ids_to_merge": [self.user2.id], - "ids_to_delete": [self.user2.id, self.user3.id], - "verification_code": self.verification_code.token, + "idsToMerge": [self.user2.id], + "idsToDelete": [self.user2.id, self.user3.id], + "verificationCode": self.verification_code.token, } response = self.get_error_response(**data) assert response.status_code == 400 From e36467cb1036b08242286de8f99079c06714c041 Mon Sep 17 00:00:00 2001 From: Michelle Fu Date: Thu, 31 Jul 2025 09:28:14 -0700 Subject: [PATCH 2/3] camel snake case serializer --- .../endpoints/auth_merge_user_accounts.py | 15 ++++---- .../test_auth_merge_user_accounts.py | 36 +++++++++---------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py b/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py index 126f439fe95f08..5e5eeada629ca7 100644 --- a/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py +++ b/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py @@ -7,6 +7,7 @@ from sentry.api.base import control_silo_endpoint from sentry.api.paginator import OffsetPaginator from sentry.api.serializers import serialize +from sentry.api.serializers.rest_framework.base import CamelSnakeSerializer from sentry.auth_v2.endpoints.base import AuthV2Endpoint from sentry.users.api.serializers.user import UserSerializerWithOrgMemberships from sentry.users.models.user import User @@ -14,13 +15,13 @@ class AuthMergeUserAccountsValidator(serializers.Serializer): - verificationCode = serializers.CharField(required=True) - idsToMerge = serializers.ListField(child=serializers.IntegerField(), required=True) - idsToDelete = serializers.ListField(child=serializers.IntegerField(), required=True) + verification_code = serializers.CharField(required=True) + ids_to_merge = serializers.ListField(child=serializers.IntegerField(), required=True) + ids_to_delete = serializers.ListField(child=serializers.IntegerField(), required=True) @control_silo_endpoint -class AuthMergeUserAccountsEndpoint(AuthV2Endpoint): +class AuthMergeUserAccountsEndpoint(AuthV2Endpoint, CamelSnakeSerializer): publish_status = { "GET": ApiPublishStatus.PRIVATE, "POST": ApiPublishStatus.PRIVATE, @@ -62,14 +63,14 @@ def post(self, request: Request) -> Response: verification_code = UserMergeVerificationCode.objects.filter( user_id=primary_user.id ).first() - if verification_code is None or verification_code.token != result["verificationCode"]: + if verification_code is None or verification_code.token != result["verification_code"]: return Response( status=403, data={"error": "Incorrect verification code"}, ) - ids_to_merge = result["idsToMerge"] - ids_to_delete = result["idsToDelete"] + ids_to_merge = result["ids_to_merge"] + ids_to_delete = result["ids_to_delete"] if not set(ids_to_merge).isdisjoint(set(ids_to_delete)): return Response( status=400, diff --git a/tests/sentry/auth_v2/endpoints/test_auth_merge_user_accounts.py b/tests/sentry/auth_v2/endpoints/test_auth_merge_user_accounts.py index 757493f8438b8b..a375248c4fdf55 100644 --- a/tests/sentry/auth_v2/endpoints/test_auth_merge_user_accounts.py +++ b/tests/sentry/auth_v2/endpoints/test_auth_merge_user_accounts.py @@ -72,9 +72,9 @@ def setUp(self) -> None: def test_simple(self) -> None: data = { - "idsToMerge": [self.user2.id], - "idsToDelete": [self.user3.id], - "verificationCode": self.verification_code.token, + "ids_to_merge": [self.user2.id], + "ids_to_delete": [self.user3.id], + "verification_code": self.verification_code.token, } self.get_success_response(**data) @@ -83,9 +83,9 @@ def test_simple(self) -> None: def test_incorrect_code(self) -> None: data = { - "idsToMerge": [self.user2.id], - "idsToDelete": [self.user3.id], - "verificationCode": "hello", + "ids_to_merge": [self.user2.id], + "ids_to_delete": [self.user3.id], + "verification_code": "hello", } response = self.get_error_response(**data) assert response.status_code == 403 @@ -93,9 +93,9 @@ def test_incorrect_code(self) -> None: def test_merge_unrelated_account(self) -> None: data = { - "idsToMerge": [self.unrelated_user.id], - "idsToDelete": [self.user2.id, self.user3.id], - "verificationCode": self.verification_code.token, + "ids_to_merge": [self.unrelated_user.id], + "ids_to_delete": [self.user2.id, self.user3.id], + "verification_code": self.verification_code.token, } response = self.get_error_response(**data) assert response.status_code == 403 @@ -105,9 +105,9 @@ def test_merge_unrelated_account(self) -> None: def test_related_and_unrelated_accounts(self) -> None: data = { - "idsToMerge": [self.user2.id, self.unrelated_user.id], - "idsToDelete": [self.user3.id], - "verificationCode": self.verification_code.token, + "ids_to_merge": [self.user2.id, self.unrelated_user.id], + "ids_to_delete": [self.user3.id], + "verification_code": self.verification_code.token, } response = self.get_error_response(**data) assert response.status_code == 403 @@ -117,9 +117,9 @@ def test_related_and_unrelated_accounts(self) -> None: def test_pass_current_user_id(self) -> None: data = { - "idsToMerge": [self.user1.id], - "idsToDelete": [self.user2.id, self.user3.id], - "verificationCode": self.verification_code.token, + "ids_to_merge": [self.user1.id], + "ids_to_delete": [self.user2.id, self.user3.id], + "verification_code": self.verification_code.token, } response = self.get_error_response(**data) assert response.status_code == 400 @@ -129,9 +129,9 @@ def test_pass_current_user_id(self) -> None: def test_not_disjoint(self) -> None: data = { - "idsToMerge": [self.user2.id], - "idsToDelete": [self.user2.id, self.user3.id], - "verificationCode": self.verification_code.token, + "ids_to_merge": [self.user2.id], + "ids_to_delete": [self.user2.id, self.user3.id], + "verification_code": self.verification_code.token, } response = self.get_error_response(**data) assert response.status_code == 400 From e3ea481b0a8192f6c3167754b5ce78af464a71d2 Mon Sep 17 00:00:00 2001 From: Michelle Fu Date: Thu, 31 Jul 2025 14:20:53 -0700 Subject: [PATCH 3/3] fix --- src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py b/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py index 5e5eeada629ca7..8dc4c04a79c341 100644 --- a/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py +++ b/src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py @@ -14,14 +14,14 @@ from sentry.users.models.user_merge_verification_code import UserMergeVerificationCode -class AuthMergeUserAccountsValidator(serializers.Serializer): +class AuthMergeUserAccountsValidator(CamelSnakeSerializer): verification_code = serializers.CharField(required=True) ids_to_merge = serializers.ListField(child=serializers.IntegerField(), required=True) ids_to_delete = serializers.ListField(child=serializers.IntegerField(), required=True) @control_silo_endpoint -class AuthMergeUserAccountsEndpoint(AuthV2Endpoint, CamelSnakeSerializer): +class AuthMergeUserAccountsEndpoint(AuthV2Endpoint): publish_status = { "GET": ApiPublishStatus.PRIVATE, "POST": ApiPublishStatus.PRIVATE,