diff --git a/django/test/client.py b/django/test/client.py
index c1764e2..3cc6a6e 100644
a
|
b
|
def encode_file(boundary, key, file):
|
155 | 155 | ] |
156 | 156 | |
157 | 157 | |
158 | | |
159 | 158 | class RequestFactory(object): |
160 | 159 | """ |
161 | 160 | Class that lets you create mock Request objects for use in testing. |
… |
… |
class RequestFactory(object):
|
227 | 226 | return urllib.unquote(parsed[2]) |
228 | 227 | |
229 | 228 | def get(self, path, data={}, **extra): |
230 | | "Construct a GET request" |
| 229 | "Construct a GET request." |
231 | 230 | |
232 | 231 | parsed = urlparse(path) |
233 | 232 | r = { |
… |
… |
class RequestFactory(object):
|
270 | 269 | r.update(extra) |
271 | 270 | return self.request(**r) |
272 | 271 | |
273 | | def options(self, path, data={}, **extra): |
274 | | "Constrict an OPTIONS request" |
275 | | |
276 | | parsed = urlparse(path) |
277 | | r = { |
278 | | 'PATH_INFO': self._get_path(parsed), |
279 | | 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], |
280 | | 'REQUEST_METHOD': 'OPTIONS', |
281 | | } |
282 | | r.update(extra) |
283 | | return self.request(**r) |
| 272 | def options(self, path, data='', content_type='application/octet-stream', |
| 273 | **extra): |
| 274 | "Construct an OPTIONS request." |
| 275 | return self.generic('OPTIONS', path, data, content_type, **extra) |
284 | 276 | |
285 | | def put(self, path, data={}, content_type=MULTIPART_CONTENT, |
| 277 | def put(self, path, data='', content_type='application/octet-stream', |
286 | 278 | **extra): |
287 | 279 | "Construct a PUT request." |
| 280 | return self.generic('PUT', path, data, content_type, **extra) |
288 | 281 | |
289 | | put_data = self._encode_data(data, content_type) |
| 282 | def delete(self, path, data='', content_type='application/octet-stream', |
| 283 | **extra): |
| 284 | "Construct a DELETE request." |
| 285 | return self.generic('DELETE', path, data, content_type, **extra) |
290 | 286 | |
| 287 | def generic(self, method, path, |
| 288 | data='', content_type='application/octet-stream', **extra): |
291 | 289 | parsed = urlparse(path) |
| 290 | data = smart_str(data, settings.DEFAULT_CHARSET) |
292 | 291 | r = { |
293 | | 'CONTENT_LENGTH': len(put_data), |
294 | | 'CONTENT_TYPE': content_type, |
295 | 292 | 'PATH_INFO': self._get_path(parsed), |
296 | 293 | 'QUERY_STRING': parsed[4], |
297 | | 'REQUEST_METHOD': 'PUT', |
298 | | 'wsgi.input': FakePayload(put_data), |
| 294 | 'REQUEST_METHOD': method, |
299 | 295 | } |
| 296 | if data: |
| 297 | r.update({ |
| 298 | 'CONTENT_LENGTH': len(data), |
| 299 | 'CONTENT_TYPE': content_type, |
| 300 | 'wsgi.input': FakePayload(data), |
| 301 | }) |
300 | 302 | r.update(extra) |
301 | 303 | return self.request(**r) |
302 | 304 | |
303 | | def delete(self, path, data={}, **extra): |
304 | | "Construct a DELETE request." |
305 | | |
306 | | parsed = urlparse(path) |
307 | | r = { |
308 | | 'PATH_INFO': self._get_path(parsed), |
309 | | 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], |
310 | | 'REQUEST_METHOD': 'DELETE', |
311 | | } |
312 | | r.update(extra) |
313 | | return self.request(**r) |
314 | | |
315 | | |
316 | 305 | class Client(RequestFactory): |
317 | 306 | """ |
318 | 307 | A class that can act as a client for testing purposes. |
… |
… |
class Client(RequestFactory):
|
445 | 434 | response = self._handle_redirects(response, **extra) |
446 | 435 | return response |
447 | 436 | |
448 | | def options(self, path, data={}, follow=False, **extra): |
| 437 | def options(self, path, data='', content_type='application/octet-stream', |
| 438 | follow=False, **extra): |
449 | 439 | """ |
450 | 440 | Request a response from the server using OPTIONS. |
451 | 441 | """ |
452 | | response = super(Client, self).options(path, data=data, **extra) |
| 442 | response = super(Client, self).options(path, |
| 443 | data=data, content_type=content_type, **extra) |
453 | 444 | if follow: |
454 | 445 | response = self._handle_redirects(response, **extra) |
455 | 446 | return response |
456 | 447 | |
457 | | def put(self, path, data={}, content_type=MULTIPART_CONTENT, |
| 448 | def put(self, path, data='', content_type='application/octet-stream', |
458 | 449 | follow=False, **extra): |
459 | 450 | """ |
460 | 451 | Send a resource to the server using PUT. |
461 | 452 | """ |
462 | | response = super(Client, self).put(path, data=data, content_type=content_type, **extra) |
| 453 | response = super(Client, self).put(path, |
| 454 | data=data, content_type=content_type, **extra) |
463 | 455 | if follow: |
464 | 456 | response = self._handle_redirects(response, **extra) |
465 | 457 | return response |
466 | 458 | |
467 | | def delete(self, path, data={}, follow=False, **extra): |
| 459 | def delete(self, path, data='', content_type='application/octet-stream', |
| 460 | follow=False, **extra): |
468 | 461 | """ |
469 | 462 | Send a DELETE request to the server. |
470 | 463 | """ |
471 | | response = super(Client, self).delete(path, data=data, **extra) |
| 464 | response = super(Client, self).delete(path, |
| 465 | data=data, content_type=content_type, **extra) |
472 | 466 | if follow: |
473 | 467 | response = self._handle_redirects(response, **extra) |
474 | 468 | return response |
diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt
index 51e64bd..68cf885 100644
a
|
b
|
Backwards incompatible changes in 1.5
|
71 | 71 | deprecation timeline for a given feature, its removal may appear as a |
72 | 72 | backwards incompatible change. |
73 | 73 | |
| 74 | OPTIONS, PUT and DELETE requests in the test client |
| 75 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 76 | |
| 77 | Unlike GET and POST, these HTTP methods aren't implemented by web browsers. |
| 78 | Rather, they're used in APIs, which transfer data in various formats such as |
| 79 | JSON or XML. Since such requests may contain arbitrary data, Django doesn't |
| 80 | attempt to decode their body. |
| 81 | |
| 82 | However, the test client used to build a query string for OPTIONS and DELETE |
| 83 | requests like for GET, and a request body for PUT requests like for POST. This |
| 84 | encoding was arbitrary and inconsistent with Django's behavior when it |
| 85 | receives the requests, so it was removed in Django 1.5. |
| 86 | |
| 87 | If you were using the ``data`` parameter in an OPTIONS or a DELETE request, |
| 88 | you must convert it to a query string and append it to the ``path`` parameter. |
| 89 | |
| 90 | If you were using the ``data`` parameter in a PUT request without a |
| 91 | ``content_type``, you must encode your data before passing it to the test |
| 92 | client and set the ``content_type`` argument. |
| 93 | |
74 | 94 | Features deprecated in 1.5 |
75 | 95 | ========================== |
76 | 96 | |
diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
index d5ccc2d..98ee9ed 100644
a
|
b
|
arguments at time of construction:
|
805 | 805 | |
806 | 806 | .. method:: Client.head(path, data={}, follow=False, **extra) |
807 | 807 | |
808 | | Makes a HEAD request on the provided ``path`` and returns a ``Response`` |
809 | | object. Useful for testing RESTful interfaces. Acts just like |
810 | | :meth:`Client.get` except it does not return a message body. |
| 808 | Makes a HEAD request on the provided ``path`` and returns a |
| 809 | ``Response`` object. This method works just like :meth:`Client.get`, |
| 810 | including the ``follow`` and ``extra`` arguments, except it does not |
| 811 | return a message body. |
811 | 812 | |
812 | | If you set ``follow`` to ``True`` the client will follow any redirects |
813 | | and a ``redirect_chain`` attribute will be set in the response object |
814 | | containing tuples of the intermediate urls and status codes. |
815 | | |
816 | | .. method:: Client.options(path, data={}, follow=False, **extra) |
| 813 | .. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, **extra) |
817 | 814 | |
818 | 815 | Makes an OPTIONS request on the provided ``path`` and returns a |
819 | 816 | ``Response`` object. Useful for testing RESTful interfaces. |
820 | 817 | |
821 | | If you set ``follow`` to ``True`` the client will follow any redirects |
822 | | and a ``redirect_chain`` attribute will be set in the response object |
823 | | containing tuples of the intermediate urls and status codes. |
| 818 | When ``data`` is provided, it is used as the request body, and |
| 819 | a ``Content-Type`` header is set to ``content_type``. |
824 | 820 | |
825 | | The ``extra`` argument acts the same as for :meth:`Client.get`. |
| 821 | .. versionchanged:: 1.5 |
| 822 | ``Client.options`` used to process ``data`` like Client.get``. |
826 | 823 | |
827 | | .. method:: Client.put(path, data={}, content_type=MULTIPART_CONTENT, follow=False, **extra) |
| 824 | The ``follow`` and ``extra`` arguments act the same as for |
| 825 | :meth:`Client.get`. |
| 826 | |
| 827 | .. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, **extra) |
828 | 828 | |
829 | 829 | Makes a PUT request on the provided ``path`` and returns a |
830 | | ``Response`` object. Useful for testing RESTful interfaces. Acts just |
831 | | like :meth:`Client.post` except with the PUT request method. |
| 830 | ``Response`` object. ``data`` is Useful for testing RESTful interfaces. |
832 | 831 | |
833 | | If you set ``follow`` to ``True`` the client will follow any redirects |
834 | | and a ``redirect_chain`` attribute will be set in the response object |
835 | | containing tuples of the intermediate urls and status codes. |
| 832 | When ``data`` is provided, it is used as the request body, and |
| 833 | a ``Content-Type`` header is set to ``content_type``. |
| 834 | |
| 835 | .. versionchanged:: 1.5 |
| 836 | ``Client.put`` used to process ``data`` like Client.post``. |
836 | 837 | |
837 | | .. method:: Client.delete(path, follow=False, **extra) |
| 838 | The ``follow`` and ``extra`` arguments act the same as for |
| 839 | :meth:`Client.get`. |
| 840 | |
| 841 | .. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, **extra) |
838 | 842 | |
839 | 843 | Makes an DELETE request on the provided ``path`` and returns a |
840 | 844 | ``Response`` object. Useful for testing RESTful interfaces. |
841 | 845 | |
842 | | If you set ``follow`` to ``True`` the client will follow any redirects |
843 | | and a ``redirect_chain`` attribute will be set in the response object |
844 | | containing tuples of the intermediate urls and status codes. |
| 846 | When ``data`` is provided, it is used as the request body, and |
| 847 | a ``Content-Type`` header is set to ``content_type``. |
| 848 | |
| 849 | .. versionchanged:: 1.5 |
| 850 | ``Client.delete`` used to process ``data`` like Client.get``. |
| 851 | |
| 852 | The ``follow`` and ``extra`` arguments act the same as for |
| 853 | :meth:`Client.get`. |
845 | 854 | |
846 | | The ``extra`` argument acts the same as for :meth:`Client.get`. |
847 | 855 | |
848 | 856 | .. method:: Client.login(**credentials) |
849 | 857 | |
diff --git a/tests/regressiontests/conditional_processing/models.py b/tests/regressiontests/conditional_processing/models.py
index f7f48bc..97aeff5 100644
a
|
b
|
class ConditionalGet(TestCase):
|
63 | 63 | |
64 | 64 | def testIfMatch(self): |
65 | 65 | self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG |
66 | | response = self.client.put('/condition/etag/', {'data': ''}) |
| 66 | response = self.client.put('/condition/etag/') |
67 | 67 | self.assertEqual(response.status_code, 200) |
68 | 68 | self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG |
69 | | response = self.client.put('/condition/etag/', {'data': ''}) |
| 69 | response = self.client.put('/condition/etag/') |
70 | 70 | self.assertEqual(response.status_code, 412) |
71 | 71 | |
72 | 72 | def testBothHeaders(self): |
diff --git a/tests/regressiontests/test_client_regress/models.py b/tests/regressiontests/test_client_regress/models.py
index 4f057b9..e69a164 100644
a
|
b
|
class AssertRedirectsTests(TestCase):
|
347 | 347 | def test_redirect_chain_options(self): |
348 | 348 | "A redirect chain will be followed from an initial OPTIONS request" |
349 | 349 | response = self.client.options('/test_client_regress/redirects/', |
350 | | {'nothing': 'to_send'}, follow=True) |
| 350 | follow=True) |
351 | 351 | self.assertRedirects(response, |
352 | 352 | '/test_client_regress/no_template_view/', 301, 200) |
353 | 353 | self.assertEqual(len(response.redirect_chain), 3) |
… |
… |
class AssertRedirectsTests(TestCase):
|
355 | 355 | def test_redirect_chain_put(self): |
356 | 356 | "A redirect chain will be followed from an initial PUT request" |
357 | 357 | response = self.client.put('/test_client_regress/redirects/', |
358 | | {'nothing': 'to_send'}, follow=True) |
| 358 | follow=True) |
359 | 359 | self.assertRedirects(response, |
360 | 360 | '/test_client_regress/no_template_view/', 301, 200) |
361 | 361 | self.assertEqual(len(response.redirect_chain), 3) |
… |
… |
class AssertRedirectsTests(TestCase):
|
363 | 363 | def test_redirect_chain_delete(self): |
364 | 364 | "A redirect chain will be followed from an initial DELETE request" |
365 | 365 | response = self.client.delete('/test_client_regress/redirects/', |
366 | | {'nothing': 'to_send'}, follow=True) |
| 366 | follow=True) |
367 | 367 | self.assertRedirects(response, |
368 | 368 | '/test_client_regress/no_template_view/', 301, 200) |
369 | 369 | self.assertEqual(len(response.redirect_chain), 3) |
… |
… |
class RequestMethodStringDataTests(TestCase):
|
809 | 809 | class QueryStringTests(TestCase): |
810 | 810 | def test_get_like_requests(self): |
811 | 811 | # See: https://code.djangoproject.com/ticket/10571. |
812 | | # Removed 'put' and 'delete' here as they are 'GET-like requests' |
813 | | for method_name in ('get','head','options'): |
| 812 | for method_name in ('get', 'head'): |
814 | 813 | # A GET-like request can pass a query string as data |
815 | 814 | method = getattr(self.client, method_name) |
816 | 815 | response = method("/test_client_regress/request_data/", data={'foo':'whiz'}) |
… |
… |
class UnicodePayloadTests(TestCase):
|
867 | 866 | response = self.client.post("/test_client_regress/parse_unicode_json/", json, |
868 | 867 | content_type="application/json") |
869 | 868 | self.assertEqual(response.content, json) |
870 | | response = self.client.put("/test_client_regress/parse_unicode_json/", json, |
871 | | content_type="application/json") |
872 | | self.assertEqual(response.content, json) |
873 | 869 | |
874 | 870 | def test_unicode_payload_utf8(self): |
875 | 871 | "A non-ASCII unicode data encoded as UTF-8 can be POSTed" |
… |
… |
class UnicodePayloadTests(TestCase):
|
878 | 874 | response = self.client.post("/test_client_regress/parse_unicode_json/", json, |
879 | 875 | content_type="application/json; charset=utf-8") |
880 | 876 | self.assertEqual(response.content, json.encode('utf-8')) |
881 | | response = self.client.put("/test_client_regress/parse_unicode_json/", json, |
882 | | content_type="application/json; charset=utf-8") |
883 | | self.assertEqual(response.content, json.encode('utf-8')) |
884 | 877 | |
885 | 878 | def test_unicode_payload_utf16(self): |
886 | 879 | "A non-ASCII unicode data encoded as UTF-16 can be POSTed" |
… |
… |
class UnicodePayloadTests(TestCase):
|
889 | 882 | response = self.client.post("/test_client_regress/parse_unicode_json/", json, |
890 | 883 | content_type="application/json; charset=utf-16") |
891 | 884 | self.assertEqual(response.content, json.encode('utf-16')) |
892 | | response = self.client.put("/test_client_regress/parse_unicode_json/", json, |
893 | | content_type="application/json; charset=utf-16") |
894 | | self.assertEqual(response.content, json.encode('utf-16')) |
895 | 885 | |
896 | 886 | def test_unicode_payload_non_utf(self): |
897 | 887 | "A non-ASCII unicode data as a non-UTF based encoding can be POSTed" |
… |
… |
class UnicodePayloadTests(TestCase):
|
900 | 890 | response = self.client.post("/test_client_regress/parse_unicode_json/", json, |
901 | 891 | content_type="application/json; charset=koi8-r") |
902 | 892 | self.assertEqual(response.content, json.encode('koi8-r')) |
903 | | response = self.client.put("/test_client_regress/parse_unicode_json/", json, |
904 | | content_type="application/json; charset=koi8-r") |
905 | | self.assertEqual(response.content, json.encode('koi8-r')) |
906 | 893 | |
907 | 894 | class DummyFile(object): |
908 | 895 | def __init__(self, filename): |