Fixed #17371 -- Made the test client more flexible

The OPTIONS, PUT and DELETE methods no longer apply arbitrary
data encoding (in the query string or in the request body).
This commit is contained in:
Aymeric Augustin 2012-05-25 19:03:15 +02:00
parent 323b414441
commit e73838b6dd
5 changed files with 93 additions and 81 deletions

View File

@ -155,7 +155,6 @@ def encode_file(boundary, key, file):
] ]
class RequestFactory(object): class RequestFactory(object):
""" """
Class that lets you create mock Request objects for use in testing. Class that lets you create mock Request objects for use in testing.
@ -227,7 +226,7 @@ class RequestFactory(object):
return urllib.unquote(parsed[2]) return urllib.unquote(parsed[2])
def get(self, path, data={}, **extra): def get(self, path, data={}, **extra):
"Construct a GET request" "Construct a GET request."
parsed = urlparse(path) parsed = urlparse(path)
r = { r = {
@ -270,49 +269,39 @@ class RequestFactory(object):
r.update(extra) r.update(extra)
return self.request(**r) return self.request(**r)
def options(self, path, data={}, **extra): def options(self, path, data='', content_type='application/octet-stream',
"Constrict an OPTIONS request" **extra):
"Construct an OPTIONS request."
return self.generic('OPTIONS', path, data, content_type, **extra)
parsed = urlparse(path) def put(self, path, data='', content_type='application/octet-stream',
r = {
'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
'REQUEST_METHOD': 'OPTIONS',
}
r.update(extra)
return self.request(**r)
def put(self, path, data={}, content_type=MULTIPART_CONTENT,
**extra): **extra):
"Construct a PUT request." "Construct a PUT request."
return self.generic('PUT', path, data, content_type, **extra)
put_data = self._encode_data(data, content_type) def delete(self, path, data='', content_type='application/octet-stream',
**extra):
"Construct a DELETE request."
return self.generic('DELETE', path, data, content_type, **extra)
def generic(self, method, path,
data='', content_type='application/octet-stream', **extra):
parsed = urlparse(path) parsed = urlparse(path)
data = smart_str(data, settings.DEFAULT_CHARSET)
r = { r = {
'CONTENT_LENGTH': len(put_data),
'CONTENT_TYPE': content_type,
'PATH_INFO': self._get_path(parsed), 'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': parsed[4], 'QUERY_STRING': parsed[4],
'REQUEST_METHOD': 'PUT', 'REQUEST_METHOD': method,
'wsgi.input': FakePayload(put_data),
} }
if data:
r.update({
'CONTENT_LENGTH': len(data),
'CONTENT_TYPE': content_type,
'wsgi.input': FakePayload(data),
})
r.update(extra) r.update(extra)
return self.request(**r) return self.request(**r)
def delete(self, path, data={}, **extra):
"Construct a DELETE request."
parsed = urlparse(path)
r = {
'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
'REQUEST_METHOD': 'DELETE',
}
r.update(extra)
return self.request(**r)
class Client(RequestFactory): class Client(RequestFactory):
""" """
A class that can act as a client for testing purposes. A class that can act as a client for testing purposes.
@ -445,30 +434,35 @@ class Client(RequestFactory):
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
return response return response
def options(self, path, data={}, follow=False, **extra): def options(self, path, data='', content_type='application/octet-stream',
follow=False, **extra):
""" """
Request a response from the server using OPTIONS. Request a response from the server using OPTIONS.
""" """
response = super(Client, self).options(path, data=data, **extra) response = super(Client, self).options(path,
data=data, content_type=content_type, **extra)
if follow: if follow:
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
return response return response
def put(self, path, data={}, content_type=MULTIPART_CONTENT, def put(self, path, data='', content_type='application/octet-stream',
follow=False, **extra): follow=False, **extra):
""" """
Send a resource to the server using PUT. Send a resource to the server using PUT.
""" """
response = super(Client, self).put(path, data=data, content_type=content_type, **extra) response = super(Client, self).put(path,
data=data, content_type=content_type, **extra)
if follow: if follow:
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
return response return response
def delete(self, path, data={}, follow=False, **extra): def delete(self, path, data='', content_type='application/octet-stream',
follow=False, **extra):
""" """
Send a DELETE request to the server. Send a DELETE request to the server.
""" """
response = super(Client, self).delete(path, data=data, **extra) response = super(Client, self).delete(path,
data=data, content_type=content_type, **extra)
if follow: if follow:
response = self._handle_redirects(response, **extra) response = self._handle_redirects(response, **extra)
return response return response

View File

@ -56,7 +56,7 @@ previously loaded. For example, with the tutorial's models::
True True
In Django 1.5, the third line no longer triggers a new SQL query to fetch In Django 1.5, the third line no longer triggers a new SQL query to fetch
``first_choice.poll``; it was set when by the second line. ``first_choice.poll``; it was set by the second line.
For one-to-one relationships, both sides can be cached. For many-to-one For one-to-one relationships, both sides can be cached. For many-to-one
relationships, only the single side of the relationship can be cached. This relationships, only the single side of the relationship can be cached. This
@ -101,6 +101,26 @@ year|date:"Y" }}``.
``next_year`` and ``previous_year`` were also added in the context. They are ``next_year`` and ``previous_year`` were also added in the context. They are
calculated according to ``allow_empty`` and ``allow_future``. calculated according to ``allow_empty`` and ``allow_future``.
OPTIONS, PUT and DELETE requests in the test client
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unlike GET and POST, these HTTP methods aren't implemented by web browsers.
Rather, they're used in APIs, which transfer data in various formats such as
JSON or XML. Since such requests may contain arbitrary data, Django doesn't
attempt to decode their body.
However, the test client used to build a query string for OPTIONS and DELETE
requests like for GET, and a request body for PUT requests like for POST. This
encoding was arbitrary and inconsistent with Django's behavior when it
receives the requests, so it was removed in Django 1.5.
If you were using the ``data`` parameter in an OPTIONS or a DELETE request,
you must convert it to a query string and append it to the ``path`` parameter.
If you were using the ``data`` parameter in a PUT request without a
``content_type``, you must encode your data before passing it to the test
client and set the ``content_type`` argument.
Features deprecated in 1.5 Features deprecated in 1.5
========================== ==========================

