Bladeren bron

validation error use now the error builder

Bastien Sevajol 6 jaren geleden
bovenliggende
commit
ad5671a3d0

+ 16 - 0
hapic/context.py Bestand weergeven

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import typing
2
 import typing
3
+
4
+from hapic.error import ErrorBuilderInterface
5
+
3
 try:  # Python 3.5+
6
 try:  # Python 3.5+
4
     from http import HTTPStatus
7
     from http import HTTPStatus
5
 except ImportError:
8
 except ImportError:
68
         :return:
71
         :return:
69
         """
72
         """
70
         raise NotImplementedError()
73
         raise NotImplementedError()
74
+
75
+    def get_default_error_builder(self) -> ErrorBuilderInterface:
76
+        """
77
+        Return a ErrorBuilder who will be used to build default errors
78
+        :return: ErrorBuilderInterface instance
79
+        """
80
+        raise NotImplementedError()
81
+
82
+
83
+class BaseContext(ContextInterface):
84
+    def get_default_error_builder(self) -> ErrorBuilderInterface:
85
+        """ see hapic.context.ContextInterface#get_default_error_builder"""
86
+        return self.default_error_builder

+ 23 - 14
hapic/decorator.py Bestand weergeven

6
 except ImportError:
6
 except ImportError:
7
     from http import client as HTTPStatus
7
     from http import client as HTTPStatus
8
 
8
 
9
-# TODO BS 20171010: bottle specific !  # see #5
10
-import marshmallow
11
 from multidict import MultiDict
9
 from multidict import MultiDict
12
-
13
 from hapic.data import HapicData
10
 from hapic.data import HapicData
14
 from hapic.description import ControllerDescription
11
 from hapic.description import ControllerDescription
15
 from hapic.exception import ProcessException
12
 from hapic.exception import ProcessException
13
+from hapic.exception import OutputValidationException
16
 from hapic.context import ContextInterface
14
 from hapic.context import ContextInterface
17
 from hapic.processor import ProcessorInterface
15
 from hapic.processor import ProcessorInterface
18
 from hapic.processor import RequestParameters
16
 from hapic.processor import RequestParameters
17
+from hapic.error import ErrorBuilderInterface
19
 
18
 
20
 # TODO: Ensure usage of DECORATION_ATTRIBUTE_NAME is documented and
19
 # TODO: Ensure usage of DECORATION_ATTRIBUTE_NAME is documented and
21
 # var names correctly choose.  see #6
20
 # var names correctly choose.  see #6
386
         self,
385
         self,
387
         handled_exception_class: typing.Type[Exception],
386
         handled_exception_class: typing.Type[Exception],
388
         context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]],  # nopep8
387
         context: typing.Union[ContextInterface, typing.Callable[[], ContextInterface]],  # nopep8
389
-        schema: marshmallow.Schema,
388
+        error_builder: typing.Union[ErrorBuilderInterface, typing.Callable[[], ErrorBuilderInterface]],  # nopep8
390
         http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
389
         http_code: HTTPStatus=HTTPStatus.INTERNAL_SERVER_ERROR,
391
     ) -> None:
390
     ) -> None:
392
         self.handled_exception_class = handled_exception_class
391
         self.handled_exception_class = handled_exception_class
393
         self._context = context
392
         self._context = context
394
         self.http_code = http_code
393
         self.http_code = http_code
395
-        self.schema = schema
394
+        self._error_builder = error_builder
396
 
395
 
397
     @property
396
     @property
398
     def context(self) -> ContextInterface:
397
     def context(self) -> ContextInterface:
400
             return self._context()
399
             return self._context()
401
         return self._context
400
         return self._context
402
 
401
 
402
+    @property
403
+    def error_builder(self) -> ErrorBuilderInterface:
404
+        if callable(self._error_builder):
405
+            return self._error_builder()
406
+        return self._error_builder
407
+
403
     def _execute_wrapped_function(
408
     def _execute_wrapped_function(
404
         self,
409
         self,
405
         func,
410
         func,
413
                 func_kwargs,
418
                 func_kwargs,
414
             )
419
             )
415
         except self.handled_exception_class as exc:
420
         except self.handled_exception_class as exc:
416
-            # TODO: "error_detail" attribute name should be configurable
417
-            # TODO BS 20171013: use overrideable mechanism, error object given
418
-            #  to schema ? see #15
419
-            raw_response = {
420
-                'message': str(exc),
421
-                'code': None,
422
-                'detail': getattr(exc, 'error_detail', {}),
423
-            }
421
+            response_content = self.error_builder.build_from_exception(exc)
422
+
423
+            # Check error format
424
+            dumped = self.error_builder.dump(response_content).data
425
+            unmarshall = self.error_builder.load(dumped)
426
+            if unmarshall.errors:
427
+                raise OutputValidationException(
428
+                    'Validation error during dump of error response: {}'
429
+                    .format(
430
+                        str(unmarshall.errors)
431
+                    )
432
+                )
424
 
433
 
425
             error_response = self.context.get_response(
434
             error_response = self.context.get_response(
426
-                raw_response,
435
+                response_content,
427
                 self.http_code,
436
                 self.http_code,
428
             )
437
             )
429
             return error_response
438
             return error_response

+ 60 - 0
hapic/error.py Bestand weergeven

1
+# -*- coding: utf-8 -*-
2
+import marshmallow
3
+
4
+from hapic.processor import ProcessValidationError
5
+
6
+
7
+class ErrorBuilderInterface(marshmallow.Schema):
8
+    """
9
+    ErrorBuilder is a class who represent a Schema (marshmallow.Schema) and
10
+    can generate a response content from exception (build_from_exception)
11
+    """
12
+    def build_from_exception(self, exception: Exception) -> dict:
13
+        """
14
+        Build the error response content from given exception
15
+        :param exception: Original exception who invoke this method
16
+        :return: a dict representing the error response content
17
+        """
18
+        raise NotImplementedError()
19
+
20
+    def build_from_validation_error(
21
+        self,
22
+        error: ProcessValidationError,
23
+    ) -> dict:
24
+        """
25
+        Build the error response content from given process validation error
26
+        :param error: Original ProcessValidationError who invoke this method
27
+        :return: a dict representing the error response content
28
+        """
29
+        raise NotImplementedError()
30
+
31
+
32
+class DefaultErrorBuilder(ErrorBuilderInterface):
33
+    message = marshmallow.fields.String(required=True)
34
+    details = marshmallow.fields.Dict(required=False, missing={})
35
+    code = marshmallow.fields.Raw(missing=None)
36
+
37
+    def build_from_exception(self, exception: Exception) -> dict:
38
+        """
39
+        See hapic.error.ErrorBuilderInterface#build_from_exception docstring
40
+        """
41
+        # TODO: "error_detail" attribute name should be configurable
42
+        return {
43
+            'message': str(exception),
44
+            'details': getattr(exception, 'error_detail', {}),
45
+            'code': None,
46
+        }
47
+
48
+    def build_from_validation_error(
49
+        self,
50
+        error: ProcessValidationError,
51
+    ) -> dict:
52
+        """
53
+        See hapic.error.ErrorBuilderInterface#build_from_validation_error
54
+        docstring
55
+        """
56
+        return {
57
+            'message': error.message,
58
+            'details': error.details,
59
+            'code': None,
60
+        }

+ 20 - 7
hapic/ext/bottle/context.py Bestand weergeven

2
 import json
2
 import json
3
 import re
3
 import re
4
 import typing
4
 import typing
5
+
5
 try:  # Python 3.5+
6
 try:  # Python 3.5+
6
     from http import HTTPStatus
7
     from http import HTTPStatus
7
 except ImportError:
8
 except ImportError:
10
 import bottle
11
 import bottle
11
 from multidict import MultiDict
12
 from multidict import MultiDict
12
 
13
 
13
-from hapic.context import ContextInterface
14
+from hapic.context import BaseContext
14
 from hapic.context import RouteRepresentation
