Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #4476 -- Added a ``follow`` option to the test client request m…

…ethods. 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
  • Loading branch information...
commit e735fe7160d786efeb2e8bea595174c1a68409a2 1 parent e20f09c
Russell Keith-Magee authored February 27, 2009
72  django/test/client.py
... ...
@@ -1,5 +1,5 @@
1 1
 import urllib
2  
-from urlparse import urlparse, urlunparse
  2
+from urlparse import urlparse, urlunparse, urlsplit
3 3
 import sys
4 4
 import os
5 5
 try:
@@ -12,7 +12,7 @@
12 12
 from django.core.handlers.base import BaseHandler
13 13
 from django.core.handlers.wsgi import WSGIRequest
14 14
 from django.core.signals import got_request_exception
15  
-from django.http import SimpleCookie, HttpRequest
  15
+from django.http import SimpleCookie, HttpRequest, QueryDict
16 16
 from django.template import TemplateDoesNotExist
17 17
 from django.test import signals
18 18
 from django.utils.functional import curry
@@ -261,7 +261,7 @@ def request(self, **request):
261 261
 
262 262
         return response
263 263
 
264  
-    def get(self, path, data={}, **extra):
  264
+    def get(self, path, data={}, follow=False, **extra):
265 265
         """
266 266
         Requests a response from the server using GET.
267 267
         """
@@ -275,9 +275,13 @@ def get(self, path, data={}, **extra):
275 275
         }
276 276
         r.update(extra)
277 277
 
278  
-        return self.request(**r)
  278
+        response = self.request(**r)
  279
+        if follow:
  280
+            response = self._handle_redirects(response)
  281
+        return response
279 282
 
280  
-    def post(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
  283
+    def post(self, path, data={}, content_type=MULTIPART_CONTENT,
  284
+             follow=False, **extra):
281 285
         """
282 286
         Requests a response from the server using POST.
283 287
         """
@@ -297,9 +301,12 @@ def post(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
297 301
         }
298 302
         r.update(extra)
299 303
 
300  
-        return self.request(**r)
  304
+        response = self.request(**r)
  305
+        if follow:
  306
+            response = self._handle_redirects(response)
  307
+        return response
301 308
 
302  
-    def head(self, path, data={}, **extra):
  309
+    def head(self, path, data={}, follow=False, **extra):
303 310
         """
304 311
         Request a response from the server using HEAD.
305 312
         """
@@ -313,9 +320,12 @@ def head(self, path, data={}, **extra):
313 320
         }
314 321
         r.update(extra)
315 322
 
316  
-        return self.request(**r)
  323
+        response = self.request(**r)
  324
+        if follow:
  325
+            response = self._handle_redirects(response)
  326
+        return response
317 327
 
318  
-    def options(self, path, data={}, **extra):
  328
+    def options(self, path, data={}, follow=False, **extra):
319 329
         """
320 330
         Request a response from the server using OPTIONS.
321 331
         """
@@ -328,9 +338,13 @@ def options(self, path, data={}, **extra):
328 338
         }
329 339
         r.update(extra)
330 340
 
331  
-        return self.request(**r)
  341
+        response = self.request(**r)
  342
+        if follow:
  343
+            response = self._handle_redirects(response)
  344
+        return response
332 345
 
333  
-    def put(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
  346
+    def put(self, path, data={}, content_type=MULTIPART_CONTENT,
  347
+            follow=False, **extra):
334 348
         """
335 349
         Send a resource to the server using PUT.
336 350
         """
@@ -350,9 +364,12 @@ def put(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
350 364
         }
351 365
         r.update(extra)
352 366
 
353  
-        return self.request(**r)
  367
+        response = self.request(**r)
  368
+        if follow:
  369
+            response = self._handle_redirects(response)
  370
+        return response
354 371
 
355  
-    def delete(self, path, data={}, **extra):
  372
+    def delete(self, path, data={}, follow=False, **extra):
356 373
         """
357 374
         Send a DELETE request to the server.
358 375
         """
@@ -365,7 +382,10 @@ def delete(self, path, data={}, **extra):
365 382
         }
366 383
         r.update(extra)
367 384
 
368  
-        return self.request(**r)
  385
+        response = self.request(**r)
  386
+        if follow:
  387
+            response = self._handle_redirects(response)
  388
+        return response
369 389
 
370 390
     def login(self, **credentials):
371 391
         """
@@ -416,3 +436,27 @@ def logout(self):
416 436
         session = __import__(settings.SESSION_ENGINE, {}, {}, ['']).SessionStore()
417 437
         session.delete(session_key=self.cookies[settings.SESSION_COOKIE_NAME].value)
418 438
         self.cookies = SimpleCookie()
  439
+
  440
+    def _handle_redirects(self, response):
  441
+        "Follows any redirects by requesting responses from the server using GET."
  442
+
  443
+        response.redirect_chain = []
  444
+        while response.status_code in (301, 302, 303, 307):
  445
+            url = response['Location']
  446
+            scheme, netloc, path, query, fragment = urlsplit(url)
  447
+
  448
+            redirect_chain = response.redirect_chain
  449
+            redirect_chain.append((url, response.status_code))
  450
+
  451
+            # The test client doesn't handle external links,
  452
+            # but since the situation is simulated in test_client,
  453
+            # we fake things here by ignoring the netloc portion of the
  454
+            # redirected URL.
  455
+            response = self.get(path, QueryDict(query), follow=False)
  456
+            response.redirect_chain = redirect_chain
  457
+
  458
+            # Prevent loops
  459
+            if response.redirect_chain[-1] in response.redirect_chain[0:-1]:
  460
+                break
  461
+        return response
  462
+
65  django/test/testcases.py
@@ -43,7 +43,7 @@ def disable_transaction_methods():
43 43
     transaction.savepoint_commit = nop
44 44
     transaction.savepoint_rollback = nop
45 45
     transaction.enter_transaction_management = nop
46  
-    transaction.leave_transaction_management = nop        
  46
+    transaction.leave_transaction_management = nop
47 47
 
48 48
 def restore_transaction_methods():
49 49
     transaction.commit = real_commit
@@ -198,7 +198,7 @@ def report_unexpected_exception(self, out, test, example, exc_info):
198 198
         # Rollback, in case of database errors. Otherwise they'd have
199 199
         # side effects on other tests.
200 200
         transaction.rollback_unless_managed()
201  
-        
  201
+
202 202
 class TransactionTestCase(unittest.TestCase):
203 203
     def _pre_setup(self):
204 204
         """Performs any pre-test setup. This includes:
@@ -242,7 +242,7 @@ def __call__(self, result=None):
242 242
             import sys
243 243
             result.addError(self, sys.exc_info())
244 244
             return
245  
-        super(TransactionTestCase, self).__call__(result)        
  245
+        super(TransactionTestCase, self).__call__(result)
246 246
         try:
247 247
             self._post_teardown()
248 248
         except (KeyboardInterrupt, SystemExit):
@@ -263,7 +263,7 @@ def _post_teardown(self):
263 263
     def _fixture_teardown(self):
264 264
         pass
265 265
 
266  
-    def _urlconf_teardown(self):        
  266
+    def _urlconf_teardown(self):
267 267
         if hasattr(self, '_old_root_urlconf'):
268 268
             settings.ROOT_URLCONF = self._old_root_urlconf
269 269
             clear_url_caches()
