1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +00:00

Fixed #29082 -- Allowed the test client to encode JSON request data.

This commit is contained in:
Nick Sarbicki 2018-02-05 10:22:24 +00:00 committed by Tim Graham
parent d968788b57
commit 47268242b0
7 changed files with 86 additions and 6 deletions

View File

@ -605,6 +605,7 @@ answer newbie questions, and generally made Django that much better:
Nick Pope <nick@nickpope.me.uk> Nick Pope <nick@nickpope.me.uk>
Nick Presta <nick@nickpresta.ca> Nick Presta <nick@nickpresta.ca>
Nick Sandford <nick.sandford@gmail.com> Nick Sandford <nick.sandford@gmail.com>
Nick Sarbicki <nick.a.sarbicki@gmail.com>
Niclas Olofsson <n@niclasolofsson.se> Niclas Olofsson <n@niclasolofsson.se>
Nicola Larosa <nico@teknico.net> Nicola Larosa <nico@teknico.net>
Nicolas Lara <nicolaslara@gmail.com> Nicolas Lara <nicolaslara@gmail.com>

View File

@ -13,6 +13,7 @@ from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit
from django.conf import settings from django.conf import settings
from django.core.handlers.base import BaseHandler from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.core.serializers.json import DjangoJSONEncoder
from django.core.signals import ( from django.core.signals import (
got_request_exception, request_finished, request_started, 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, 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. 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.defaults = defaults
self.cookies = SimpleCookie() self.cookies = SimpleCookie()
self.errors = BytesIO() self.errors = BytesIO()
@ -310,6 +312,14 @@ class RequestFactory:
charset = settings.DEFAULT_CHARSET charset = settings.DEFAULT_CHARSET
return force_bytes(data, encoding=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): def _get_path(self, parsed):
path = parsed.path path = parsed.path
# If there are parameters, add them # If there are parameters, add them
@ -332,7 +342,7 @@ class RequestFactory:
def post(self, path, data=None, content_type=MULTIPART_CONTENT, def post(self, path, data=None, content_type=MULTIPART_CONTENT,
secure=False, **extra): secure=False, **extra):
"""Construct a POST request.""" """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) post_data = self._encode_data(data, content_type)
return self.generic('POST', path, post_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', def put(self, path, data='', content_type='application/octet-stream',
secure=False, **extra): secure=False, **extra):
"""Construct a PUT request.""" """Construct a PUT request."""
data = self._encode_json(data, content_type)
return self.generic('PUT', path, data, content_type, return self.generic('PUT', path, data, content_type,
secure=secure, **extra) secure=secure, **extra)
def patch(self, path, data='', content_type='application/octet-stream', def patch(self, path, data='', content_type='application/octet-stream',
secure=False, **extra): secure=False, **extra):
"""Construct a PATCH request.""" """Construct a PATCH request."""
data = self._encode_json(data, content_type)
return self.generic('PATCH', path, data, content_type, return self.generic('PATCH', path, data, content_type,
secure=secure, **extra) secure=secure, **extra)
def delete(self, path, data='', content_type='application/octet-stream', def delete(self, path, data='', content_type='application/octet-stream',
secure=False, **extra): secure=False, **extra):
"""Construct a DELETE request.""" """Construct a DELETE request."""
data = self._encode_json(data, content_type)
return self.generic('DELETE', path, data, content_type, return self.generic('DELETE', path, data, content_type,
secure=secure, **extra) secure=secure, **extra)

View File

@ -208,6 +208,10 @@ Tests
* Added test :class:`~django.test.Client` support for 307 and 308 redirects. * 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 URLs
~~~~ ~~~~

View File

@ -109,7 +109,7 @@ Making requests
Use the ``django.test.Client`` class to make 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 It requires no arguments at time of construction. However, you can use
keywords arguments to specify some default headers. For example, this will 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 The ``enforce_csrf_checks`` argument can be used to test CSRF
protection (see above). 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 Once you have a ``Client`` instance, you can call any of the following
methods: methods:
@ -206,9 +213,23 @@ Use the ``django.test.Client`` class to make requests.
name=fred&passwd=secret name=fred&passwd=secret
If you provide ``content_type`` (e.g. :mimetype:`text/xml` for an XML If you provide ``content_type`` as :mimetype:`application/json`, a
payload), the contents of ``data`` will be sent as-is in the POST ``data`` dictionary is serialized using :func:`json.dumps` with
request, using ``content_type`` in the HTTP ``Content-Type`` header. :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 If you don't provide a value for ``content_type``, the values in
``data`` will be transmitted with a content type of ``data`` will be transmitted with a content type of

View File

@ -21,6 +21,7 @@ rather than the HTML rendered to the end-user.
""" """
import itertools import itertools
import tempfile import tempfile
from unittest import mock
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import mail from django.core import mail
@ -86,6 +87,31 @@ class ClientTest(TestCase):
self.assertEqual(response.templates[0].name, 'POST Template') self.assertEqual(response.templates[0].name, 'POST Template')
self.assertContains(response, 'Data received') 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): def test_trace(self):
"""TRACE a view""" """TRACE a view"""
response = self.client.trace('/trace_view/') response = self.client.trace('/trace_view/')

View File

@ -25,6 +25,7 @@ urlpatterns = [
url(r'^form_view/$', views.form_view), url(r'^form_view/$', views.form_view),
url(r'^form_view_with_template/$', views.form_view_with_template), url(r'^form_view_with_template/$', views.form_view_with_template),
url(r'^formset_view/$', views.formset_view), 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_view/$', views.login_protected_view),
url(r'^login_protected_method_view/$', views.login_protected_method_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), url(r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect),

View File

@ -1,3 +1,4 @@
import json
from urllib.parse import urlencode from urllib.parse import urlencode
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
@ -73,7 +74,20 @@ def post_view(request):
else: else:
t = Template('Viewing GET page.', name='Empty GET Template') t = Template('Viewing GET page.', name='Empty GET Template')
c = Context() 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)) return HttpResponse(t.render(c))