15
 from hapic.context import RouteRepresentation
15
 from hapic.decorator import DecoratedController
16
 from hapic.decorator import DecoratedController
16
 from hapic.decorator import DECORATION_ATTRIBUTE_NAME
17
 from hapic.decorator import DECORATION_ATTRIBUTE_NAME
19
 from hapic.exception import RouteNotFound
20
 from hapic.exception import RouteNotFound
20
 from hapic.processor import RequestParameters
21
 from hapic.processor import RequestParameters
21
 from hapic.processor import ProcessValidationError
22
 from hapic.processor import ProcessValidationError
23
+from hapic.error import DefaultErrorBuilder
24
+from hapic.error import ErrorBuilderInterface
22
 
25
 
23
 # Bottle regular expression to locate url parameters
26
 # Bottle regular expression to locate url parameters
24
 BOTTLE_RE_PATH_URL = re.compile(r'<([^:<>]+)(?::[^<>]+)?>')
27
 BOTTLE_RE_PATH_URL = re.compile(r'<([^:<>]+)(?::[^<>]+)?>')
25
 
28
 
26
 
29
 
27
-class BottleContext(ContextInterface):
28
-    def __init__(self, app: bottle.Bottle):
30
+class BottleContext(BaseContext):
31
+    def __init__(
32
+        self,
33
+        app: bottle.Bottle,
34
+        default_error_builder: ErrorBuilderInterface=None,
35
+    ):
29
         self.app = app
36
         self.app = app
37
+        self.default_error_builder = \
38
+            default_error_builder or DefaultErrorBuilder()  # FDV
30
 
39
 
31
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
40
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
32
         path_parameters = dict(bottle.request.url_args)
41
         path_parameters = dict(bottle.request.url_args)
63
         error: ProcessValidationError,
72
         error: ProcessValidationError,
64
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
73
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
65
     ) -> typing.Any:
74
     ) -> typing.Any:
66
-        # TODO BS 20171010: Manage error schemas, see #4
67
-        from hapic.hapic import _default_global_error_schema
68
-        unmarshall = _default_global_error_schema.dump(error)
75
+        error_content = self.default_error_builder.build_from_validation_error(
76
+            error,
77
+        )
78
+
79
+        # Check error
80
+        dumped = self.default_error_builder.dump(error).data
81
+        unmarshall = self.default_error_builder.load(dumped)
69
         if unmarshall.errors:
82
         if unmarshall.errors:
