From 272f685794de0b8dead220ee57b30e65c9aa097c Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 22 May 2017 11:49:39 +0100 Subject: [PATCH] Fixed #27999 -- Added test client support for HTTP 307 and 308 redirects. --- django/test/client.py | 38 +++++++++++++++++++++++++++----------- docs/releases/2.1.txt | 2 +- tests/test_client/tests.py | 34 ++++++++++++++++++++++++++++++++++ tests/test_client/urls.py | 3 +++ tests/test_client/views.py | 24 ++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/django/test/client.py b/django/test/client.py index d69c33f1bd..9fce782dd4 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -5,6 +5,7 @@ import re import sys from copy import copy from functools import partial +from http import HTTPStatus from importlib import import_module from io import BytesIO from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit @@ -512,7 +513,7 @@ class Client(RequestFactory): """Request a response from the server using GET.""" response = super().get(path, data=data, secure=secure, **extra) if follow: - response = self._handle_redirects(response, **extra) + response = self._handle_redirects(response, data=data, **extra) return response def post(self, path, data=None, content_type=MULTIPART_CONTENT, @@ -520,14 +521,14 @@ class Client(RequestFactory): """Request a response from the server using POST.""" response = super().post(path, data=data, content_type=content_type, secure=secure, **extra) if follow: - response = self._handle_redirects(response, **extra) + response = self._handle_redirects(response, data=data, content_type=content_type, **extra) return response def head(self, path, data=None, follow=False, secure=False, **extra): """Request a response from the server using HEAD.""" response = super().head(path, data=data, secure=secure, **extra) if follow: - response = self._handle_redirects(response, **extra) + response = self._handle_redirects(response, data=data, **extra) return response def options(self, path, data='', content_type='application/octet-stream', @@ -535,7 +536,7 @@ class Client(RequestFactory): """Request a response from the server using OPTIONS.""" response = super().options(path, data=data, content_type=content_type, secure=secure, **extra) if follow: - response = self._handle_redirects(response, **extra) + response = self._handle_redirects(response, data=data, content_type=content_type, **extra) return response def put(self, path, data='', content_type='application/octet-stream', @@ -543,7 +544,7 @@ class Client(RequestFactory): """Send a resource to the server using PUT.""" response = super().put(path, data=data, content_type=content_type, secure=secure, **extra) if follow: - response = self._handle_redirects(response, **extra) + response = self._handle_redirects(response, data=data, content_type=content_type, **extra) return response def patch(self, path, data='', content_type='application/octet-stream', @@ -551,7 +552,7 @@ class Client(RequestFactory): """Send a resource to the server using PATCH.""" response = super().patch(path, data=data, content_type=content_type, secure=secure, **extra) if follow: - response = self._handle_redirects(response, **extra) + response = self._handle_redirects(response, data=data, content_type=content_type, **extra) return response def delete(self, path, data='', content_type='application/octet-stream', @@ -559,14 +560,14 @@ class Client(RequestFactory): """Send a DELETE request to the server.""" response = super().delete(path, data=data, content_type=content_type, secure=secure, **extra) if follow: - response = self._handle_redirects(response, **extra) + response = self._handle_redirects(response, data=data, content_type=content_type, **extra) return response def trace(self, path, data='', follow=False, secure=False, **extra): """Send a TRACE request to the server.""" response = super().trace(path, data=data, secure=secure, **extra) if follow: - response = self._handle_redirects(response, **extra) + response = self._handle_redirects(response, data=data, **extra) return response def login(self, **credentials): @@ -648,12 +649,19 @@ class Client(RequestFactory): response._json = json.loads(response.content.decode(), **extra) return response._json - def _handle_redirects(self, response, **extra): + def _handle_redirects(self, response, data='', content_type='', **extra): """ Follow any redirects by requesting responses from the server using GET. """ response.redirect_chain = [] - while response.status_code in (301, 302, 303, 307): + redirect_status_codes = ( + HTTPStatus.MOVED_PERMANENTLY, + HTTPStatus.FOUND, + HTTPStatus.SEE_OTHER, + HTTPStatus.TEMPORARY_REDIRECT, + HTTPStatus.PERMANENT_REDIRECT, + ) + while response.status_code in redirect_status_codes: response_url = response.url redirect_chain = response.redirect_chain redirect_chain.append((response_url, response.status_code)) @@ -671,7 +679,15 @@ class Client(RequestFactory): if not path.startswith('/'): path = urljoin(response.request['PATH_INFO'], path) - response = self.get(path, QueryDict(url.query), follow=False, **extra) + if response.status_code in (HTTPStatus.TEMPORARY_REDIRECT, HTTPStatus.PERMANENT_REDIRECT): + # Preserve request method post-redirect for 307/308 responses. + request_method = getattr(self, response.request['REQUEST_METHOD'].lower()) + else: + request_method = self.get + data = QueryDict(url.query) + content_type = None + + response = request_method(path, data=data, content_type=content_type, follow=False, **extra) response.redirect_chain = redirect_chain if redirect_chain[-1] in redirect_chain[:-1]: diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt index ce5fd3551b..54666e3d44 100644 --- a/docs/releases/2.1.txt +++ b/docs/releases/2.1.txt @@ -206,7 +206,7 @@ Templates Tests ~~~~~ -* ... +* Added test :class:`~django.test.Client` support for 307 and 308 redirects. URLs ~~~~ diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index f7d32aa9cd..2643a279b1 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -19,6 +19,7 @@ testing against the contexts and templates produced by a view, rather than the HTML rendered to the end-user. """ +import itertools import tempfile from django.contrib.auth.models import User @@ -202,6 +203,39 @@ class ClientTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.request['PATH_INFO'], '/accounts/login/') + def test_follow_307_and_308_redirect(self): + """ + A 307 or 308 redirect preserves the request method after the redirect. + """ + methods = ('get', 'post', 'head', 'options', 'put', 'patch', 'delete', 'trace') + codes = (307, 308) + for method, code in itertools.product(methods, codes): + with self.subTest(method=method, code=code): + req_method = getattr(self.client, method) + response = req_method('/redirect_view_%s/' % code, data={'value': 'test'}, follow=True) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.request['PATH_INFO'], '/post_view/') + self.assertEqual(response.request['REQUEST_METHOD'], method.upper()) + + def test_follow_307_and_308_preserves_post_data(self): + for code in (307, 308): + with self.subTest(code=code): + response = self.client.post('/redirect_view_%s/' % code, data={'value': 'test'}, follow=True) + self.assertContains(response, 'test is the value') + + def test_follow_307_and_308_preserves_put_body(self): + for code in (307, 308): + with self.subTest(code=code): + response = self.client.put('/redirect_view_%s/?to=/put_view/' % code, data='a=b', follow=True) + self.assertContains(response, 'a=b is the body') + + def test_follow_307_and_308_preserves_get_params(self): + data = {'var': 30, 'to': '/get_view/'} + for code in (307, 308): + with self.subTest(code=code): + response = self.client.get('/redirect_view_%s/' % code, data=data, follow=True) + self.assertContains(response, '30 is the value') + def test_redirect_http(self): "GET a URL that redirects to an http URI" response = self.client.get('/http_redirect_view/', follow=True) diff --git a/tests/test_client/urls.py b/tests/test_client/urls.py index 40ca384751..749426f2c4 100644 --- a/tests/test_client/urls.py +++ b/tests/test_client/urls.py @@ -8,10 +8,13 @@ urlpatterns = [ url(r'^upload_view/$', views.upload_view, name='upload_view'), url(r'^get_view/$', views.get_view, name='get_view'), url(r'^post_view/$', views.post_view), + url(r'^put_view/$', views.put_view), url(r'^trace_view/$', views.trace_view), url(r'^header_view/$', views.view_with_header), url(r'^raw_post_view/$', views.raw_post_view), url(r'^redirect_view/$', views.redirect_view), + url(r'^redirect_view_307/$', views.method_saving_307_redirect_view), + url(r'^redirect_view_308/$', views.method_saving_308_redirect_view), url(r'^secure_view/$', views.view_with_secure), url(r'^permanent_redirect_view/$', RedirectView.as_view(url='/get_view/', permanent=True)), url(r'^temporary_redirect_view/$', RedirectView.as_view(url='/get_view/', permanent=False)), diff --git a/tests/test_client/views.py b/tests/test_client/views.py index 3387008d66..80daa9bcad 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -49,6 +49,16 @@ def trace_view(request): return HttpResponse(t.render(c)) +def put_view(request): + if request.method == 'PUT': + t = Template('Data received: {{ data }} is the body.', name='PUT Template') + c = Context({'data': request.body.decode()}) + else: + t = Template('Viewing GET page.', name='Empty GET Template') + c = Context() + return HttpResponse(t.render(c)) + + def post_view(request): """A view that expects a POST, and returns a different template depending on whether any POST data is available @@ -99,6 +109,20 @@ def redirect_view(request): return HttpResponseRedirect('/get_view/' + query) +def _post_view_redirect(request, status_code): + """Redirect to /post_view/ using the status code.""" + redirect_to = request.GET.get('to', '/post_view/') + return HttpResponseRedirect(redirect_to, status=status_code) + + +def method_saving_307_redirect_view(request): + return _post_view_redirect(request, 307) + + +def method_saving_308_redirect_view(request): + return _post_view_redirect(request, 308) + + def view_with_secure(request): "A view that indicates if the request was secure" response = HttpResponse()