From 28634394f53b9ab27d3419cfde047ee78e491d97 Mon Sep 17 00:00:00 2001 From: Rigel Di Scala Date: Mon, 13 Oct 2014 12:10:00 +0100 Subject: [PATCH] Fixed #23606 -- Implemented Client and RequestFactory trace() methods. Thanks KevinEtienne for the suggestion. --- django/test/client.py | 13 ++++++++ docs/releases/1.8.txt | 4 +++ docs/topics/testing/advanced.txt | 4 +-- docs/topics/testing/tools.txt | 14 ++++++++ tests/test_client/tests.py | 55 ++++++++++++++++++++++++++++++-- tests/test_client/urls.py | 1 + tests/test_client/views.py | 30 ++++++++++++++++- 7 files changed, 115 insertions(+), 6 deletions(-) diff --git a/django/test/client.py b/django/test/client.py index b1ff33e3d3..fb8ff75e95 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -306,6 +306,10 @@ class RequestFactory(object): r.update(extra) return self.generic('HEAD', path, secure=secure, **r) + def trace(self, path, secure=False, **extra): + "Construct a TRACE request." + return self.generic('TRACE', path, secure=secure, **extra) + def options(self, path, data='', content_type='application/octet-stream', secure=False, **extra): "Construct an OPTIONS request." @@ -552,6 +556,15 @@ class Client(RequestFactory): response = self._handle_redirects(response, **extra) return response + def trace(self, path, data='', follow=False, secure=False, **extra): + """ + Send a TRACE request to the server. + """ + response = super(Client, self).trace(path, data=data, secure=secure, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response + def login(self, **credentials): """ Sets the Factory to appear as if it has successfully logged into a site. diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index eef836f6aa..23c6deeef1 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -372,6 +372,10 @@ Requests and Responses Tests ^^^^^ +* The :class:`RequestFactory.trace() ` + and :class:`Client.trace() ` methods were + implemented, allowing you to create ``TRACE`` requests in your tests. + * The ``count`` argument was added to :meth:`~django.test.SimpleTestCase.assertTemplateUsed`. This allows you to assert that a template was rendered a specific number of times. diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index bbd8584328..d817cf4f02 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -21,8 +21,8 @@ restricted subset of the test client API: * It only has access to the HTTP methods :meth:`~Client.get()`, :meth:`~Client.post()`, :meth:`~Client.put()`, - :meth:`~Client.delete()`, :meth:`~Client.head()` and - :meth:`~Client.options()`. + :meth:`~Client.delete()`, :meth:`~Client.head()`, + :meth:`~Client.options()`, and :meth:`~Client.trace()`. * These methods accept all the same arguments *except* for ``follows``. Since this is just a factory for producing diff --git a/docs/topics/testing/tools.txt b/docs/topics/testing/tools.txt index 43c5fcaf48..439910aa8a 100644 --- a/docs/topics/testing/tools.txt +++ b/docs/topics/testing/tools.txt @@ -316,6 +316,20 @@ Use the ``django.test.Client`` class to make requests. The ``follow``, ``secure`` and ``extra`` arguments act the same as for :meth:`Client.get`. + .. method:: Client.trace(path, follow=False, secure=False, **extra) + + .. versionadded:: 1.8 + + Makes a TRACE request on the provided ``path`` and returns a + ``Response`` object. Useful for simulating diagnostic probes. + + Unlike the other request methods, ``data`` is not provided as a keyword + parameter in order to comply with :rfc:`2616`, which mandates that + TRACE requests should not have an entity-body. + + The ``follow``, ``secure``, and ``extra`` arguments act the same as for + :meth:`Client.get`. + .. method:: Client.login(**credentials) If your site uses Django's :doc:`authentication system` diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index 034bb95b41..cf96b89a62 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -23,10 +23,11 @@ rather than the HTML rendered to the end-user. from __future__ import unicode_literals from django.core import mail +from django.http import HttpResponse from django.test import Client, TestCase, RequestFactory from django.test import override_settings -from .views import get_view +from .views import get_view, post_view, trace_view @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), @@ -79,6 +80,13 @@ class ClientTest(TestCase): self.assertEqual(response.templates[0].name, 'POST Template') self.assertContains(response, 'Data received') + def test_trace(self): + """TRACE a view""" + response = self.client.trace('/trace_view/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['method'], 'TRACE') + self.assertEqual(response.templates[0].name, 'TRACE Template') + def test_response_headers(self): "Check the value of HTTP headers returned in a response" response = self.client.get("/header_view/") @@ -552,13 +560,54 @@ class CustomTestClientTest(TestCase): self.assertEqual(hasattr(self.client, "i_am_customized"), True) +_generic_view = lambda request: HttpResponse(status=200) + + @override_settings(ROOT_URLCONF='test_client.urls') class RequestFactoryTest(TestCase): + """Tests for the request factory.""" + + # A mapping between names of HTTP/1.1 methods and their test views. + http_methods_and_views = ( + ('get', get_view), + ('post', post_view), + ('put', _generic_view), + ('patch', _generic_view), + ('delete', _generic_view), + ('head', _generic_view), + ('options', _generic_view), + ('trace', trace_view), + ) + + def setUp(self): + self.request_factory = RequestFactory() def test_request_factory(self): - factory = RequestFactory() - request = factory.get('/somewhere/') + """The request factory implements all the HTTP/1.1 methods.""" + for method_name, view in self.http_methods_and_views: + method = getattr(self.request_factory, method_name) + request = method('/somewhere/') + response = view(request) + + self.assertEqual(response.status_code, 200) + + def test_get_request_from_factory(self): + """ + The request factory returns a templated response for a GET request. + """ + request = self.request_factory.get('/somewhere/') response = get_view(request) self.assertEqual(response.status_code, 200) self.assertContains(response, 'This is a test') + + def test_trace_request_from_factory(self): + """The request factory returns an echo response for a TRACE request.""" + url_path = '/somewhere/' + request = self.request_factory.trace(url_path) + response = trace_view(request) + protocol = request.META["SERVER_PROTOCOL"] + echoed_request_line = "TRACE {} {}".format(url_path, protocol) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, echoed_request_line) diff --git a/tests/test_client/urls.py b/tests/test_client/urls.py index 4b34bfb113..7073d4590e 100644 --- a/tests/test_client/urls.py +++ b/tests/test_client/urls.py @@ -8,6 +8,7 @@ from . import views urlpatterns = [ url(r'^get_view/$', views.get_view, name='get_view'), url(r'^post_view/$', views.post_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), diff --git a/tests/test_client/views.py b/tests/test_client/views.py index 8660e93c7f..2b7a4de63c 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -5,7 +5,10 @@ from django.core import mail from django.forms import fields from django.forms.forms import Form, ValidationError from django.forms.formsets import formset_factory, BaseFormSet -from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound +from django.http import ( + HttpResponse, HttpResponseRedirect, HttpResponseNotFound, + HttpResponseNotAllowed, HttpResponseBadRequest, +) from django.shortcuts import render_to_response from django.template import Context, Template from django.utils.decorators import method_decorator @@ -20,6 +23,31 @@ def get_view(request): return HttpResponse(t.render(c)) +def trace_view(request): + """ + A simple view that expects a TRACE request and echoes its status line. + + TRACE requests should not have an entity; the view will return a 400 status + response if it is present. + """ + if request.method.upper() != "TRACE": + return HttpResponseNotAllowed("TRACE") + elif request.body: + return HttpResponseBadRequest("TRACE requests MUST NOT include an entity") + else: + protocol = request.META["SERVER_PROTOCOL"] + t = Template( + '{{ method }} {{ uri }} {{ version }}', + name="TRACE Template", + ) + c = Context({ + 'method': request.method, + 'uri': request.path, + 'version': protocol, + }) + 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