70
             raise OutputValidationException(
83
             raise OutputValidationException(
71
                 'Validation error during dump of error response: {}'.format(
84
                 'Validation error during dump of error response: {}'.format(
74
             )
87
             )
75
 
88
 
76
         return bottle.HTTPResponse(
89
         return bottle.HTTPResponse(
77
-            body=json.dumps(unmarshall.data),
90
+            body=json.dumps(error_content),
78
             headers=[
91
             headers=[
79
                 ('Content-Type', 'application/json'),
92
                 ('Content-Type', 'application/json'),
80
             ],
93
             ],

+ 24 - 9
hapic/ext/flask/context.py Bestand weergeven

2
 import json
2
 import json
3
 import re
3
 import re
4
 import typing
4
 import typing
5
+
5
 try:  # Python 3.5+
6
 try:  # Python 3.5+
6
     from http import HTTPStatus
7
     from http import HTTPStatus
7
 except ImportError:
8
 except ImportError:
8
     from http import client as HTTPStatus
9
     from http import client as HTTPStatus
9
 
10
 
10
-from hapic.context import ContextInterface
11
+from hapic.context import BaseContext
11
 from hapic.context import RouteRepresentation
12
 from hapic.context import RouteRepresentation
12
 from hapic.decorator import DecoratedController
13
 from hapic.decorator import DecoratedController
13
 from hapic.decorator import DECORATION_ATTRIBUTE_NAME
14
 from hapic.decorator import DECORATION_ATTRIBUTE_NAME
14
 from hapic.exception import OutputValidationException
15
 from hapic.exception import OutputValidationException
15
-from hapic.processor import RequestParameters, ProcessValidationError
16
+from hapic.processor import RequestParameters
17
+from hapic.processor import ProcessValidationError
18
+from hapic.error import DefaultErrorBuilder
19
+from hapic.error import ErrorBuilderInterface
16
 from flask import Flask
20
 from flask import Flask
17
 
21
 
18
 if typing.TYPE_CHECKING:
22
 if typing.TYPE_CHECKING:
22
 FLASK_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
26
 FLASK_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
23
 
27
 
24
 
28
 
25
-class FlaskContext(ContextInterface):
26
-    def __init__(self, app: Flask):
29
+class FlaskContext(BaseContext):
30
+    def __init__(
31
+        self,
32
+        app: Flask,
33
+        default_error_builder: ErrorBuilderInterface=None,
34
+    ):
27
         self.app = app
35
         self.app = app
36
+        self.default_error_builder = \
37
+            default_error_builder or DefaultErrorBuilder()  # FDV
28
 
38
 
29
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
39
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
30
         from flask import request
40
         from flask import request
34
             body_parameters=request.get_json(),  # TODO: Check
44
             body_parameters=request.get_json(),  # TODO: Check
35
             form_parameters=request.form,
45
             form_parameters=request.form,
36
             header_parameters=request.headers,
46
             header_parameters=request.headers,
37
-            files_parameters={},  # TODO: BS 20171115: Code it
47
+            files_parameters=request.files,
38
         )
48
         )
39
 
49
 
40
     def get_response(
50
     def get_response(
54
         error: ProcessValidationError,
64
         error: ProcessValidationError,
55
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
65
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
56
     ) -> typing.Any:
66
     ) -> typing.Any:
57
-        # TODO BS 20171010: Manage error schemas, see #4
58
-        from hapic.hapic import _default_global_error_schema
59
-        unmarshall = _default_global_error_schema.dump(error)
67
+        error_content = self.default_error_builder.build_from_validation_error(
68
+            error,
69
+        )
70
+
71
+        # Check error
72
+        dumped = self.default_error_builder.dump(error).data
73
+        unmarshall = self.default_error_builder.load(dumped)
74
+
60
         if unmarshall.errors:
75
         if unmarshall.errors:
61
             raise OutputValidationException(
76
             raise OutputValidationException(
62
                 'Validation error during dump of error response: {}'.format(
77
                 'Validation error during dump of error response: {}'.format(
65
             )
80
             )
66
         from flask import Response
81
         from flask import Response
67
         return Response(
82
         return Response(
68
-            response=json.dumps(unmarshall.data),
83
+            response=json.dumps(error_content),
69
             mimetype='application/json',
84
             mimetype='application/json',
70
             status=int(http_code),
85
             status=int(http_code),
71
         )
86
         )

+ 21 - 7
hapic/ext/pyramid/context.py Bestand weergeven

2
 import json
2
 import json
3
 import re
3
 import re
4
 import typing
4
 import typing
5
+
5
 try:  # Python 3.5+
6
 try:  # Python 3.5+
6
     from http import HTTPStatus
7
     from http import HTTPStatus
7
 except ImportError:
8
 except ImportError:
8
     from http import client as HTTPStatus
9
     from http import client as HTTPStatus
9
 
10
 
10
-from hapic.context import ContextInterface
11
+from hapic.context import BaseContext
11
 from hapic.context import RouteRepresentation
12
 from hapic.context import RouteRepresentation
12
 from hapic.decorator import DecoratedController
13
 from hapic.decorator import DecoratedController
13
 from hapic.decorator import DECORATION_ATTRIBUTE_NAME
14
 from hapic.decorator import DECORATION_ATTRIBUTE_NAME
14
 from hapic.exception import OutputValidationException
15
 from hapic.exception import OutputValidationException
15
 from hapic.processor import RequestParameters
16
 from hapic.processor import RequestParameters
16
 from hapic.processor import ProcessValidationError
17
 from hapic.processor import ProcessValidationError
18
+from hapic.error import DefaultErrorBuilder
19
+from hapic.error import ErrorBuilderInterface
17
 
20
 
18
 if typing.TYPE_CHECKING:
21
 if typing.TYPE_CHECKING:
19
     from pyramid.response import Response
22
     from pyramid.response import Response
23
 PYRAMID_RE_PATH_URL = re.compile(r'')
26
 PYRAMID_RE_PATH_URL = re.compile(r'')
24
 
27
 
25
 
28
 
26
-class PyramidContext(ContextInterface):
27
-    def __init__(self, configurator: 'Configurator'):
29
+class PyramidContext(BaseContext):
30
+    def __init__(
31
+        self,
32
+        configurator: 'Configurator',
33
+        default_error_builder: ErrorBuilderInterface = None,
34
+    ):
28
         self.configurator = configurator
35
         self.configurator = configurator
36
+        self.default_error_builder = \
37
+            default_error_builder or DefaultErrorBuilder()  # FDV
29
 
38
 
30
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
39
     def get_request_parameters(self, *args, **kwargs) -> RequestParameters:
31
         req = args[-1]  # TODO : Check
40
         req = args[-1]  # TODO : Check
65
         error: ProcessValidationError,
74
         error: ProcessValidationError,
66
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
75
         http_code: HTTPStatus=HTTPStatus.BAD_REQUEST,
67
     ) -> typing.Any:
76
     ) -> typing.Any:
68
-        # TODO BS 20171010: Manage error schemas, see #4
69
         from pyramid.response import Response
77
         from pyramid.response import Response
70
-        from hapic.hapic import _default_global_error_schema
71
-        unmarshall = _default_global_error_schema.dump(error)
78
+
79
+        error_content = self.default_error_builder.build_from_validation_error(
80
+            error,
81
+        )
82
+
83
+        # Check error
84
+        dumped = self.default_error_builder.dump(error).data
85
+        unmarshall = self.default_error_builder.load(dumped)
72
         if unmarshall.errors:
86
         if unmarshall.errors:
73
             raise OutputValidationException(
87
             raise OutputValidationException(
74
                 'Validation error during dump of error response: {}'.format(
88
                 'Validation error during dump of error response: {}'.format(
77
             )
91
             )
78
 
92
 
79
         return Response(
93
         return Response(
80
-            body=json.dumps(unmarshall.data),
94
+            body=json.dumps(error_content),
81
             headers=[
95
             headers=[
82
                 ('Content-Type', 'application/json'),
96
                 ('Content-Type', 'application/json'),
83
             ],
97
             ],

+ 14 - 16
hapic/hapic.py Bestand weergeven

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import typing
2
 import typing
3
 import uuid
3
 import uuid
4
+import functools
4
 try:  # Python 3.5+
5
 try:  # Python 3.5+
5
     from http import HTTPStatus
6
     from http import HTTPStatus
6
 except ImportError:
7
 except ImportError:
7
     from http import client as HTTPStatus
8
     from http import client as HTTPStatus
8
 
9
 
9
-import functools
10
-
11
-import marshmallow
12
-
13
 from hapic.buffer import DecorationBuffer
10
 from hapic.buffer import DecorationBuffer
14
 from hapic.context import ContextInterface
11
 from hapic.context import ContextInterface
15
 from hapic.decorator import DecoratedController
12
 from hapic.decorator import DecoratedController
39
 from hapic.processor import MarshmallowInputProcessor
36
 from hapic.processor import MarshmallowInputProcessor
40
 from hapic.processor import MarshmallowInputFilesProcessor
37
 from hapic.processor import MarshmallowInputFilesProcessor
41
 from hapic.processor import MarshmallowOutputProcessor
38
 from hapic.processor import MarshmallowOutputProcessor
42
-
43
-
44
-class ErrorResponseSchema(marshmallow.Schema):
45
-    message = marshmallow.fields.String(required=True)
46
-    details = marshmallow.fields.Dict(required=False, missing={})
47
-    code = marshmallow.fields.Raw(missing=None)
48
-
49
-
50
-_default_global_error_schema = ErrorResponseSchema()
39
+from hapic.error import ErrorBuilderInterface
51
 
40
 
52
 
41
 
53
 # TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb), see #12
42
 # TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb), see #12
59
         self._buffer = DecorationBuffer()
48
         self._buffer = DecorationBuffer()
60
         self._controllers = []  # type: typing.List[DecoratedController]
49
         self._controllers = []  # type: typing.List[DecoratedController]
61
         self._context = None  # type: ContextInterface
50
         self._context = None  # type: ContextInterface
51
+        self._error_builder = None  # type: ErrorBuilderInterface
62
         self.doc_generator = DocGenerator()
52
         self.doc_generator = DocGenerator()
63
 
53
 
64
         # This local function will be pass to different components
54
         # This local function will be pass to different components
67
         def context_getter():
57
         def context_getter():
68
             return self._context
58
             return self._context
69
 
59
 
60
+        # This local function will be pass to different components
61
+        # who will need error_builder but declared (like with decorator)
62
+        # before error_builder declaration
63
+        def error_builder_getter():
64
+            return self._context.get_default_error_builder()
65
+
70
         self._context_getter = context_getter
66
         self._context_getter = context_getter
67
+        self._error_builder_getter = error_builder_getter
71
 
68
 
72
         # TODO: Permettre la surcharge des classes utilisés ci-dessous, see #14
69
         # TODO: Permettre la surcharge des classes utilisés ci-dessous, see #14
73
 
70
 
346
 
343
 
347
     def handle_exception(
344
     def handle_exception(
348
         self,
345
         self,
349
-        handled_exception_class: typing.Type[Exception],
346
+        handled_exception_class: typing.Type[Exception]=Exception,
350
         http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
347
         http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
348
+        error_builder: ErrorBuilderInterface=None,
351
         context: ContextInterface = None,
349
         context: ContextInterface = None,
352
     ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
350
     ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
353
         context = context or self._context_getter
351
         context = context or self._context_getter
352
+        error_builder = error_builder or self._error_builder_getter
354
 
353
 
355
         decoration = ExceptionHandlerControllerWrapper(
354
         decoration = ExceptionHandlerControllerWrapper(
356
             handled_exception_class,
355
             handled_exception_class,
357
             context,
356
             context,
358
-            # TODO BS 20171013: Permit schema overriding, see #15
359
-            schema=_default_global_error_schema,
357
+            error_builder=error_builder,
360
             http_code=http_code,
358
             http_code=http_code,
361
         )
359
         )
362
 
360
 

+ 1 - 0
tests/func/fake_api/test_bottle.py Bestand weergeven

59
     resp = app.post('/users/', status='*')
59
     resp = app.post('/users/', status='*')
60
     assert resp.status_int == 400
60
     assert resp.status_int == 400
61
     assert resp.json == {
61
     assert resp.json == {
62
+        'code': None,
62
         'details': {
63
         'details': {
63
             'email_address': ['Missing data for required field.'],
64
             'email_address': ['Missing data for required field.'],
64
             'username': ['Missing data for required field.'],
65
             'username': ['Missing data for required field.'],

+ 1 - 0
tests/func/fake_api/test_flask.py Bestand weergeven

57
     resp = app.post('/users/', status='*')
57
     resp = app.post('/users/', status='*')
58
     assert resp.status_int == 400
58
     assert resp.status_int == 400
59
     assert resp.json == {
59
     assert resp.json == {
60
+        'code': None,
60
         'details': {
61
         'details': {
61
             'email_address': ['Missing data for required field.'],
62
             'email_address': ['Missing data for required field.'],
62
             'username': ['Missing data for required field.'],
63
             'username': ['Missing data for required field.'],

+ 1 - 0
tests/func/fake_api/test_pyramid.py Bestand weergeven

58
     resp = app.post('/users/', status='*')
58
     resp = app.post('/users/', status='*')
59
     assert resp.status_int == 400
59
     assert resp.status_int == 400
60
     assert resp.json == {
60
     assert resp.json == {
61
+        'code': None,
61
         'details': {
62
         'details': {
62
             'email_address': ['Missing data for required field.'],
63
             'email_address': ['Missing data for required field.'],
63
             'username': ['Missing data for required field.'],
64
             'username': ['Missing data for required field.'],

+ 32 - 5
tests/unit/test_decorator.py Bestand weergeven

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import pytest
2
 import typing
3
 import typing
4
+
5
+from hapic.exception import OutputValidationException
6
+
3
 try:  # Python 3.5+
7
 try:  # Python 3.5+
4
     from http import HTTPStatus
8
     from http import HTTPStatus
5
 except ImportError:
9
 except ImportError:
14
 from hapic.decorator import InputControllerWrapper
18
 from hapic.decorator import InputControllerWrapper
15
 from hapic.decorator import InputOutputControllerWrapper
19
 from hapic.decorator import InputOutputControllerWrapper
16
 from hapic.decorator import OutputControllerWrapper
20
 from hapic.decorator import OutputControllerWrapper
17
-from hapic.hapic import ErrorResponseSchema
21
+from hapic.error import DefaultErrorBuilder
18
 from hapic.processor import MarshmallowOutputProcessor
22
 from hapic.processor import MarshmallowOutputProcessor
19
 from hapic.processor import ProcessValidationError
23
 from hapic.processor import ProcessValidationError
20
 from hapic.processor import ProcessorInterface
24
 from hapic.processor import ProcessorInterface
260
         wrapper = ExceptionHandlerControllerWrapper(
264
         wrapper = ExceptionHandlerControllerWrapper(
261
             ZeroDivisionError,
265
             ZeroDivisionError,
262
             context,
266
             context,
263
-            schema=ErrorResponseSchema(),
267
+            error_builder=DefaultErrorBuilder(),
264
             http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
268
             http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
265
         )
269
         )
266
 
270
 
275
         assert response['original_response'] == {
279
         assert response['original_response'] == {
276
             'message': 'We are testing',
280
             'message': 'We are testing',
277
             'code': None,
281
             'code': None,
278
-            'detail': {},
282
+            'details': {},
279
         }
283
         }
280
 
284
 
281
     def test_unit__exception_handled__ok__exception_error_dict(self):
285
     def test_unit__exception_handled__ok__exception_error_dict(self):
288
         wrapper = ExceptionHandlerControllerWrapper(
292
         wrapper = ExceptionHandlerControllerWrapper(
289
             MyException,
293
             MyException,
290
             context,
294
             context,
291
-            schema=ErrorResponseSchema(),
295
+            error_builder=DefaultErrorBuilder(),
292
             http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
296
             http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
293
         )
297
         )
294
 
298
 
305
         assert response['original_response'] == {
309
         assert response['original_response'] == {
306
             'message': 'We are testing',
310
             'message': 'We are testing',
307
             'code': None,
311
             'code': None,
308
-            'detail': {'foo': 'bar'},
312
+            'details': {'foo': 'bar'},
309
         }
313
         }
314
+
315
+    def test_unit__exception_handler__error__error_content_malformed(self):
316
+        class MyException(Exception):
317
+            pass
318
+
319
+        class MyErrorBuilder(DefaultErrorBuilder):
320
+            def build_from_exception(self, exception: Exception) -> dict:
321
+                # this is not matching with DefaultErrorBuilder schema
322
+                return {}
323
+
324
+        context = MyContext(app=None)
325
+        wrapper = ExceptionHandlerControllerWrapper(
326
+            MyException,
327
+            context,
328
+            error_builder=MyErrorBuilder(),
329
+        )
330
+
331
+        def raise_it():
332
+            raise MyException()
333
+
334
+        wrapper = wrapper.get_wrapper(raise_it)
335
+        with pytest.raises(OutputValidationException):
336
+            wrapper()