Kaydet (Commit) 47268242 authored tarafından Nick Sarbicki's avatar Nick Sarbicki Kaydeden (comit) Tim Graham

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

üst d968788b
...@@ -605,6 +605,7 @@ answer newbie questions, and generally made Django that much better: ...@@ -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>
......
...@@ -13,6 +13,7 @@ from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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)
......
...@@ -208,6 +208,10 @@ Tests ...@@ -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
~~~~ ~~~~
......
...@@ -109,7 +109,7 @@ Making requests ...@@ -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. ...@@ -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. ...@@ -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
......
...@@ -21,6 +21,7 @@ rather than the HTML rendered to the end-user. ...@@ -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): ...@@ -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/')
......
...@@ -25,6 +25,7 @@ urlpatterns = [ ...@@ -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),
......
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): ...@@ -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))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment