Index: django/test/testcases.py
===================================================================
--- django/test/testcases.py	(revision 6122)
+++ django/test/testcases.py	(working copy)
@@ -190,3 +190,74 @@
         self.failIf(template_name in template_names,
             (u"Template '%s' was used unexpectedly in rendering the"
              u" response") % template_name)
+
+    def assertNoBrokenLinks(self, response, internal_only=True):
+        """
+        Asserts that all the links within the response, when followed, return
+        a valid page (a 200) or a redirect (302).
+        
+        Current issues/thoughts:
+          * Should we follow 302's to verify the page redirects to a 200 result?
+        """
+        non_broken_status_codes = (200, 301, 302, 304)
+
+        # Create the parser to grab the internal and external links
+        import HTMLParser
+
+        class AnchorParser(HTMLParser.HTMLParser):
+            external_href_re = re.compile(r'^https?://', re.IGNORECASE)
+            ignore_href_re = re.compile(r'^(mailto|ftp):', re.IGNORECASE)
+            
+            def __init__(self):
+                self.hrefs_internal = []
+                self.hrefs_external = []
+                self.reset()
+        
+            def handle_starttag(self, tag, attrs):
+                if tag == "a":                        
+                    for k, v in attrs:
+                        if k == "href":
+                            if self.ignore_href_re.match(v):
+                                break
+                            
+                            if self.external_href_re.match(v):
+                                self.hrefs_external.append(v)
+                            else:
+                                self.hrefs_internal.append(v)
+                            break
+
+        p = AnchorParser()
+        p.feed(response.content)
+
+        # Check the internal links first:
+        for link in p.hrefs_internal:
+            link_response = response.client.get(link)
+            self.failUnless(
+                link_response.status_code in non_broken_status_codes,
+                (u"The link '%(link)s' appears to be broken (status is %(status)s)") % {
+                    'link': link,
+                    'status': link_response.status_code                                                        
+                }           
+            )
+
+        # Then check the external links
+        if not internal_only:
+            import urllib2
+            from django.conf import settings
+
+            headers = {
+                "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
+                "Accept-Language": "en-us,en;q=0.5",
+                "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7",
+                "Connection": "close",
+                "User-Agent": settings.URL_VALIDATOR_USER_AGENT,
+            }
+
+            for link in p.hrefs_external:
+                try:
+                    req = urllib2.Request(link, None, headers)
+                    u = urllib2.urlopen(req)
+                except ValueError:
+                    self.fail(u"The URL '%s' appears to be invalid." % link)
+                except: # urllib2.URLError, httplib.InvalidURL, etc.
+                    self.fail(u"The URL '%s' appears to be a broken link." % link)
Index: tests/regressiontests/test_client_regress/views.py
===================================================================
--- tests/regressiontests/test_client_regress/views.py	(revision 6122)
+++ tests/regressiontests/test_client_regress/views.py	(working copy)
@@ -1,6 +1,6 @@
 from django.contrib.auth.decorators import login_required
 from django.core.mail import EmailMessage, SMTPConnection
-from django.http import HttpResponse, HttpResponseRedirect, HttpResponseServerError
+from django.http import HttpResponse, HttpResponseRedirect, HttpResponseServerError, HttpResponseNotFound
 from django.shortcuts import render_to_response
 
 def no_template_view(request):
@@ -27,4 +27,82 @@
 def login_protected_redirect_view(request):
     "A view that redirects all requests to the GET view"
     return HttpResponseRedirect('/test_client_regress/get_view/')
-login_protected_redirect_view = login_required(login_protected_redirect_view)
\ No newline at end of file
+login_protected_redirect_view = login_required(login_protected_redirect_view)
+
+def no_broken_links_view(request):
+    return HttpResponse(
+    """
+    Lots of html stuff, including a few local links:
+      <a href="/test_client_regress/get_view/">here</a>
+      <a href="/test_client_regress/file_upload">This should redirect to add the /</a>
+      <a some="attribute" href="/test_client_regress/no_template_view/">
+        a redirect view. Doesn't matter if closing tag missing.
+        
+    Some external links:
+      <a href="http://djangoproject.com/weblog/">Django blog</a>
+
+    Some anchor links should be ignored, such as:
+      <a href="mailto:me@example.com">Send me an email</a>, or
+      <a href="ftp://example.com">Download from here</a>
+    
+    Including a <a lats of bad="stuff" href="http://djangoproject.com">Missing slash</a>
+    Whole bunch of other stuff before the page ends.
+    """                                     
+    )
+    
+def broken_external_link_view(request):
+    return HttpResponse(
+    """
+    Lots of html stuff, including a few local links:
+      <a href="/test_client_regress/get_view/">here</a>
+      <a href="/test_client_regress/file_upload">This should redirect to add the /</a>
+      <a some="attribute" href="/test_client_regress/no_template_view/">
+        a redirect view. Doesn't matter if closing tag missing.
+        
+    Some external links:
+      <a class="this one's fine" href="http://djangoproject.com/weblog">Django blog</a>
+    But this one's a 
+      <a href="http://djangoproject.com/badlink.html">Broken link</a>
+    Whole bunch of other stuff before the page ends.
+    """                                     
+    )
+    
+def broken_internal_link_view(request):
+    return HttpResponse(
+    """
+    Lots of html stuff, including a few local links:
+
+      <a class="test" href="/test_client_regress/broken_view/">A broken view</a>
+        
+    """                                     
+    )
+
+def bad_internal_link_view(request):
+    return HttpResponse(
+    """
+    Lots of html stuff, including a few local links:
+
+      <a class="test" href="/test_client_regress/bad_view/">A bad view</a>
+    
+    """                                     
+    )
+    
+def invalid_external_link_view(request):
+    return HttpResponse(
+    """
+    Lots of html stuff, including a few local links:
+
+      <a class="test" href="http://djangoproject&.com">An invalid link</a>
+        
+    """                                     
+    )
+
+def broken_view(request):
+    return HttpResponseServerError()
+
+def bad_view(request):
+    return HttpResponseNotFound()
+
+
+
+    
\ No newline at end of file
Index: tests/regressiontests/test_client_regress/models.py
===================================================================
--- tests/regressiontests/test_client_regress/models.py	(revision 6122)
+++ tests/regressiontests/test_client_regress/models.py	(working copy)
@@ -233,6 +233,80 @@
         except AssertionError, e:
             self.assertEqual(str(e), "The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )")        
 
