Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 7 additions & 7 deletions src/sentry/auth_v2/endpoints/auth_merge_user_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@


class AuthMergeUserAccountsValidator(serializers.Serializer):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make AuthMergeUserAccountsValidator subclass CamelSnakeSerializer and keep the snake case here, we like snake case on the backend

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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Single User Serialization Returns Array

The AuthMergeUserAccountsEndpoint's POST request returns a list containing a single serialized user object (e.g., [{user_data}]) instead of the single user object (e.g., {user_data}). This is because the serialize function is called with [primary_user] (a list), causing it to return an array, which is inconsistent with typical REST API patterns for single resource operations.

Locations (1)
Fix in Cursor Fix in Web

12 changes: 6 additions & 6 deletions src/sentry/users/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
36 changes: 18 additions & 18 deletions tests/sentry/auth_v2/endpoints/test_auth_merge_user_accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -83,19 +83,19 @@ 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
assert response.data == {"error": "Incorrect verification code"}

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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading