From 287b0854e499794163a3126ea64a28d098f5b908 Mon Sep 17 00:00:00 2001 From: Kiptoo Magutt Date: Tue, 6 Feb 2018 17:18:42 +0300 Subject: [PATCH] catch validation errors during patch serialization - we want to force triggering of validation exceptions during serialiation - add unit test to verify that validation exception is raised on patch - a validation exception should be raised when a validation error occurs during serialization - create a serializer class that automatically throws the validation exception - uses marshmallow's ValidationError exception, but in theory any exception could be used to force the check in order to pass them through to the flask-restless validations exceptions handler and thus have it return meaningful errors to client --- flask_restless/views/resources.py | 2 ++ requirements/test.txt | 1 + tests/helpers.py | 23 +++++++++++++++++++++++ tests/test_updating.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/flask_restless/views/resources.py b/flask_restless/views/resources.py index f4fe1f4b..0f3132e0 100644 --- a/flask_restless/views/resources.py +++ b/flask_restless/views/resources.py @@ -721,6 +721,8 @@ def patch(self, resource_id): result = self.serializer.serialize(instance, only=only) except SerializationException as exception: return errors_from_serialization_exceptions([exception]) + except self.validation_exceptions as exception: + return self._handle_validation_exception(exception) status = 200 else: result = dict() diff --git a/requirements/test.txt b/requirements/test.txt index 893c49f0..30d0ea5a 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,2 +1,3 @@ -r install.txt unittest2 +marshmallow diff --git a/tests/helpers.py b/tests/helpers.py index 6e00cadd..c30c1420 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -39,6 +39,7 @@ from sqlalchemy.orm.session import Session as SessionBase from sqlalchemy.types import CHAR from sqlalchemy.types import TypeDecorator +from marshmallow import ValidationError from flask_restless import APIManager from flask_restless import collection_name @@ -118,6 +119,28 @@ def deserialize_many(self, *args, **kw): """Immediately raises a :exc:`DeserializationException`.""" raise DeserializationException +class raise_s_validation_error(DefaultSerializer): + """A serializer that unconditionally raises a validation error when + either :meth:`.serialize` or :meth:`.serialize_many` is called. + + This class is useful for tests of validation errors. + + """ + + def serialize(self, instance, *args, **kw): + """Immediately raises a :exc:`ValidationError` with + access to the provided `instance` of a SQLAlchemy model. + + """ + raise ValidationError(instance) + + def serialize_many(self, instances, *args, **kw): + """Immediately raises a :exc:`ValidationError`. + + This function requires `instances` to be non-empty. + + """ + raise ValidationError(instances[0]) def isclass(obj): """Returns ``True`` if and only if the specified object is a type (or a diff --git a/tests/test_updating.py b/tests/test_updating.py index 542bba22..52e21180 100644 --- a/tests/test_updating.py +++ b/tests/test_updating.py @@ -41,6 +41,7 @@ from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import backref from sqlalchemy.orm import relationship +from marshmallow import ValidationError from flask_restless import APIManager from flask_restless import JSONAPI_MIMETYPE @@ -55,6 +56,7 @@ from .helpers import MSIE9_UA from .helpers import ManagerTestBase from .helpers import raise_s_exception as raise_exception +from .helpers import raise_s_validation_error as raise_validation_error class TestUpdating(ManagerTestBase): @@ -893,6 +895,34 @@ def test_serialization_exception(self): check_sole_error(response, 500, ['Failed to serialize', 'type', 'tag', 'ID', '1']) + def test_serialization_validation_error(self): + """Tests that serialization validation errors are caught when + responding with content. + + A representation of the modified resource is returned to the + client when an update causes additional changes in the resource + in ways other than those specified by the client. + + """ + tag = self.Tag(id=1) + self.session.add(tag) + self.session.commit() + self.manager.create_api(self.Tag, methods=['PATCH'], + validation_exceptions=[ValidationError], + serializer_class=raise_validation_error) + data = { + 'data': { + 'type': 'tag', + 'id': '1', + 'attributes': { + 'name': u'foo' + } + } + } + response = self.app.patch('/api/tag/1', data=dumps(data)) + assert response.status_code == 400 + assert response.status == '400 BAD REQUEST' + def test_dont_assign_to_method(self): """Tests that if a certain method is to be included in a resource, that method is not assigned to when updating the