View File

@ -805,45 +805,56 @@ arguments at time of construction:
.. method:: Client.head(path, data={}, follow=False, **extra) .. method:: Client.head(path, data={}, follow=False, **extra)
Makes a HEAD request on the provided ``path`` and returns a ``Response`` Makes a HEAD request on the provided ``path`` and returns a
object. Useful for testing RESTful interfaces. Acts just like ``Response`` object. This method works just like :meth:`Client.get`,
:meth:`Client.get` except it does not return a message body. including the ``follow`` and ``extra`` arguments, except it does not
return a message body.
If you set ``follow`` to ``True`` the client will follow any redirects .. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, **extra)
and a ``redirect_chain`` attribute will be set in the response object
containing tuples of the intermediate urls and status codes.
.. method:: Client.options(path, data={}, follow=False, **extra)
Makes an OPTIONS request on the provided ``path`` and returns a Makes an OPTIONS request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``Response`` object. Useful for testing RESTful interfaces.
If you set ``follow`` to ``True`` the client will follow any redirects When ``data`` is provided, it is used as the request body, and
and a ``redirect_chain`` attribute will be set in the response object a ``Content-Type`` header is set to ``content_type``.
containing tuples of the intermediate urls and status codes.
The ``extra`` argument acts the same as for :meth:`Client.get`. .. versionchanged:: 1.5
:meth:`Client.options` used to process ``data`` like
:meth:`Client.get`.
.. method:: Client.put(path, data={}, content_type=MULTIPART_CONTENT, follow=False, **extra) The ``follow`` and ``extra`` arguments act the same as for
:meth:`Client.get`.
.. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, **extra)
Makes a PUT request on the provided ``path`` and returns a Makes a PUT request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. Acts just ``Response`` object. Useful for testing RESTful interfaces.
like :meth:`Client.post` except with the PUT request method.
If you set ``follow`` to ``True`` the client will follow any redirects When ``data`` is provided, it is used as the request body, and
and a ``redirect_chain`` attribute will be set in the response object a ``Content-Type`` header is set to ``content_type``.
containing tuples of the intermediate urls and status codes.
.. method:: Client.delete(path, follow=False, **extra) .. versionchanged:: 1.5
:meth:`Client.put` used to process ``data`` like
:meth:`Client.post`.
The ``follow`` and ``extra`` arguments act the same as for
:meth:`Client.get`.
.. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, **extra)
Makes an DELETE request on the provided ``path`` and returns a Makes an DELETE request on the provided ``path`` and returns a
``Response`` object. Useful for testing RESTful interfaces. ``Response`` object. Useful for testing RESTful interfaces.
If you set ``follow`` to ``True`` the client will follow any redirects When ``data`` is provided, it is used as the request body, and
and a ``redirect_chain`` attribute will be set in the response object a ``Content-Type`` header is set to ``content_type``.
containing tuples of the intermediate urls and status codes.
.. versionchanged:: 1.5
:meth:`Client.delete` used to process ``data`` like
:meth:`Client.get`.
The ``follow`` and ``extra`` arguments act the same as for
:meth:`Client.get`.
The ``extra`` argument acts the same as for :meth:`Client.get`.
.. method:: Client.login(**credentials) .. method:: Client.login(**credentials)

