From 2e3f36e0c4690c23c15d0cb470c1eb84bc516c09 Mon Sep 17 00:00:00 2001 From: Hoss Date: Fri, 7 Jun 2024 12:13:35 +0800 Subject: [PATCH] feat(decorator): align response fmt for dj and drf --- data_spec_validator/decorator/decorators.py | 16 +++++---- test/test_decorator_dj.py | 36 +++++++++++++++++++-- test/test_decorator_drf.py | 23 +++++++++++++ test/test_nested_spec.py | 15 +++++++-- 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/data_spec_validator/decorator/decorators.py b/data_spec_validator/decorator/decorators.py index 30087ca..45ce6ae 100644 --- a/data_spec_validator/decorator/decorators.py +++ b/data_spec_validator/decorator/decorators.py @@ -7,7 +7,7 @@ try: from django.core.handlers.asgi import ASGIRequest from django.core.handlers.wsgi import WSGIRequest - from django.http import HttpResponseBadRequest, HttpResponseForbidden, QueryDict + from django.http import HttpResponseBadRequest, HttpResponseForbidden, JsonResponse, QueryDict from django.views.generic.base import View except ModuleNotFoundError as e: print(f'[DSV][WARNING] decorator: "dsv" cannot be used, {e}') @@ -150,11 +150,11 @@ def _do_validate(data, spec, multirow): is_multirow = _eval_is_multirow(multirow, data) validate_data_spec(data, spec, multirow=is_multirow) except ValueError as value_err: - error = ValidationError(str(value_err.args)) + error = ValidationError(value_err.args) except PermissionError as perm_err: - error = PermissionDenied(str(perm_err.args)) + error = PermissionDenied(perm_err.args) except (LookupError, TypeError, RuntimeError, DSVError) as parse_err: - error = ParseError(str(parse_err.args)) + error = ParseError(parse_err.args) if error: raise error @@ -165,20 +165,24 @@ def _get_error_response(error, use_drf): Return the error response based on the error type. If the attribute use_drf is True, Raise DRF's exception to let DRF's exception handler do something about it. """ + error_msg = {'messages': error.message} + if use_drf: err_map = { ValidationError: drf_exceptions.ValidationError, PermissionDenied: drf_exceptions.PermissionDenied, ParseError: drf_exceptions.ParseError, } - raise err_map[error.__class__](error.message) + raise err_map[error.__class__](error_msg) resp_map = { ValidationError: HttpResponseBadRequest, PermissionDenied: HttpResponseForbidden, ParseError: HttpResponseBadRequest, } - return resp_map[error.__class__](error.message) + + status_code = resp_map[error.__class__].status_code + return JsonResponse(error_msg, status=status_code) def dsv(spec, multirow=False): diff --git a/test/test_decorator_dj.py b/test/test_decorator_dj.py index 3fb89fc..e82d5e3 100644 --- a/test/test_decorator_dj.py +++ b/test/test_decorator_dj.py @@ -15,7 +15,7 @@ from django.conf import settings from django.core.handlers.asgi import ASGIRequest from django.core.handlers.wsgi import WSGIRequest - from django.http import HttpResponse, HttpResponseBadRequest + from django.http import HttpResponse, JsonResponse from django.test import RequestFactory from django.views import View @@ -58,7 +58,7 @@ class _ViewSpec: class _View(View): @dsv(_ViewSpec) def decorated_func(self, request, named_arg): - pass + return HttpResponse(status=200) factory = RequestFactory() wsgi_req = factory.request() @@ -68,7 +68,8 @@ def decorated_func(self, request, named_arg): view.decorated_func(wsgi_req, named_arg='1') # should pass validation resp = view.decorated_func(wsgi_req, named_arg='') - assert isinstance(resp, HttpResponseBadRequest) + self.assertIsInstance(resp, JsonResponse) + self.assertEqual(resp.status_code, 400) def test_data_and_url_params_should_not_have_intersection(self): # arrange @@ -220,6 +221,35 @@ def decorated_func(self, request, field_a): with self.assertRaises(Exception): non_view.decorated_func(fake_args, field_a='1') + def test_json_response_content(self): + # arrange + class _ViewSpec: + named_arg = Checker([DIGIT_STR]) + + class _View(View): + @dsv(_ViewSpec) + def decorated_func(self, request, named_arg): + return HttpResponse(status=200) + + factory = RequestFactory() + req = factory.request() + view = _View() + + # action + resp_valid = view.decorated_func(req, named_arg='1') + resp_invalid = view.decorated_func(req, named_arg='hi') + + # assert + self.assertIsInstance(resp_valid, HttpResponse) + self.assertEqual(resp_valid.status_code, 200) + + self.assertIsInstance(resp_invalid, JsonResponse) + self.assertEqual(resp_invalid.status_code, 400) + self.assertEqual( + json.loads(resp_invalid.content), + {'messages': ["field: _ViewSpec.named_arg, reason: 'hi' is not a digit str"]}, + ) + if __name__ == '__main__': unittest.main() diff --git a/test/test_decorator_drf.py b/test/test_decorator_drf.py index 6ad06bf..c813328 100644 --- a/test/test_decorator_drf.py +++ b/test/test_decorator_drf.py @@ -176,6 +176,29 @@ def decorated_func(self, request, field_a): with self.assertRaises(Exception): non_view.decorated_func(fake_args, field_a='1') + def test_json_response_content(self): + # arrange + class _ViewSpec: + field_a = Checker([DIGIT_STR]) + + class _View(View): + @dsv(_ViewSpec) + def decorated_func(self, request, field_a): + pass + + factory = RequestFactory() + wsgi_req = factory.request() + req = Request(wsgi_req) + view = _View() + + # action & assert + with self.assertRaises(Exception) as exc_info: + view.decorated_func(req, field_a='hi') + + self.assertEqual( + exc_info.exception.detail, {'messages': ["field: _ViewSpec.field_a, reason: 'hi' is not a digit str"]} + ) + if __name__ == '__main__': unittest.main() diff --git a/test/test_nested_spec.py b/test/test_nested_spec.py index 5138a31..279e4b9 100644 --- a/test/test_nested_spec.py +++ b/test/test_nested_spec.py @@ -1,7 +1,18 @@ import unittest -from data_spec_validator.spec import BOOL, DICT, DIGIT_STR, FLOAT, INT, NONE, SPEC, STR, Checker, validate_data_spec, \ - dsv_feature +from data_spec_validator.spec import ( + BOOL, + DICT, + DIGIT_STR, + FLOAT, + INT, + NONE, + SPEC, + STR, + Checker, + dsv_feature, + validate_data_spec, +) from .utils import is_something_error