@@ -276,25 +276,48 @@ def assertRedirects(self, response, expected_url, status_code=302,
276 276
         Note that assertRedirects won't work for external links since it uses
277 277
         TestClient to do a request.
278 278
         """
279  
-        self.assertEqual(response.status_code, status_code,
280  
-            ("Response didn't redirect as expected: Response code was %d"
281  
-             " (expected %d)" % (response.status_code, status_code)))
282  
-        url = response['Location']
283  
-        scheme, netloc, path, query, fragment = urlsplit(url)
  279
+        if hasattr(response, 'redirect_chain'):
  280
+            # The request was a followed redirect
  281
+            self.assertTrue(len(response.redirect_chain) > 0,
  282
+                ("Response didn't redirect as expected: Response code was %d"
  283
+                " (expected %d)" % (response.status_code, status_code)))
  284
+
  285
+            self.assertEqual(response.redirect_chain[0][1], status_code,
  286
+                ("Initial response didn't redirect as expected: Response code was %d"
  287
+                 " (expected %d)" % (response.redirect_chain[0][1], status_code)))
  288
+
  289
+            url, status_code = response.redirect_chain[-1]
  290
+
  291
+            self.assertEqual(response.status_code, target_status_code,
  292
+                ("Response didn't redirect as expected: Final Response code was %d"
  293
+                " (expected %d)" % (response.status_code, target_status_code)))
  294
+
  295
+        else:
  296
+            # Not a followed redirect
  297
+            self.assertEqual(response.status_code, status_code,
  298
+                ("Response didn't redirect as expected: Response code was %d"
  299
+                 " (expected %d)" % (response.status_code, status_code)))
  300
+
  301
+            url = response['Location']
  302
+            scheme, netloc, path, query, fragment = urlsplit(url)
  303
+
  304
+            redirect_response = response.client.get(path, QueryDict(query))
  305
+
  306
+            # Get the redirection page, using the same client that was used
  307
+            # to obtain the original response.
  308
+            self.assertEqual(redirect_response.status_code, target_status_code,
  309
+                ("Couldn't retrieve redirection page '%s': response code was %d"
  310
+                 " (expected %d)") %
  311
+                     (path, redirect_response.status_code, target_status_code))
  312
+
284 313
         e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url)
285 314
         if not (e_scheme or e_netloc):
286 315
             expected_url = urlunsplit(('http', host or 'testserver', e_path,
287  
-                    e_query, e_fragment))
  316
+                e_query, e_fragment))
  317
+
288 318
         self.assertEqual(url, expected_url,
289 319
             "Response redirected to '%s', expected '%s'" % (url, expected_url))
290 320
 
291  
-        # Get the redirection page, using the same client that was used
292  
-        # to obtain the original response.
293  
-        redirect_response = response.client.get(path, QueryDict(query))
294  
-        self.assertEqual(redirect_response.status_code, target_status_code,
295  
-            ("Couldn't retrieve redirection page '%s': response code was %d"
296  
-             " (expected %d)") %
297  
-                 (path, redirect_response.status_code, target_status_code))
298 321
 
299 322
     def assertContains(self, response, text, count=None, status_code=200):
300 323
         """
@@ -401,15 +424,15 @@ def assertTemplateNotUsed(self, response, template_name):
401 424
 class TestCase(TransactionTestCase):
402 425
     """
403 426
     Does basically the same as TransactionTestCase, but surrounds every test
404  
-    with a transaction, monkey-patches the real transaction management routines to 
405  
-    do nothing, and rollsback the test transaction at the end of the test. You have 
  427
+    with a transaction, monkey-patches the real transaction management routines to
  428
+    do nothing, and rollsback the test transaction at the end of the test. You have
406 429
     to use TransactionTestCase, if you need transaction management inside a test.
407 430
     """
408 431
 
409 432
     def _fixture_setup(self):
410 433
         if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
411 434
             return super(TestCase, self)._fixture_setup()
412  
-        
  435
+
413 436
         transaction.enter_transaction_management()
414 437
         transaction.managed(True)
415 438
         disable_transaction_methods()
@@ -426,7 +449,7 @@ def _fixture_setup(self):
426 449
     def _fixture_teardown(self):
427 450
         if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
428 451
             return super(TestCase, self)._fixture_teardown()
429  
-                
  452
+
430 453
         restore_transaction_methods()
431 454
         transaction.rollback()
432 455
         transaction.leave_transaction_management()
114  docs/topics/testing.txt
@@ -478,7 +478,8 @@ arguments at time of construction:
478 478
     Once you have a ``Client`` instance, you can call any of the following
479 479
     methods:
480 480
 
481  
-    .. method:: Client.get(path, data={})
  481
+    .. method:: Client.get(path, data={}, follow=False)
  482
+
482 483
 
483 484
         Makes a GET request on the provided ``path`` and returns a ``Response``
484 485
         object, which is documented below.
@@ -505,7 +506,18 @@ arguments at time of construction:
505 506
         If you provide URL both an encoded GET data and a data argument,
506 507
         the data argument will take precedence.
507 508
 
508  
-    .. method:: Client.post(path, data={}, content_type=MULTIPART_CONTENT)
  509
+        If you set ``follow`` to ``True`` the client will follow any redirects
  510
+        and a ``redirect_chain`` attribute will be set in the response object
  511
+        containing tuples of the intermediate urls and status codes.
  512
+
  513
+        If you had an url ``/redirect_me/`` that redirected to ``/next/``, that
  514
+        redirected to ``/final/``, this is what you'd see::
  515
+
  516
+            >>> response = c.get('/redirect_me/')
  517
+            >>> response.redirect_chain
  518
+            [(u'http://testserver/next/', 302), (u'http://testserver/final/', 302)]
  519
+
  520
+    .. method:: Client.post(path, data={}, content_type=MULTIPART_CONTENT, follow=False)
509 521
 
510 522
         Makes a POST request on the provided ``path`` and returns a
511 523
         ``Response`` object, which is documented below.
@@ -556,7 +568,7 @@ arguments at time of construction:
556 568
         Note that you should manually close the file after it has been provided
557 569
         to ``post()``.
558 570
 
559  
-        .. versionadded:: development
  571
+        .. versionchanged:: 1.1
560 572
 
561 573
         If the URL you request with a POST contains encoded parameters, these
562 574
         parameters will be made available in the request.GET data. For example,
@@ -568,7 +580,11 @@ arguments at time of construction:
568 580
         to retrieve the username and password, and could interrogate request.GET
569 581
         to determine if the user was a visitor.
570 582
 
571  
-    .. method:: Client.head(path, data={})
  583
+        If you set ``follow`` to ``True`` the client will follow any redirects
  584
+        and a ``redirect_chain`` attribute will be set in the response object
  585
+        containing tuples of the intermediate urls and status codes.
  586
+
  587
+    .. method:: Client.head(path, data={}, follow=False)
572 588
 
573 589
         .. versionadded:: development
574 590
 
@@ -576,14 +592,22 @@ arguments at time of construction:
576 592
         object. Useful for testing RESTful interfaces. Acts just like
577 593
         :meth:`Client.get` except it does not return a message body.
578 594
 
579  
-    .. method:: Client.options(path, data={})
  595
+        If you set ``follow`` to ``True`` the client will follow any redirects
  596
+        and a ``redirect_chain`` attribute will be set in the response object
  597
+        containing tuples of the intermediate urls and status codes.
  598
+
  599
+    .. method:: Client.options(path, data={}, follow=False)
580 600
 
581 601
         .. versionadded:: development
582 602
 
583 603
         Makes an OPTIONS request on the provided ``path`` and returns a
584 604
         ``Response`` object. Useful for testing RESTful interfaces.
585 605
 
586  
-    .. method:: Client.put(path, data={}, content_type=MULTIPART_CONTENT)
  606
+        If you set ``follow`` to ``True`` the client will follow any redirects
  607
+        and a ``redirect_chain`` attribute will be set in the response object
  608
+        containing tuples of the intermediate urls and status codes.
  609
+
  610
+    .. method:: Client.put(path, data={}, content_type=MULTIPART_CONTENT, follow=False)
587 611
 
588 612
         .. versionadded:: development
589 613
 
@@ -591,13 +615,21 @@ arguments at time of construction:
591 615
         ``Response`` object. Useful for testing RESTful interfaces. Acts just
592 616
         like :meth:`Client.post` except with the PUT request method.
593 617
 
594  
-    .. method:: Client.delete(path)
  618
+        If you set ``follow`` to ``True`` the client will follow any redirects
  619
+        and a ``redirect_chain`` attribute will be set in the response object
  620
+        containing tuples of the intermediate urls and status codes.
  621
+
  622
+    .. method:: Client.delete(path, follow=False)
595 623
 
596 624
         .. versionadded:: development
597 625
 
598 626
         Makes an DELETE request on the provided ``path`` and returns a
599 627
         ``Response`` object. Useful for testing RESTful interfaces.
600 628
 
  629
+        If you set ``follow`` to ``True`` the client will follow any redirects
  630
+        and a ``redirect_chain`` attribute will be set in the response object
  631
+        containing tuples of the intermediate urls and status codes.
  632
+
601 633
     .. method:: Client.login(**credentials)
602 634
 
603 635
         .. versionadded:: 1.0
@@ -789,47 +821,47 @@ additions.
789 821
 
790 822
 .. class:: TransactionTestCase()
791 823
 
792  
-Django ``TestCase`` classes make use of database transaction facilities, if 
793  
-available, to speed up the process of resetting the database to a known state 
794  
-at the beginning of each test. A consequence of this, however, is that the 
795  
-effects of transaction commit and rollback cannot be tested by a Django 
796  
-``TestCase`` class. If your test requires testing of such transactional 
  824
+Django ``TestCase`` classes make use of database transaction facilities, if
  825
+available, to speed up the process of resetting the database to a known state
  826
+at the beginning of each test. A consequence of this, however, is that the
  827
+effects of transaction commit and rollback cannot be tested by a Django
  828
+``TestCase`` class. If your test requires testing of such transactional
797 829
 behavior, you should use a Django ``TransactionTestCase``.
798 830
 
799  
-``TransactionTestCase`` and ``TestCase`` are identical except for the manner 
800  
-in which the database is reset to a known state and the ability for test code 
801  
-to test the effects of commit and rollback. A ``TranscationTestCase`` resets 
802  
-the database before the test runs by truncating all tables and reloading 
803  
-initial data. A ``TransactionTestCase`` may call commit and rollback and 
804  
-observe the effects of these calls on the database.  
805  
-
806  
-A ``TestCase``, on the other hand, does not truncate tables and reload initial 
807  
-data at the beginning of a test. Instead, it encloses the test code in a 
808  
-database transaction that is rolled back at the end of the test.  It also 
809  
-prevents the code under test from issuing any commit or rollback operations 
810  
-on the database, to ensure that the rollback at the end of the test restores 
811  
-the database to its initial state. In order to guarantee that all ``TestCase`` 
812  
-code starts with a clean database, the Django test runner runs all ``TestCase`` 
813  
-tests first, before any other tests (e.g. doctests) that may alter the 
  831
+``TransactionTestCase`` and ``TestCase`` are identical except for the manner
  832
+in which the database is reset to a known state and the ability for test code
  833
+to test the effects of commit and rollback. A ``TranscationTestCase`` resets
  834
+the database before the test runs by truncating all tables and reloading
  835
+initial data. A ``TransactionTestCase`` may call commit and rollback and
  836
+observe the effects of these calls on the database.
  837
+
  838
+A ``TestCase``, on the other hand, does not truncate tables and reload initial
  839
+data at the beginning of a test. Instead, it encloses the test code in a
  840
+database transaction that is rolled back at the end of the test.  It also
  841
+prevents the code under test from issuing any commit or rollback operations
  842
+on the database, to ensure that the rollback at the end of the test restores
  843
+the database to its initial state. In order to guarantee that all ``TestCase``
  844
+code starts with a clean database, the Django test runner runs all ``TestCase``
  845
+tests first, before any other tests (e.g. doctests) that may alter the
814 846
 database without restoring it to its original state.
815 847
 
816  
-When running on a database that does not support rollback (e.g. MySQL with the 
817  
-MyISAM storage engine), ``TestCase`` falls back to initializing the database 
  848
+When running on a database that does not support rollback (e.g. MySQL with the
  849
+MyISAM storage engine), ``TestCase`` falls back to initializing the database
818 850
 by truncating tables and reloading initial data.
819 851
 
820 852
 
821 853
 .. note::
822  
-    The ``TestCase`` use of rollback to un-do the effects of the test code 
823  
-    may reveal previously-undetected errors in test code.  For example, 
824  
-    test code that assumes primary keys values will be assigned starting at 
825  
-    one may find that assumption no longer holds true when rollbacks instead 
826  
-    of table truncation are being used to reset the database.  Similarly, 
827  
-    the reordering of tests so that all ``TestCase`` classes run first may 
828  
-    reveal unexpected dependencies on test case ordering.  In such cases a 
  854
+    The ``TestCase`` use of rollback to un-do the effects of the test code
  855
+    may reveal previously-undetected errors in test code.  For example,
  856
+    test code that assumes primary keys values will be assigned starting at
  857
+    one may find that assumption no longer holds true when rollbacks instead
  858
+    of table truncation are being used to reset the database.  Similarly,
  859
+    the reordering of tests so that all ``TestCase`` classes run first may
  860
+    reveal unexpected dependencies on test case ordering.  In such cases a
829 861
     quick fix is to switch the ``TestCase`` to a ``TransactionTestCase``.
830 862
     A better long-term fix, that allows the test to take advantage of the
831 863
     speed benefit of ``TestCase``, is to fix the underlying test problem.
832  
-              
  864
+
833 865
 
834 866
 Default test client
835 867
 ~~~~~~~~~~~~~~~~~~~
@@ -1028,9 +1060,15 @@ applications:
1028 1060
 .. method:: assertRedirects(response, expected_url, status_code=302, target_status_code=200)
1029 1061
 
1030 1062
     Asserts that the response return a ``status_code`` redirect status, it
1031  
-    redirected to ``expected_url`` (including any GET data), and the subsequent
  1063
+    redirected to ``expected_url`` (including any GET data), and the final
1032 1064
     page was received with ``target_status_code``.
1033 1065
 
  1066
+    .. versionadded:: 1.1
  1067
+
  1068
+    If your request used the ``follow`` argument, the ``expected_url`` and
  1069
+    ``target_status_code`` will be the url and status code for the final
  1070
+    point of the redirect chain.
  1071
+
1034 1072
 E-mail services
1035 1073
 ---------------
1036 1074
 
12  tests/modeltests/test_client/models.py
@@ -70,13 +70,13 @@ def test_post(self):
70 70
         self.assertEqual(response.context['data'], '37')
71 71
         self.assertEqual(response.template.name, 'POST Template')
72 72
         self.failUnless('Data received' in response.content)
73  
-    
  73
+
74 74
     def test_response_headers(self):
75 75
         "Check the value of HTTP headers returned in a response"
76 76
         response = self.client.get("/test_client/header_view/")
77  
-        
  77
+
78 78
         self.assertEquals(response['X-DJANGO-TEST'], 'Slartibartfast')
79  
-        
  79
+
80 80
     def test_raw_post(self):
81 81
         "POST raw data (with a content type) to a view"
82 82
         test_doc = """<?xml version="1.0" encoding="utf-8"?><library><book><title>Blink</title><author>Malcolm Gladwell</author></book></library>"""
@@ -132,6 +132,12 @@ def test_redirect_to_strange_location(self):
132 132
         # the attempt to get the redirection location returned 301 when retrieved
133 133
         self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/', target_status_code=301)
134 134
 
  135
+    def test_follow_redirect(self):
  136
+        "A URL that redirects can be followed to termination."
  137
+        response = self.client.get('/test_client/double_redirect_view/', follow=True)
  138
+        self.assertRedirects(response, 'http://testserver/test_client/get_view/', status_code=302, target_status_code=200)
  139
+        self.assertEquals(len(response.redirect_chain), 2)
  140
+
135 141
     def test_notfound_response(self):
136 142
         "GET a URL that responds as '404:Not Found'"
137 143
         response = self.client.get('/test_client/bad_view/')
101  tests/regressiontests/test_client_regress/models.py
@@ -148,6 +148,107 @@ def test_target_page(self):
148 148
         except AssertionError, e:
149 149
             self.assertEquals(str(e), "Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)")
150 150
 
  151
+    def test_redirect_chain(self):
  152
+        "You can follow a redirect chain of multiple redirects"
  153
+        response = self.client.get('/test_client_regress/redirects/further/more/', {}, follow=True)
  154
+        self.assertRedirects(response, '/test_client_regress/no_template_view/',
  155
+            status_code=301, target_status_code=200)
  156
+
  157
+        self.assertEquals(len(response.redirect_chain), 1)
  158
+        self.assertEquals(response.redirect_chain[0], ('http://testserver/test_client_regress/no_template_view/', 301))
  159
+
  160
+    def test_multiple_redirect_chain(self):
  161
+        "You can follow a redirect chain of multiple redirects"
  162
+        response = self.client.get('/test_client_regress/redirects/', {}, follow=True)
  163
+        self.assertRedirects(response, '/test_client_regress/no_template_view/',
  164
+            status_code=301, target_status_code=200)
  165
+
  166
+        self.assertEquals(len(response.redirect_chain), 3)
  167
+        self.assertEquals(response.redirect_chain[0], ('http://testserver/test_client_regress/redirects/further/', 301))
  168
+        self.assertEquals(response.redirect_chain[1], ('http://testserver/test_client_regress/redirects/further/more/', 301))
  169
+        self.assertEquals(response.redirect_chain[2], ('http://testserver/test_client_regress/no_template_view/', 301))
  170
+
  171
+    def test_redirect_chain_to_non_existent(self):
  172
+        "You can follow a chain to a non-existent view"
  173
+        response = self.client.get('/test_client_regress/redirect_to_non_existent_view2/', {}, follow=True)
  174
+        self.assertRedirects(response, '/test_client_regress/non_existent_view/',
  175
+            status_code=301, target_status_code=404)
  176
+
  177
+    def test_redirect_chain_to_self(self):
  178
+        "Redirections to self are caught and escaped"
  179
+        response = self.client.get('/test_client_regress/redirect_to_self/', {}, follow=True)
  180
+        # The chain of redirects stops once the cycle is detected.
  181
+        self.assertRedirects(response, '/test_client_regress/redirect_to_self/',
  182
+            status_code=301, target_status_code=301)
  183
+        self.assertEquals(len(response.redirect_chain), 2)
  184
+
  185
+    def test_circular_redirect(self):
  186
+        "Circular redirect chains are caught and escaped"
  187
+        response = self.client.get('/test_client_regress/circular_redirect_1/', {}, follow=True)
  188
+        # The chain of redirects will get back to the starting point, but stop there.
  189
+        self.assertRedirects(response, '/test_client_regress/circular_redirect_2/',
  190
+            status_code=301, target_status_code=301)
  191
+        self.assertEquals(len(response.redirect_chain), 4)
  192
+
  193
+    def test_redirect_chain_post(self):
  194
+        "A redirect chain will be followed from an initial POST post"
  195
+        response = self.client.post('/test_client_regress/redirects/',
  196
+            {'nothing': 'to_send'}, follow=True)
  197
+        self.assertRedirects(response,
  198
+            '/test_client_regress/no_template_view/', 301, 200)
  199
+        self.assertEquals(len(response.redirect_chain), 3)
  200
+
  201
+    def test_redirect_chain_head(self):
  202
+        "A redirect chain will be followed from an initial HEAD request"
  203
+        response = self.client.head('/test_client_regress/redirects/',
  204
+            {'nothing': 'to_send'}, follow=True)
  205
+        self.assertRedirects(response,
  206
+            '/test_client_regress/no_template_view/', 301, 200)
  207
+        self.assertEquals(len(response.redirect_chain), 3)
  208
+
  209
+    def test_redirect_chain_options(self):
  210
+        "A redirect chain will be followed from an initial OPTIONS request"
  211
+        response = self.client.options('/test_client_regress/redirects/',
  212
+            {'nothing': 'to_send'}, follow=True)
  213
+        self.assertRedirects(response,
  214
+            '/test_client_regress/no_template_view/', 301, 200)
  215
+        self.assertEquals(len(response.redirect_chain), 3)
  216
+
  217
+    def test_redirect_chain_put(self):
  218
+        "A redirect chain will be followed from an initial PUT request"
  219
+        response = self.client.put('/test_client_regress/redirects/',
  220
+            {'nothing': 'to_send'}, follow=True)
  221
+        self.assertRedirects(response,
  222
+            '/test_client_regress/no_template_view/', 301, 200)
  223
+        self.assertEquals(len(response.redirect_chain), 3)
  224
+
  225
+    def test_redirect_chain_delete(self):
  226
+        "A redirect chain will be followed from an initial DELETE request"
  227
+        response = self.client.delete('/test_client_regress/redirects/',
  228
+            {'nothing': 'to_send'}, follow=True)
  229
+        self.assertRedirects(response,
  230
+            '/test_client_regress/no_template_view/', 301, 200)
  231
+        self.assertEquals(len(response.redirect_chain), 3)
  232
+
  233
+    def test_redirect_chain_on_non_redirect_page(self):
  234
+        "An assertion is raised if the original page couldn't be retrieved as expected"
  235
+        # This page will redirect with code 301, not 302
  236
+        response = self.client.get('/test_client/get_view/', follow=True)
  237
+        try:
  238
+            self.assertRedirects(response, '/test_client/get_view/')
  239
+        except AssertionError, e:
  240
+            self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 200 (expected 302)")
  241
+
  242
+    def test_redirect_on_non_redirect_page(self):
  243
+        "An assertion is raised if the original page couldn't be retrieved as expected"
  244
+        # This page will redirect with code 301, not 302
  245
+        response = self.client.get('/test_client/get_view/')
  246
+        try:
  247
+            self.assertRedirects(response, '/test_client/get_view/')
  248
+        except AssertionError, e:
  249
+            self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 200 (expected 302)")
  250
+
  251
+
151 252
 class AssertFormErrorTests(TestCase):
152 253
     def test_unknown_form(self):
153 254
         "An assertion is raised if the form name is unknown"
10  tests/regressiontests/test_client_regress/urls.py
... ...
@@ -1,4 +1,5 @@
1 1
 from django.conf.urls.defaults import *
  2
+from django.views.generic.simple import redirect_to
2 3
 import views
3 4
 
4 5
 urlpatterns = patterns('',
@@ -8,6 +9,15 @@
8 9
     (r'^request_data/$', views.request_data),
9 10
     url(r'^arg_view/(?P<name>.+)/$', views.view_with_argument, name='arg_view'),
10 11
     (r'^login_protected_redirect_view/$', views.login_protected_redirect_view),
  12
+    (r'^redirects/$', redirect_to, {'url': '/test_client_regress/redirects/further/'}),
  13
+    (r'^redirects/further/$', redirect_to, {'url': '/test_client_regress/redirects/further/more/'}),
  14
+    (r'^redirects/further/more/$', redirect_to, {'url': '/test_client_regress/no_template_view/'}),
  15
+    (r'^redirect_to_non_existent_view/$', redirect_to, {'url': '/test_client_regress/non_existent_view/'}),
  16
+    (r'^redirect_to_non_existent_view2/$', redirect_to, {'url': '/test_client_regress/redirect_to_non_existent_view/'}),
  17
+    (r'^redirect_to_self/$', redirect_to, {'url': '/test_client_regress/redirect_to_self/'}),
  18
+    (r'^circular_redirect_1/$', redirect_to, {'url': '/test_client_regress/circular_redirect_2/'}),
  19
+    (r'^circular_redirect_2/$', redirect_to, {'url': '/test_client_regress/circular_redirect_3/'}),
  20
+    (r'^circular_redirect_3/$', redirect_to, {'url': '/test_client_regress/circular_redirect_1/'}),
11 21
     (r'^set_session/$', views.set_session_view),
12 22
     (r'^check_session/$', views.check_session_view),
13 23
     (r'^request_methods/$', views.request_methods_view),
2  tests/regressiontests/test_client_regress/views.py
@@ -58,4 +58,4 @@ def check_session_view(request):
58 58
 
59 59
 def request_methods_view(request):
60 60
     "A view that responds with the request method"
61  
-    return HttpResponse('request method: %s' % request.method)
  61
+    return HttpResponse('request method: %s' % request.method)

0 notes on commit e735fe7

Please sign in to comment.
Something went wrong with that request. Please try again.