From 1c29a28bab58e83d74f9879442b95360d10a4530 Mon Sep 17 00:00:00 2001 From: Hoss Date: Fri, 16 Jun 2023 12:28:22 +0800 Subject: [PATCH 1/2] feat(decorator): support application/json --- data_spec_validator/decorator/decorators.py | 12 ++++++++- test/test_decorator_dj.py | 28 ++++++++++++++++++--- test/utils.py | 10 +++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/data_spec_validator/decorator/decorators.py b/data_spec_validator/decorator/decorators.py index 8f4a91a..30087ca 100644 --- a/data_spec_validator/decorator/decorators.py +++ b/data_spec_validator/decorator/decorators.py @@ -1,3 +1,4 @@ +import json from functools import wraps from typing import Dict, List, Union @@ -102,8 +103,17 @@ def _collect_data(method, req_qp, req_data) -> Dict: # TODO: Don't care about the query_params if it's not a dict or the payload is in list. return req_data + def _get_dj_payload(request): + content_type = request.headers.get('Content-Type') + if content_type == 'application/json': + try: + return request.body and json.loads(request.body) or {} + except Exception: + raise ParseError('Unable to parse request body as JSON') + return request.POST + if is_wsgi_request or is_asgi_request: - data = _collect_data(req.method, req.GET, req.POST) + data = _collect_data(req.method, req.GET, _get_dj_payload(req)) else: data = _collect_data(req.method, req.query_params, req.data) diff --git a/test/test_decorator_dj.py b/test/test_decorator_dj.py index 64ece1e..3fb89fc 100644 --- a/test/test_decorator_dj.py +++ b/test/test_decorator_dj.py @@ -1,10 +1,12 @@ import itertools +import json import unittest from unittest.mock import patch from parameterized import parameterized, parameterized_class from data_spec_validator.decorator import dsv, dsv_request_meta +from data_spec_validator.decorator.decorators import ParseError from data_spec_validator.spec import DIGIT_STR, LIST_OF, ONE_OF, STR, Checker, dsv_feature from .utils import is_django_installed, make_request @@ -112,13 +114,15 @@ def decorated_func(self, req, *_args, **_kwargs): view = _View(request=fake_request) view.decorated_func(fake_request, **kwargs) - @parameterized.expand(['PUT', 'PATCH', 'DELETE']) - def test_query_params_with_data(self, method): + @parameterized.expand(itertools.product(['POST', 'PUT', 'PATCH', 'DELETE'], [True, False])) + def test_query_params_with_data(self, method, is_json): # arrange qs = 'q_a=3&q_b=true&d.o.t=dot&array[]=a1&array[]=a2&array[]=a3' payload = {'test_a': 'TEST A', 'test_f[]': [1, 2, 3]} - fake_request = make_request(self.request_class, method=method, data=payload, qs=qs) + if is_json: + payload = json.dumps(payload).encode('utf-8') + fake_request = make_request(self.request_class, method=method, data=payload, qs=qs, is_json=is_json) kwargs = {'test_b': 'TEST_B', 'test_c.d.e': 'TEST C.D.E'} @@ -141,6 +145,24 @@ def decorated_func(self, req, *_args, **_kwargs): view = _View(request=fake_request) assert view.decorated_func(fake_request, **kwargs) + @parameterized.expand(['POST', 'PUT', 'PATCH', 'DELETE']) + def test_query_params_with_data_in_invalid_json_format(self, method): + payload = 'invalid json data' + + fake_request = make_request(self.request_class, method=method, data=payload, is_json=True) + + class _ViewSpec: + pass + + class _View(View): + @dsv(_ViewSpec) + def decorated_func(self, req, *_args, **_kwargs): + return True + + view = _View(request=fake_request) + with self.assertRaises(ParseError): + assert view.decorated_func(fake_request) + def test_req_list_data_with_no_multirow_set(self): # arrange payload = [{'test_a': 'TEST A1'}, {'test_a': 'TEST A2'}, {'test_a': 'TEST A3'}] diff --git a/test/utils.py b/test/utils.py index a0d1955..eeb75aa 100644 --- a/test/utils.py +++ b/test/utils.py @@ -33,7 +33,7 @@ def is_drf_installed(): return True -def make_request(cls, path='/', method='GET', user=None, headers=None, data=None, qs=None): +def make_request(cls, path='/', method='GET', user=None, headers=None, data=None, qs=None, is_json=False): assert is_django_installed() from django.core.handlers.asgi import ASGIRequest @@ -62,11 +62,15 @@ def make_request(cls, path='/', method='GET', user=None, headers=None, data=None req.read() # trigger RawPostDataException and force DRF to load data from req.POST req.META.update( { - 'CONTENT_TYPE': 'application/x-www-form-urlencoded', + 'CONTENT_TYPE': 'application/json' if is_json else 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': len(str(data)), } ) - req.POST = data + if is_json: + req._body = data + req.POST = {} + else: + req.POST = data if is_drf_installed() and cls is not WSGIRequest and cls is not ASGIRequest: from rest_framework.parsers import FormParser From 461361b47cbd3480384115b65de80501e0f843f7 Mon Sep 17 00:00:00 2001 From: Hoss Date: Mon, 19 Jun 2023 15:37:45 +0800 Subject: [PATCH 2/2] bump version to 3.3.0 --- CHANGELOG.md | 2 +- data_spec_validator/__version__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cbee08..2e0f504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Changelog Changes: - DEPRECATIONS: The decorators support to Django version 1.x and 2.x is dropped. The minimum supported version is Django 3.0. - +- [feature] `dsv` decorator support loading payload from Django WSGIRequest for Content-Type: application/json 3.2.0 ----- diff --git a/data_spec_validator/__version__.py b/data_spec_validator/__version__.py index 573cf70..6a157dc 100644 --- a/data_spec_validator/__version__.py +++ b/data_spec_validator/__version__.py @@ -1 +1 @@ -__version__ = '3.2.0' +__version__ = '3.3.0'