View File

@ -63,10 +63,10 @@ class ConditionalGet(TestCase):
def testIfMatch(self): def testIfMatch(self):
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG
response = self.client.put('/condition/etag/', {'data': ''}) response = self.client.put('/condition/etag/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG
response = self.client.put('/condition/etag/', {'data': ''}) response = self.client.put('/condition/etag/')
self.assertEqual(response.status_code, 412) self.assertEqual(response.status_code, 412)
def testBothHeaders(self): def testBothHeaders(self):

View File

@ -347,7 +347,7 @@ class AssertRedirectsTests(TestCase):
def test_redirect_chain_options(self): def test_redirect_chain_options(self):
"A redirect chain will be followed from an initial OPTIONS request" "A redirect chain will be followed from an initial OPTIONS request"
response = self.client.options('/test_client_regress/redirects/', response = self.client.options('/test_client_regress/redirects/',
{'nothing': 'to_send'}, follow=True) follow=True)
self.assertRedirects(response, self.assertRedirects(response,
'/test_client_regress/no_template_view/', 301, 200) '/test_client_regress/no_template_view/', 301, 200)
self.assertEqual(len(response.redirect_chain), 3) self.assertEqual(len(response.redirect_chain), 3)
@ -355,7 +355,7 @@ class AssertRedirectsTests(TestCase):
def test_redirect_chain_put(self): def test_redirect_chain_put(self):
"A redirect chain will be followed from an initial PUT request" "A redirect chain will be followed from an initial PUT request"
response = self.client.put('/test_client_regress/redirects/', response = self.client.put('/test_client_regress/redirects/',
{'nothing': 'to_send'}, follow=True) follow=True)
self.assertRedirects(response, self.assertRedirects(response,
'/test_client_regress/no_template_view/', 301, 200) '/test_client_regress/no_template_view/', 301, 200)
self.assertEqual(len(response.redirect_chain), 3) self.assertEqual(len(response.redirect_chain), 3)
@ -363,7 +363,7 @@ class AssertRedirectsTests(TestCase):
def test_redirect_chain_delete(self): def test_redirect_chain_delete(self):
"A redirect chain will be followed from an initial DELETE request" "A redirect chain will be followed from an initial DELETE request"
response = self.client.delete('/test_client_regress/redirects/', response = self.client.delete('/test_client_regress/redirects/',
{'nothing': 'to_send'}, follow=True) follow=True)
self.assertRedirects(response, self.assertRedirects(response,
'/test_client_regress/no_template_view/', 301, 200) '/test_client_regress/no_template_view/', 301, 200)
self.assertEqual(len(response.redirect_chain), 3) self.assertEqual(len(response.redirect_chain), 3)
@ -809,8 +809,7 @@ class RequestMethodStringDataTests(TestCase):
class QueryStringTests(TestCase): class QueryStringTests(TestCase):
def test_get_like_requests(self): def test_get_like_requests(self):
# See: https://code.djangoproject.com/ticket/10571. # See: https://code.djangoproject.com/ticket/10571.
# Removed 'put' and 'delete' here as they are 'GET-like requests' for method_name in ('get', 'head'):
for method_name in ('get','head','options'):
# A GET-like request can pass a query string as data # A GET-like request can pass a query string as data
method = getattr(self.client, method_name) method = getattr(self.client, method_name)
response = method("/test_client_regress/request_data/", data={'foo':'whiz'}) response = method("/test_client_regress/request_data/", data={'foo':'whiz'})
@ -867,9 +866,6 @@ class UnicodePayloadTests(TestCase):
response = self.client.post("/test_client_regress/parse_unicode_json/", json, response = self.client.post("/test_client_regress/parse_unicode_json/", json,
content_type="application/json") content_type="application/json")
self.assertEqual(response.content, json) self.assertEqual(response.content, json)
response = self.client.put("/test_client_regress/parse_unicode_json/", json,
content_type="application/json")
self.assertEqual(response.content, json)
def test_unicode_payload_utf8(self): def test_unicode_payload_utf8(self):
"A non-ASCII unicode data encoded as UTF-8 can be POSTed" "A non-ASCII unicode data encoded as UTF-8 can be POSTed"
@ -878,9 +874,6 @@ class UnicodePayloadTests(TestCase):
response = self.client.post("/test_client_regress/parse_unicode_json/", json, response = self.client.post("/test_client_regress/parse_unicode_json/", json,
content_type="application/json; charset=utf-8") content_type="application/json; charset=utf-8")
self.assertEqual(response.content, json.encode('utf-8')) self.assertEqual(response.content, json.encode('utf-8'))
response = self.client.put("/test_client_regress/parse_unicode_json/", json,
content_type="application/json; charset=utf-8")
self.assertEqual(response.content, json.encode('utf-8'))
def test_unicode_payload_utf16(self): def test_unicode_payload_utf16(self):
"A non-ASCII unicode data encoded as UTF-16 can be POSTed" "A non-ASCII unicode data encoded as UTF-16 can be POSTed"
@ -889,9 +882,6 @@ class UnicodePayloadTests(TestCase):
response = self.client.post("/test_client_regress/parse_unicode_json/", json, response = self.client.post("/test_client_regress/parse_unicode_json/", json,
content_type="application/json; charset=utf-16") content_type="application/json; charset=utf-16")
self.assertEqual(response.content, json.encode('utf-16')) self.assertEqual(response.content, json.encode('utf-16'))
response = self.client.put("/test_client_regress/parse_unicode_json/", json,
content_type="application/json; charset=utf-16")
self.assertEqual(response.content, json.encode('utf-16'))
def test_unicode_payload_non_utf(self): def test_unicode_payload_non_utf(self):
"A non-ASCII unicode data as a non-UTF based encoding can be POSTed" "A non-ASCII unicode data as a non-UTF based encoding can be POSTed"
@ -900,9 +890,6 @@ class UnicodePayloadTests(TestCase):
response = self.client.post("/test_client_regress/parse_unicode_json/", json, response = self.client.post("/test_client_regress/parse_unicode_json/", json,
content_type="application/json; charset=koi8-r") content_type="application/json; charset=koi8-r")
self.assertEqual(response.content, json.encode('koi8-r')) self.assertEqual(response.content, json.encode('koi8-r'))
response = self.client.put("/test_client_regress/parse_unicode_json/", json,
content_type="application/json; charset=koi8-r")
self.assertEqual(response.content, json.encode('koi8-r'))
class DummyFile(object): class DummyFile(object):
def __init__(self, filename): def __init__(self, filename):