+class AssertNoBrokenLinksTests(TestCase):
+    def test_no_broken_links(self):
+        "Tests that assertion confirms internal and external non-broken links."
+        
+        response = self.client.get('/test_client_regress/no_broken_links_view/')
+        self.assertEqual(response.status_code, 200)
+                
+        self.assertNoBrokenLinks(response, internal_only=False)
+        
+    def test_broken_external_link(self):
+        "Tests that assertion finds broken external links"
+
+        response = self.client.get('/test_client_regress/broken_external_link_view/')
+        self.assertEqual(response.status_code, 200)
+        
+        # No internal broken links:
+        self.assertNoBrokenLinks(response)
+
+        # But there is an external broken link:
+        assertion_raised=True # Just to make sure we can check that the error was raised.
+        try:
+            self.assertNoBrokenLinks(response, internal_only=False)
+            essertion_raised = False # Should not get here
+        except AssertionError, e:
+            self.assertEqual(str(e), "The URL 'http://djangoproject.com/badlink.html' appears to be a broken link.")
+            
+        self.assertTrue(assertion_raised)
+
+    def test_invalid_external_link(self):
+        "Tests that assertion finds invalid external links"
+
+        response = self.client.get('/test_client_regress/invalid_external_link_view/')
+        self.assertEqual(response.status_code, 200)
+        
+        assertion_raised=True 
+        try:
+            self.assertNoBrokenLinks(response, internal_only=False)
+            essertion_raised = False # Should not get here
+        except AssertionError, e:
+            self.assertEqual(str(e), "The URL 'http://djangoproject&.com' appears to be a broken link.")
+            
+        self.assertTrue(assertion_raised)
+        
+    def test_broken_internal_link(self):
+        "Tests that assertion finds broken internal links"
+
+        response = self.client.get('/test_client_regress/broken_internal_link_view/')
+        self.assertEqual(response.status_code, 200)
+        
+        assertion_raised=True 
+        try:
+            self.assertNoBrokenLinks(response)
+            essertion_raised = False # Should not get here
+        except AssertionError, e:
+            self.assertEqual(str(e), "The link '/test_client_regress/broken_view/' appears to be broken (status is 500)")
+            
+        self.assertTrue(assertion_raised)
+        
+    def test_bad_internal_link(self):
+        "Tests that assertion finds bad internal links"
+
+        response = self.client.get('/test_client_regress/bad_internal_link_view/')
+        self.assertEqual(response.status_code, 200)
+        
+        assertion_raised=True 
+        try:
+            self.assertNoBrokenLinks(response)
+            essertion_raised = False # Should not get here
+        except AssertionError, e:
+            self.assertEqual(str(e), "The link '/test_client_regress/bad_view/' appears to be broken (status is 404)")
+            
+        self.assertTrue(assertion_raised)
+
+
 class FileUploadTests(TestCase):
     def test_simple_upload(self):
         fd = open(os.path.join(os.path.dirname(__file__), "views.py"))
Index: tests/regressiontests/test_client_regress/urls.py
===================================================================
--- tests/regressiontests/test_client_regress/urls.py	(revision 6122)
+++ tests/regressiontests/test_client_regress/urls.py	(working copy)
@@ -5,5 +5,12 @@
     (r'^no_template_view/$', views.no_template_view),
     (r'^file_upload/$', views.file_upload_view),
     (r'^get_view/$', views.get_view),
-    (r'^login_protected_redirect_view/$', views.login_protected_redirect_view)
+    (r'^login_protected_redirect_view/$', views.login_protected_redirect_view),
+    (r'^no_broken_links_view/$', views.no_broken_links_view),
+    (r'^broken_external_link_view/$', views.broken_external_link_view),
+    (r'^broken_internal_link_view/$', views.broken_internal_link_view),
+    (r'^bad_internal_link_view/$', views.bad_internal_link_view),
+    (r'^invalid_external_link_view/$', views.invalid_external_link_view),
+    (r'^broken_view/$', views.broken_view),
+    (r'^bad_view/$', views.bad_view)
 )
Index: AUTHORS
===================================================================
--- AUTHORS	(revision 6122)
+++ AUTHORS	(working copy)
@@ -199,6 +199,7 @@
     Jason McBrayer <http://www.carcosa.net/jason/>
     mccutchen@gmail.com
     michael.mcewan@gmail.com
+    Michael Nelson <http://liveandletlearn.net/>
     mikko@sorl.net
     Slawek Mikula <slawek dot mikula at gmail dot com>
     mitakummaa@gmail.com
Index: docs/testing.txt
===================================================================
--- docs/testing.txt	(revision 6122)
+++ docs/testing.txt	(working copy)
@@ -842,6 +842,13 @@
 
     The name is a string such as ``'admin/index.html'``.
 
+``assertNoBrokenLinks(response, internal_only=True)``
+    Asserts that all the anchor links within the response are not broken (ie.
+    result in a status of 200 or a redirect). By default only internal links will 
+    be checked (ie. those not beginning with http:// or https://). Note: As this
+    assertion effectively clicks on all the links within the response, care needs
+    to be taken if any link has a side effect (such as modifying your database).
+
 E-mail services
 ---------------
 
