Index: django/test/client.py
===================================================================
--- django/test/client.py	(revision 6825)
+++ django/test/client.py	(working copy)
@@ -1,14 +1,14 @@
 import datetime
 import sys
 from cStringIO import StringIO
-from urlparse import urlparse
+from urlparse import urlparse, urlsplit
 from django.conf import settings
 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.dispatch import dispatcher
-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
@@ -205,7 +205,7 @@
 
         return response
 
-    def get(self, path, data={}, **extra):
+    def get(self, path, data={}, follow=True, **extra):
         "Request a response from the server using GET."
         r = {
             'CONTENT_LENGTH':  None,
@@ -216,9 +216,12 @@
         }
         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=True, **extra):
         "Request a response from the server using POST."
 
         if content_type is MULTIPART_CONTENT:
@@ -235,7 +238,10 @@
         }
         r.update(extra)
 
-        return self.request(**r)
+        response = self.request(**r)
+        if follow:
+            response = self._handle_redirects(response)
+        return response
 
     def login(self, **credentials):
         """Set the Client to appear as if it has sucessfully logged into a site.
@@ -276,3 +282,27 @@
         session = __import__(settings.SESSION_ENGINE, {}, {}, ['']).SessionStore()
         session.delete(session_key=self.cookies['sessionid'].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)
+
+            local_redirect_chain = response.redirect_chain
+            local_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 = local_redirect_chain
+
+            # prevent any loops
+            if response.redirect_chain[-1] in response.redirect_chain[0:-1]:
+                break
+
+        return response
\ No newline at end of file
Index: django/test/testcases.py
===================================================================
--- django/test/testcases.py	(revision 6825)
+++ django/test/testcases.py	(working copy)
@@ -2,7 +2,6 @@
 import unittest
 from urlparse import urlsplit, urlunsplit
 
-from django.http import QueryDict
 from django.db import transaction
 from django.core import mail
 from django.core.management import call_command
@@ -74,32 +73,39 @@
         super(TestCase, self).__call__(result)
 
     def assertRedirects(self, response, expected_url, status_code=302,
-                        target_status_code=200, host=None):
+                        target_status_code=200, host=None, redirect_level=-1):
         """Asserts that a response redirected to a specific URL, and that the
         redirect URL can be loaded.
 
         Note that assertRedirects won't work for external links since it uses
         TestClient to do a request.
+        
+        Note that assertRedirects only works properly if follow was set to true
+        in the request.
         """
-        self.assertEqual(response.status_code, status_code,
+
+        redirect_status_code = response.status_code
+        redirect_url = response.get('Location', '')
+        chain = response.__dict__.get('redirect_chain')
+        if chain:
+            redirect_url = chain[redirect_level][0]
+            redirect_status_code = chain[redirect_level][1]
+
+        self.assertEqual(redirect_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)
+             " (expected %d)" % (redirect_status_code, 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))
+        self.assertEqual(redirect_url, expected_url,
+            "Response redirected to '%s', expected '%s'" % (redirect_url, expected_url))
 
-        # 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,
+        self.assertEqual(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))
+                 (redirect_url, response.status_code, target_status_code))
 
     def assertContains(self, response, text, count=None, status_code=200):
         """
Index: docs/testing.txt
===================================================================
--- docs/testing.txt	(revision 6825)
+++ docs/testing.txt	(working copy)
@@ -453,9 +453,10 @@
 
 Once you have a ``Client`` instance, you can call any of the following methods:
 
-``get(path, data={})``
+``get(path, data={}, follow=True)``
     Makes a GET request on the provided ``path`` and returns a ``Response``
-    object, which is documented below.
+    object, which is documented below, and will optionally ``follow`` any
+    server redirects.
 
     The key-value pairs in the ``data`` dictionary are used to create a GET
     data payload. For example::
@@ -467,9 +468,19 @@
 
         /customers/details/?name=fred&age=7
 
-``post(path, data={}, content_type=MULTIPART_CONTENT)``
+    If any redirects occurred, the response will have a ``redirect_chain``
+    attribute containing tuples of the intermediate urls and status codes.
+    If you had an url ``/redirect_me/`` that redirected to ``/final/``, this is 
+    what you'd see:
+    
+        >>> response = c.get('/redirect_me/')
+        >>> response.redirect_chain
+        [(u'/final/', 301)]
+
+``post(path, data={}, content_type=MULTIPART_CONTENT, follow=True)``
     Makes a POST request on the provided ``path`` and returns a ``Response``
-    object, which is documented below.
+    object, which is documented below, and will optionally ``follow`` any
+    server redirects.
 
     The key-value pairs in the ``data`` dictionary are used to submit POST
     data. For example::
@@ -501,6 +512,15 @@
 
         {'choices': ('a', 'b', 'd')}
 
+    If any redirects occurred, the response will have a ``redirect_chain``
+    attribute containing tuples of the intermediate urls and status codes.
+    If you had an url ``/redirect_me/`` that redirected to ``/final/``, this is 
+    what you'd see:
+    
+        >>> response = c.post('/redirect_me/', {'data': 'test'})
+        >>> response.redirect_chain
+        [(u'/final/', 302)]
+
     Submitting files is a special case. To POST a file, you need only provide
     the file field name as a key, and a file handle to the file you wish to
     upload as a value. For example::
