diff --git a/api/edge_api/identities/views.py b/api/edge_api/identities/views.py index ab241b6ba652..5188b8138fd3 100644 --- a/api/edge_api/identities/views.py +++ b/api/edge_api/identities/views.py @@ -176,8 +176,20 @@ def update_traits(self, request, *args, **kwargs): # type: ignore[no-untyped-de if not environment.project.organisation.persist_trait_data: raise TraitPersistenceError() edge_identity = self.get_object() + # `pyngo.drf_error_details` assumes every pydantic error has a non-empty + # `loc`, which isn't true when the payload itself isn't an object (e.g. a + # JSON array), so that case is rejected here before it ever reaches + # `TraitModel.model_validate`/`drf_error_details`. + if not isinstance(request.data, typing.Mapping): + raise ValidationError( + { + "non_field_errors": [ + "Expected an object with trait_key and trait_value." + ] + } + ) try: - trait = TraitModel(**request.data) + trait = TraitModel.model_validate(request.data) except pydantic.ValidationError as validation_error: raise ValidationError( drf_error_details(validation_error) diff --git a/api/tests/integration/edge_api/identities/test_edge_identity_viewset.py b/api/tests/integration/edge_api/identities/test_edge_identity_viewset.py index 9d16e4e3193e..89ae70121a96 100644 --- a/api/tests/integration/edge_api/identities/test_edge_identity_viewset.py +++ b/api/tests/integration/edge_api/identities/test_edge_identity_viewset.py @@ -696,3 +696,32 @@ def test_edge_identities_update_trait__persist_trait_data_disabled__returns_400( ) # Then assert response.status_code == status.HTTP_400_BAD_REQUEST + + +def test_edge_identities_update_trait__malformed_traits_payload__returns_400( # type: ignore[no-untyped-def] + admin_client, + dynamo_enabled_environment, + environment_api_key, + identity_document, + edge_identity_dynamo_wrapper_mock, +): + # Given + edge_identity_dynamo_wrapper_mock.get_item_from_uuid_or_404.return_value = ( + identity_document + ) + identity_uuid = identity_document["identity_uuid"] + url = reverse( + "api-v1:environments:environment-edge-identities-update-traits", + args=[environment_api_key, identity_uuid], + ) + # A JSON array instead of the expected object. + data = [{"trait_key": "some_key", "trait_value": "some_value"}] + + # When + response = admin_client.put( + url, data=json.dumps(data), content_type="application/json" + ) + + # Then + assert response.status_code == status.HTTP_400_BAD_REQUEST + edge_identity_dynamo_wrapper_mock.put_item.assert_not_called()