Fixed #29082 -- Allowed the test client to encode JSON request data.
This commit is contained in:
parent
d968788b57
commit
47268242b0
1
AUTHORS
1
AUTHORS
|
@ -605,6 +605,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Nick Pope <nick@nickpope.me.uk>
|
||||
Nick Presta <nick@nickpresta.ca>
|
||||
Nick Sandford <nick.sandford@gmail.com>
|
||||
Nick Sarbicki <nick.a.sarbicki@gmail.com>
|
||||
Niclas Olofsson <n@niclasolofsson.se>
|
||||
Nicola Larosa <nico@teknico.net>
|
||||
Nicolas Lara <nicolaslara@gmail.com>
|
||||
|
|
|
@ -13,6 +13,7 @@ from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit
|
|||
from django.conf import settings
|
||||
from django.core.handlers.base import BaseHandler
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.core.signals import (
|
||||
got_request_exception, request_finished, request_started,
|
||||
)
|
||||
|
@ -261,7 +262,8 @@ class RequestFactory:
|
|||
Once you have a request object you can pass it to any view function,
|
||||
just as if that view had been hooked up using a URLconf.
|
||||
"""
|
||||
def __init__(self, **defaults):
|
||||
def __init__(self, *, json_encoder=DjangoJSONEncoder, **defaults):
|
||||
self.json_encoder = json_encoder
|
||||
self.defaults = defaults
|
||||
self.cookies = SimpleCookie()
|
||||
self.errors = BytesIO()
|
||||
|
@ -310,6 +312,14 @@ class RequestFactory:
|
|||
charset = settings.DEFAULT_CHARSET
|
||||
return force_bytes(data, encoding=charset)
|
||||
|
||||
def _encode_json(self, data, content_type):
|
||||
"""
|
||||
Return encoded JSON if data is a dict and content_type is
|
||||
application/json.
|
||||
"""
|
||||
should_encode = JSON_CONTENT_TYPE_RE.match(content_type) and isinstance(data, dict)
|
||||
return json.dumps(data, cls=self.json_encoder) if should_encode else data
|
||||
|
||||
def _get_path(self, parsed):
|
||||
path = parsed.path
|
||||
# If there are parameters, add them
|
||||
|
@ -332,7 +342,7 @@ class RequestFactory:
|
|||
def post(self, path, data=None, content_type=MULTIPART_CONTENT,
|
||||
secure=False, **extra):
|
||||
"""Construct a POST request."""
|
||||
data = {} if data is None else data
|
||||
data = self._encode_json({} if data is None else data, content_type)
|
||||
post_data = self._encode_data(data, content_type)
|
||||
|
||||
return self.generic('POST', path, post_data, content_type,
|
||||
|
@ -359,18 +369,21 @@ class RequestFactory:
|
|||
def put(self, path, data='', content_type='application/octet-stream',
|
||||
secure=False, **extra):
|
||||
"""Construct a PUT request."""
|
||||
data = self._encode_json(data, content_type)
|
||||
return self.generic('PUT', path, data, content_type,
|
||||
secure=secure, **extra)
|
||||
|
||||
def patch(self, path, data='', content_type='application/octet-stream',
|
||||
secure=False, **extra):
|
||||
"""Construct a PATCH request."""
|
||||
data = self._encode_json(data, content_type)
|
||||
return self.generic('PATCH', path, data, content_type,
|
||||
secure=secure, **extra)
|
||||
|
||||
def delete(self, path, data='', content_type='application/octet-stream',
|
||||
secure=False, **extra):
|
||||
"""Construct a DELETE request."""
|
||||
data = self._encode_json(data, content_type)
|
||||
return self.generic('DELETE', path, data, content_type,
|
||||
secure=secure, **extra)
|
||||
|
||||
|
|
|
@ -208,6 +208,10 @@ Tests
|
|||
|
||||
* Added test :class:`~django.test.Client` support for 307 and 308 redirects.
|
||||
|
||||
* The test :class:`~django.test.Client` now serializes a request data
|
||||
dictionary as JSON if ``content_type='application/json'``. You can customize
|
||||
the JSON encoder with test client's ``json_encoder`` parameter.
|
||||
|
||||
URLs
|
||||
~~~~
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ Making requests
|
|||
|
||||
Use the ``django.test.Client`` class to make requests.
|
||||
|
||||
.. class:: Client(enforce_csrf_checks=False, **defaults)
|
||||
.. class:: Client(enforce_csrf_checks=False, json_encoder=DjangoJSONEncoder, **defaults)
|
||||
|
||||
It requires no arguments at time of construction. However, you can use
|
||||
keywords arguments to specify some default headers. For example, this will
|
||||
|
@ -125,6 +125,13 @@ Use the ``django.test.Client`` class to make requests.
|
|||
The ``enforce_csrf_checks`` argument can be used to test CSRF
|
||||
protection (see above).
|
||||
|
||||
The ``json_encoder`` argument allows setting a custom JSON encoder for
|
||||
the JSON serialization that's described in :meth:`post`.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
The ``json_encoder`` argument was added.
|
||||
|
||||
Once you have a ``Client`` instance, you can call any of the following
|
||||
methods:
|
||||
|
||||
|
@ -206,9 +213,23 @@ Use the ``django.test.Client`` class to make requests.
|
|||
|
||||
name=fred&passwd=secret
|
||||
|
||||
If you provide ``content_type`` (e.g. :mimetype:`text/xml` for an XML
|
||||
payload), the contents of ``data`` will be sent as-is in the POST
|
||||
request, using ``content_type`` in the HTTP ``Content-Type`` header.
|
||||
If you provide ``content_type`` as :mimetype:`application/json`, a
|
||||
``data`` dictionary is serialized using :func:`json.dumps` with
|
||||
:class:`~django.core.serializers.json.DjangoJSONEncoder`. You can
|
||||
change the encoder by providing a ``json_encoder`` argument to
|
||||
:class:`Client`. This serialization also happens for :meth:`put`,
|
||||
:meth:`patch`, and :meth:`delete` requests.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
The JSON serialization described above was added. In older versions,
|
||||
you can call :func:`json.dumps` on ``data`` before passing it to
|
||||
``post()`` to achieve the same thing.
|
||||
|
||||
If you provide any other ``content_type`` (e.g. :mimetype:`text/xml`
|
||||
for an XML payload), the contents of ``data`` are sent as-is in the
|
||||
POST request, using ``content_type`` in the HTTP ``Content-Type``
|
||||
header.
|
||||
|
||||
If you don't provide a value for ``content_type``, the values in
|
||||
``data`` will be transmitted with a content type of
|
||||
|
|
|
@ -21,6 +21,7 @@ rather than the HTML rendered to the end-user.
|
|||
"""
|
||||
import itertools
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import mail
|
||||
|
@ -86,6 +87,31 @@ class ClientTest(TestCase):
|
|||
self.assertEqual(response.templates[0].name, 'POST Template')
|
||||
self.assertContains(response, 'Data received')
|
||||
|
||||
def test_json_serialization(self):
|
||||
"""The test client serializes JSON data."""
|
||||
methods = ('post', 'put', 'patch', 'delete')
|
||||
for method in methods:
|
||||
with self.subTest(method=method):
|
||||
client_method = getattr(self.client, method)
|
||||
method_name = method.upper()
|
||||
response = client_method('/json_view/', {'value': 37}, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.context['data'], 37)
|
||||
self.assertContains(response, 'Viewing %s page.' % method_name)
|
||||
|
||||
def test_json_encoder_argument(self):
|
||||
"""The test Client accepts a json_encoder."""
|
||||
mock_encoder = mock.MagicMock()
|
||||
mock_encoding = mock.MagicMock()
|
||||
mock_encoder.return_value = mock_encoding
|
||||
mock_encoding.encode.return_value = '{"value": 37}'
|
||||
|
||||
client = self.client_class(json_encoder=mock_encoder)
|
||||
# Vendored tree JSON content types are accepted.
|
||||
client.post('/json_view/', {'value': 37}, content_type='application/vnd.api+json')
|
||||
self.assertTrue(mock_encoder.called)
|
||||
self.assertTrue(mock_encoding.encode.called)
|
||||
|
||||
def test_trace(self):
|
||||
"""TRACE a view"""
|
||||
response = self.client.trace('/trace_view/')
|
||||
|
|
|
@ -25,6 +25,7 @@ urlpatterns = [
|
|||
url(r'^form_view/$', views.form_view),
|
||||
url(r'^form_view_with_template/$', views.form_view_with_template),
|
||||
url(r'^formset_view/$', views.formset_view),
|
||||
url(r'^json_view/$', views.json_view),
|
||||
url(r'^login_protected_view/$', views.login_protected_view),
|
||||
url(r'^login_protected_method_view/$', views.login_protected_method_view),
|
||||
url(r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from urllib.parse import urlencode
|
||||
from xml.dom.minidom import parseString
|
||||
|
||||
|
@ -73,7 +74,20 @@ def post_view(request):
|
|||
else:
|
||||
t = Template('Viewing GET page.', name='Empty GET Template')
|
||||
c = Context()
|
||||
return HttpResponse(t.render(c))
|
||||
|
||||
|
||||
def json_view(request):
|
||||
"""
|
||||
A view that expects a request with the header 'application/json' and JSON
|
||||
data with a key named 'value'.
|
||||
"""
|
||||
if request.META.get('CONTENT_TYPE') != 'application/json':
|
||||
return HttpResponse()
|
||||
|
||||
t = Template('Viewing {} page. With data {{ data }}.'.format(request.method))
|
||||
data = json.loads(request.body.decode('utf-8'))
|
||||
c = Context({'data': data['value']})
|
||||
return HttpResponse(t.render(c))
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue