Commit e735fe71 authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

Fixed #4476 -- Added a ``follow`` option to the test client request methods....

Fixed #4476 -- Added a ``follow`` option to the test client request methods. This implements browser-like behavior for the test client, following redirect chains when a 30X response is received. Thanks to Marc Fargas and Keith Bussell for their work on this.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@9911 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent e20f09c2
Loading
Loading
Loading
Loading
+58 −14
Original line number Diff line number Diff line
import urllib
from urlparse import urlparse, urlunparse
from urlparse import urlparse, urlunparse, urlsplit
import sys
import os
try:
@@ -12,7 +12,7 @@ from django.contrib.auth import authenticate, login
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest
from django.core.signals import got_request_exception
from django.http import SimpleCookie, HttpRequest
from django.http import SimpleCookie, HttpRequest, QueryDict
from django.template import TemplateDoesNotExist
from django.test import signals
from django.utils.functional import curry
@@ -261,7 +261,7 @@ class Client(object):

        return response

    def get(self, path, data={}, **extra):
    def get(self, path, data={}, follow=False, **extra):
        """
        Requests a response from the server using GET.
        """
@@ -275,9 +275,13 @@ class Client(object):
        }
        r.update(extra)

        return self.request(**r)
        response = self.request(**r)
        if follow:
            response = self._handle_redirects(response)
        return response

    def post(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
    def post(self, path, data={}, content_type=MULTIPART_CONTENT,
             follow=False, **extra):
        """
        Requests a response from the server using POST.
        """
@@ -297,9 +301,12 @@ class Client(object):
        }
        r.update(extra)

        return self.request(**r)
        response = self.request(**r)
        if follow:
            response = self._handle_redirects(response)
        return response

    def head(self, path, data={}, **extra):
    def head(self, path, data={}, follow=False, **extra):
        """
        Request a response from the server using HEAD.
        """
@@ -313,9 +320,12 @@ class Client(object):
        }
        r.update(extra)

        return self.request(**r)
        response = self.request(**r)
        if follow:
            response = self._handle_redirects(response)
        return response

    def options(self, path, data={}, **extra):
    def options(self, path, data={}, follow=False, **extra):
        """
        Request a response from the server using OPTIONS.
        """
@@ -328,9 +338,13 @@ class Client(object):
        }
        r.update(extra)

        return self.request(**r)
        response = self.request(**r)
        if follow:
            response = self._handle_redirects(response)
        return response

    def put(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
    def put(self, path, data={}, content_type=MULTIPART_CONTENT,
            follow=False, **extra):
        """
        Send a resource to the server using PUT.
        """
@@ -350,9 +364,12 @@ class Client(object):
        }
        r.update(extra)

        return self.request(**r)
        response = self.request(**r)
        if follow:
            response = self._handle_redirects(response)
        return response

    def delete(self, path, data={}, **extra):
    def delete(self, path, data={}, follow=False, **extra):
        """
        Send a DELETE request to the server.
        """
@@ -365,7 +382,10 @@ class Client(object):
        }
        r.update(extra)

        return self.request(**r)
        response = self.request(**r)
        if follow:
            response = self._handle_redirects(response)
        return response

    def login(self, **credentials):
        """
@@ -416,3 +436,27 @@ class Client(object):
        session = __import__(settings.SESSION_ENGINE, {}, {}, ['']).SessionStore()
        session.delete(session_key=self.cookies[settings.SESSION_COOKIE_NAME].value)
        self.cookies = SimpleCookie()

    def _handle_redirects(self, response):
        "Follows any redirects by requesting responses from the server using GET."

        response.redirect_chain = []
        while response.status_code in (301, 302, 303, 307):
            url = response['Location']
            scheme, netloc, path, query, fragment = urlsplit(url)

            redirect_chain = response.redirect_chain
            redirect_chain.append((url, response.status_code))

            # The test client doesn't handle external links,
            # but since the situation is simulated in test_client,
            # we fake things here by ignoring the netloc portion of the
            # redirected URL.
            response = self.get(path, QueryDict(query), follow=False)
            response.redirect_chain = redirect_chain

            # Prevent loops
            if response.redirect_chain[-1] in response.redirect_chain[0:-1]:
                break
        return response
+44 −21
Original line number Diff line number Diff line
@@ -276,26 +276,49 @@ class TransactionTestCase(unittest.TestCase):
        Note that assertRedirects won't work for external links since it uses
        TestClient to do a request.
        """
        if hasattr(response, 'redirect_chain'):
            # The request was a followed redirect
            self.assertTrue(len(response.redirect_chain) > 0,
                ("Response didn't redirect as expected: Response code was %d"
                " (expected %d)" % (response.status_code, status_code)))

            self.assertEqual(response.redirect_chain[0][1], status_code,
                ("Initial response didn't redirect as expected: Response code was %d"
                 " (expected %d)" % (response.redirect_chain[0][1], status_code)))

            url, status_code = response.redirect_chain[-1]

            self.assertEqual(response.status_code, target_status_code,
                ("Response didn't redirect as expected: Final Response code was %d"
                " (expected %d)" % (response.status_code, target_status_code)))

        else:
            # Not a followed redirect
            self.assertEqual(response.status_code, status_code,
                ("Response didn't redirect as expected: Response code was %d"
                 " (expected %d)" % (response.status_code, status_code)))

            url = response['Location']
            scheme, netloc, path, query, fragment = urlsplit(url)
        e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url)
        if not (e_scheme or e_netloc):
            expected_url = urlunsplit(('http', host or 'testserver', e_path,
                    e_query, e_fragment))
        self.assertEqual(url, expected_url,
            "Response redirected to '%s', expected '%s'" % (url, expected_url))

            redirect_response = response.client.get(path, QueryDict(query))

            # Get the redirection page, using the same client that was used
            # to obtain the original response.
        redirect_response = response.client.get(path, QueryDict(query))
            self.assertEqual(redirect_response.status_code, target_status_code,
                ("Couldn't retrieve redirection page '%s': response code was %d"
                 " (expected %d)") %
                     (path, redirect_response.status_code, target_status_code))

        e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url)
        if not (e_scheme or e_netloc):
            expected_url = urlunsplit(('http', host or 'testserver', e_path,
                e_query, e_fragment))

        self.assertEqual(url, expected_url,
            "Response redirected to '%s', expected '%s'" % (url, expected_url))


    def assertContains(self, response, text, count=None, status_code=200):
        """
        Asserts that a response indicates that a page was retrieved
+76 −38
Original line number Diff line number Diff line
@@ -478,7 +478,8 @@ arguments at time of construction:
    Once you have a ``Client`` instance, you can call any of the following
    methods:

    .. method:: Client.get(path, data={})
    .. method:: Client.get(path, data={}, follow=False)


        Makes a GET request on the provided ``path`` and returns a ``Response``
        object, which is documented below.
@@ -505,7 +506,18 @@ arguments at time of construction:
        If you provide URL both an encoded GET data and a data argument,
        the data argument will take precedence.

    .. method:: Client.post(path, data={}, content_type=MULTIPART_CONTENT)
        If you set ``follow`` to ``True`` the client will follow any redirects
        and a ``redirect_chain`` attribute will be set in the response object
        containing tuples of the intermediate urls and status codes.

        If you had an url ``/redirect_me/`` that redirected to ``/next/``, that
        redirected to ``/final/``, this is what you'd see::

            >>> response = c.get('/redirect_me/')
            >>> response.redirect_chain
            [(u'http://testserver/next/', 302), (u'http://testserver/final/', 302)]

    .. method:: Client.post(path, data={}, content_type=MULTIPART_CONTENT, follow=False)

        Makes a POST request on the provided ``path`` and returns a
        ``Response`` object, which is documented below.
@@ -556,7 +568,7 @@ arguments at time of construction:
        Note that you should manually close the file after it has been provided
        to ``post()``.

        .. versionadded:: development
        .. versionchanged:: 1.1

        If the URL you request with a POST contains encoded parameters, these
        parameters will be made available in the request.GET data. For example,
@@ -568,7 +580,11 @@ arguments at time of construction:
        to retrieve the username and password, and could interrogate request.GET
        to determine if the user was a visitor.

    .. method:: Client.head(path, data={})
        If you set ``follow`` to ``True`` the client will follow any redirects
        and a ``redirect_chain`` attribute will be set in the response object
        containing tuples of the intermediate urls and status codes.

    .. method:: Client.head(path, data={}, follow=False)

        .. versionadded:: development

@@ -576,14 +592,22 @@ arguments at time of construction:
        object. Useful for testing RESTful interfaces. Acts just like
        :meth:`Client.get` except it does not return a message body.

    .. method:: Client.options(path, data={})
        If you set ``follow`` to ``True`` the client will follow any redirects
        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)

        .. versionadded:: development

        Makes an OPTIONS request on the provided ``path`` and returns a
        ``Response`` object. Useful for testing RESTful interfaces.

    .. method:: Client.put(path, data={}, content_type=MULTIPART_CONTENT)
        If you set ``follow`` to ``True`` the client will follow any redirects
        and a ``redirect_chain`` attribute will be set in the response object
        containing tuples of the intermediate urls and status codes.

    .. method:: Client.put(path, data={}, content_type=MULTIPART_CONTENT, follow=False)

        .. versionadded:: development

@@ -591,13 +615,21 @@ arguments at time of construction:
        ``Response`` object. Useful for testing RESTful interfaces. Acts just
        like :meth:`Client.post` except with the PUT request method.

    .. method:: Client.delete(path)
        If you set ``follow`` to ``True`` the client will follow any redirects
        and a ``redirect_chain`` attribute will be set in the response object
        containing tuples of the intermediate urls and status codes.

    .. method:: Client.delete(path, follow=False)

        .. versionadded:: development

        Makes an DELETE request on the provided ``path`` and returns a
        ``Response`` object. Useful for testing RESTful interfaces.

        If you set ``follow`` to ``True`` the client will follow any redirects
        and a ``redirect_chain`` attribute will be set in the response object
        containing tuples of the intermediate urls and status codes.

    .. method:: Client.login(**credentials)

        .. versionadded:: 1.0
@@ -1028,9 +1060,15 @@ applications:
.. method:: assertRedirects(response, expected_url, status_code=302, target_status_code=200)

    Asserts that the response return a ``status_code`` redirect status, it
    redirected to ``expected_url`` (including any GET data), and the subsequent
    redirected to ``expected_url`` (including any GET data), and the final
    page was received with ``target_status_code``.

    .. versionadded:: 1.1

    If your request used the ``follow`` argument, the ``expected_url`` and
    ``target_status_code`` will be the url and status code for the final
    point of the redirect chain.

E-mail services
---------------

+9 −3
Original line number Diff line number Diff line
@@ -132,6 +132,12 @@ class ClientTest(TestCase):
        # the attempt to get the redirection location returned 301 when retrieved
        self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/', target_status_code=301)

    def test_follow_redirect(self):
        "A URL that redirects can be followed to termination."
        response = self.client.get('/test_client/double_redirect_view/', follow=True)
        self.assertRedirects(response, 'http://testserver/test_client/get_view/', status_code=302, target_status_code=200)
        self.assertEquals(len(response.redirect_chain), 2)

    def test_notfound_response(self):
        "GET a URL that responds as '404:Not Found'"
        response = self.client.get('/test_client/bad_view/')
+101 −0
Original line number Diff line number Diff line
@@ -148,6 +148,107 @@ class AssertRedirectsTests(TestCase):
        except AssertionError, e:
            self.assertEquals(str(e), "Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)")

    def test_redirect_chain(self):
        "You can follow a redirect chain of multiple redirects"
        response = self.client.get('/test_client_regress/redirects/further/more/', {}, follow=True)
        self.assertRedirects(response, '/test_client_regress/no_template_view/',
            status_code=301, target_status_code=200)

        self.assertEquals(len(response.redirect_chain), 1)
        self.assertEquals(response.redirect_chain[0], ('http://testserver/test_client_regress/no_template_view/', 301))

    def test_multiple_redirect_chain(self):
        "You can follow a redirect chain of multiple redirects"
        response = self.client.get('/test_client_regress/redirects/', {}, follow=True)
        self.assertRedirects(response, '/test_client_regress/no_template_view/',
            status_code=301, target_status_code=200)

        self.assertEquals(len(response.redirect_chain), 3)
        self.assertEquals(response.redirect_chain[0], ('http://testserver/test_client_regress/redirects/further/', 301))
        self.assertEquals(response.redirect_chain[1], ('http://testserver/test_client_regress/redirects/further/more/', 301))
        self.assertEquals(response.redirect_chain[2], ('http://testserver/test_client_regress/no_template_view/', 301))

    def test_redirect_chain_to_non_existent(self):
        "You can follow a chain to a non-existent view"
        response = self.client.get('/test_client_regress/redirect_to_non_existent_view2/', {}, follow=True)
        self.assertRedirects(response, '/test_client_regress/non_existent_view/',
            status_code=301, target_status_code=404)

    def test_redirect_chain_to_self(self):
        "Redirections to self are caught and escaped"
        response = self.client.get('/test_client_regress/redirect_to_self/', {}, follow=True)
        # The chain of redirects stops once the cycle is detected.
        self.assertRedirects(response, '/test_client_regress/redirect_to_self/',
            status_code=301, target_status_code=301)
        self.assertEquals(len(response.redirect_chain), 2)

    def test_circular_redirect(self):
        "Circular redirect chains are caught and escaped"
        response = self.client.get('/test_client_regress/circular_redirect_1/', {}, follow=True)
        # The chain of redirects will get back to the starting point, but stop there.
        self.assertRedirects(response, '/test_client_regress/circular_redirect_2/',
            status_code=301, target_status_code=301)
        self.assertEquals(len(response.redirect_chain), 4)

    def test_redirect_chain_post(self):
        "A redirect chain will be followed from an initial POST post"
        response = self.client.post('/test_client_regress/redirects/',
            {'nothing': 'to_send'}, follow=True)
        self.assertRedirects(response,
            '/test_client_regress/no_template_view/', 301, 200)
        self.assertEquals(len(response.redirect_chain), 3)

    def test_redirect_chain_head(self):
        "A redirect chain will be followed from an initial HEAD request"
        response = self.client.head('/test_client_regress/redirects/',
            {'nothing': 'to_send'}, follow=True)
        self.assertRedirects(response,
            '/test_client_regress/no_template_view/', 301, 200)
        self.assertEquals(len(response.redirect_chain), 3)

    def test_redirect_chain_options(self):
        "A redirect chain will be followed from an initial OPTIONS request"
        response = self.client.options('/test_client_regress/redirects/',
            {'nothing': 'to_send'}, follow=True)
        self.assertRedirects(response,
            '/test_client_regress/no_template_view/', 301, 200)
        self.assertEquals(len(response.redirect_chain), 3)

    def test_redirect_chain_put(self):
        "A redirect chain will be followed from an initial PUT request"
        response = self.client.put('/test_client_regress/redirects/',
            {'nothing': 'to_send'}, follow=True)
        self.assertRedirects(response,
            '/test_client_regress/no_template_view/', 301, 200)
        self.assertEquals(len(response.redirect_chain), 3)

    def test_redirect_chain_delete(self):
        "A redirect chain will be followed from an initial DELETE request"
        response = self.client.delete('/test_client_regress/redirects/',
            {'nothing': 'to_send'}, follow=True)
        self.assertRedirects(response,
            '/test_client_regress/no_template_view/', 301, 200)
        self.assertEquals(len(response.redirect_chain), 3)

    def test_redirect_chain_on_non_redirect_page(self):
        "An assertion is raised if the original page couldn't be retrieved as expected"
        # This page will redirect with code 301, not 302
        response = self.client.get('/test_client/get_view/', follow=True)
        try:
            self.assertRedirects(response, '/test_client/get_view/')
        except AssertionError, e:
            self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 200 (expected 302)")

    def test_redirect_on_non_redirect_page(self):
        "An assertion is raised if the original page couldn't be retrieved as expected"
        # This page will redirect with code 301, not 302
        response = self.client.get('/test_client/get_view/')
        try:
            self.assertRedirects(response, '/test_client/get_view/')
        except AssertionError, e:
            self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 200 (expected 302)")


class AssertFormErrorTests(TestCase):
    def test_unknown_form(self):
        "An assertion is raised if the form name is unknown"
Loading