@@ -834,10 +854,14 @@
     Asserts that the template with the given name was *not* used in rendering
     the response.
 
-``assertRedirects(response, expected_url, status_code=302, target_status_code=200)``
+``assertRedirects(response, expected_url, status_code=302, target_status_code=200, redirect_level=-1)``
     Asserts that the response return a ``status_code`` redirect status,
-    it redirected to ``expected_url`` (including any GET data), and the subsequent
+    it redirected to ``expected_url`` (including any GET data), and the final
     page was received with ``target_status_code``.
+    
+    If you're expecting multiple levels of redirects, you can specify to which 
+    ``redirect_level`` the ``expected_url`` and ``status_code`` are matched against.
+    A value of -1 checks against the last redirect in the chain.
 
 ``assertTemplateUsed(response, template_name)``
     Asserts that the template with the given name was used in rendering the
Index: tests/modeltests/test_client/models.py
===================================================================
--- tests/modeltests/test_client/models.py	(revision 6825)
+++ tests/modeltests/test_client/models.py	(working copy)
@@ -114,11 +114,18 @@
 
     def test_redirect_to_strange_location(self):
         "GET a URL that redirects to a non-200 page"
+        response = self.client.get('/test_client/redirect_to_bad_view/')
+
+        # Check that the response was a 404
+        self.assertRedirects(response, 'http://testserver/test_client/bad_view/', target_status_code=404)
+
+    def test_chained_redirects(self):
+        "GET a URL that redirects more than once"
         response = self.client.get('/test_client/double_redirect_view/')
 
-        # Check that the response was a 302, and that
-        # 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)
+        # Check that there were two redirects, the first being a 302 and the second 301
+        self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/', status_code=302, redirect_level=0)
+        self.assertRedirects(response, 'http://testserver/test_client/get_view/', status_code=301, redirect_level=1)
 
     def test_notfound_response(self):
         "GET a URL that responds as '404:Not Found'"
Index: tests/modeltests/test_client/urls.py
===================================================================
--- tests/modeltests/test_client/urls.py	(revision 6825)
+++ tests/modeltests/test_client/urls.py	(working copy)
@@ -9,6 +9,8 @@
     (r'^redirect_view/$', views.redirect_view),
     (r'^permanent_redirect_view/$', redirect_to, { 'url': '/test_client/get_view/' }),
     (r'^double_redirect_view/$', views.double_redirect_view),
+    (r'^redirect_to_bad_view/$', views.redirect_to_bad_view),
+    (r'^infinite_redirect/$', views.redirect_to_self),
     (r'^bad_view/$', views.bad_view),
     (r'^form_view/$', views.form_view),
     (r'^form_view_with_template/$', views.form_view_with_template),
Index: tests/modeltests/test_client/views.py
===================================================================
--- tests/modeltests/test_client/views.py	(revision 6825)
+++ tests/modeltests/test_client/views.py	(working copy)
@@ -60,6 +60,14 @@
     "A view that redirects all requests to a redirection view"
     return HttpResponseRedirect('/test_client/permanent_redirect_view/')
 
+def redirect_to_bad_view(request):
+    "A view that redirects all requests to 404 page"
+    return HttpResponseRedirect('/test_client/bad_view/')
+
+def redirect_to_self(request):
+    "A view that redirects to itself (bad thing!)"
+    return HttpResponseRedirect('/test_client/infinite_redirect/')
+
 def bad_view(request):
     "A view that returns a 404 with some error content"
     return HttpResponseNotFound('Not found!. This page contains some MAGIC content')
Index: tests/regressiontests/test_client_regress/models.py
===================================================================
--- tests/regressiontests/test_client_regress/models.py	(revision 6825)
+++ tests/regressiontests/test_client_regress/models.py	(working copy)
@@ -132,13 +132,26 @@
 
     def test_target_page(self):
         "An assertion is raised if the response redirect target cannot be retrieved as expected"
-        response = self.client.get('/test_client/double_redirect_view/')
+        response = self.client.get('/test_client/redirect_to_bad_view/')
         try:
-            # The redirect target responds with a 301 code, not 200
-            self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/')
+            # The redirect target responds with a 404 code, not 200 (simulated case.)
+            self.assertRedirects(response, 'http://testserver/test_client/bad_view/')
         except AssertionError, e:
-            self.assertEquals(str(e), "Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)")
+            self.assertEquals(str(e), "Couldn't retrieve redirection page 'http://testserver/test_client/bad_view/': response code was 404 (expected 200)")
 
+    def test_infinite_loop_redirect(self):
+        "An exception is raised if an URL contains a loop in the redirect chain"
+
+        # Check that the test client breaks out of infinite redirect loops
+        response = self.client.get('/test_client/infinite_redirect/')
+        try:
+            self.assertRedirects(response, 'http://testserver/test_client/infinite_redirect/')
+        except AssertionError, e:
+            self.assertEquals(str(e), "Couldn't retrieve redirection page 'http://testserver/test_client/infinite_redirect/': response code was 302 (expected 200)")
+
+        # For educational purposes: how to tell that you encountered a loop
+        self.assertTrue(response.redirect_chain[-1] in response.redirect_chain[0:-1])
+
 class AssertFormErrorTests(TestCase):
     def test_unknown_form(self):
         "An assertion is raised if the form name is unknown"
