Refs #26666 -- Added ALLOWED_HOSTS validation when running tests.

Also used ALLOWED_HOSTS to check for external hosts in assertRedirects().
This commit is contained in:
Tobias McNulty 2016-06-03 15:02:38 -07:00 committed by Tim Graham
parent 00551c3eff
commit 17e661641d
11 changed files with 105 additions and 12 deletions

View File

@ -29,6 +29,7 @@ from django.core.servers.basehttp import WSGIRequestHandler, WSGIServer
from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
from django.forms.fields import CharField
from django.http import QueryDict
from django.http.request import split_domain_port, validate_host
from django.test.client import Client
from django.test.html import HTMLParseError, parse_html
from django.test.signals import setting_changed, template_rendered
@ -306,10 +307,13 @@ class SimpleTestCase(unittest.TestCase):
# netloc might be empty, or in cases where Django tests the
# HTTP scheme, the convention is for netloc to be 'testserver'.
# Trust both as "internal" URLs here.
if netloc and netloc != 'testserver':
domain, port = split_domain_port(netloc)
if domain and not validate_host(domain, settings.ALLOWED_HOSTS):
raise ValueError(
"The Django test client is unable to fetch remote URLs (got %s). "
"Use assertRedirects(..., fetch_redirect_response=False) instead." % url
"The test client is unable to fetch remote URLs (got %s). "
"If the host is served by Django, add '%s' to ALLOWED_HOSTS. "
"Otherwise, use assertRedirects(..., fetch_redirect_response=False)."
% (url, domain)
)
redirect_response = response.client.get(path, QueryDict(query), secure=(scheme == 'https'))
@ -1324,9 +1328,12 @@ class LiveServerTestCase(TransactionTestCase):
conn.allow_thread_sharing = True
connections_override[conn.alias] = conn
# Launch the live server's thread
specified_address = os.environ.get(
'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081-8179')
cls._live_server_modified_settings = modify_settings(
ALLOWED_HOSTS={'append': specified_address.split(':')[0]},
)
cls._live_server_modified_settings.enable()
# The specified ports may be of the form '8000-8010,8080,9200-9300'
# i.e. a comma-separated list of ports or ranges of ports, so we break
@ -1348,6 +1355,7 @@ class LiveServerTestCase(TransactionTestCase):
except Exception:
msg = 'Invalid address ("%s") for live server.' % specified_address
six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), sys.exc_info()[2])
# Launch the live server's thread
cls.server_thread = cls._create_server_thread(host, possible_ports, connections_override)
cls.server_thread.daemon = True
cls.server_thread.start()
@ -1386,6 +1394,7 @@ class LiveServerTestCase(TransactionTestCase):
@classmethod
def tearDownClass(cls):
cls._tearDownClassInternal()
cls._live_server_modified_settings.disable()
super(LiveServerTestCase, cls).tearDownClass()

View File

@ -111,7 +111,8 @@ def setup_test_environment():
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
request._original_allowed_hosts = settings.ALLOWED_HOSTS
settings.ALLOWED_HOSTS = ['*']
# Add the default host of the test client.
settings.ALLOWED_HOSTS = settings.ALLOWED_HOSTS + ['testserver']
mail.outbox = []

View File

@ -90,14 +90,18 @@ If the ``Host`` header (or ``X-Forwarded-Host`` if
list, the :meth:`django.http.HttpRequest.get_host()` method will raise
:exc:`~django.core.exceptions.SuspiciousOperation`.
When :setting:`DEBUG` is ``True`` or when running tests, host validation is
disabled; any host will be accepted. Thus it's usually only necessary to set it
in production.
When :setting:`DEBUG` is ``True``, host validation is disabled; any host will
be accepted. ``ALLOWED_HOSTS`` is :ref:`checked when running tests
<topics-testing-advanced-multiple-hosts>`.
This validation only applies via :meth:`~django.http.HttpRequest.get_host()`;
if your code accesses the ``Host`` header directly from ``request.META`` you
are bypassing this security protection.
.. versionchanged:: 1.11
In older versions, ``ALLOWED_HOSTS`` wasn't checked when running tests.
.. setting:: APPEND_SLASH
``APPEND_SLASH``

View File

@ -262,6 +262,11 @@ Miscellaneous
* CSRF failures are logged to the ``django.security.csrf ``` logger instead of
``django.request``.
* :setting:`ALLOWED_HOSTS` validation is no longer disabled when running tests.
If your application includes tests with custom host names, you must include
those host names in :setting:`ALLOWED_HOSTS`. See
:ref:`topics-testing-advanced-multiple-hosts`.
* Using a foreign key's id (e.g. ``'field_id'``) in ``ModelAdmin.list_display``
displays the related object's ID. Remove the ``_id`` suffix if you want the
old behavior of the string representation of the object.

View File

@ -498,6 +498,7 @@ multiline
multilinestring
multipart
multipolygon
multitenancy
multithreaded
multithreading
Mumbai

View File

@ -67,6 +67,61 @@ The following is a simple unit test using the request factory::
response = MyView.as_view()(request)
self.assertEqual(response.status_code, 200)
.. _topics-testing-advanced-multiple-hosts:
Tests and multiple host names
=============================
The :setting:`ALLOWED_HOSTS` setting is validated when running tests. This
allows the test client to differentiate between internal and external URLs.
Projects that support multitenancy or otherwise alter business logic based on
the request's host and use custom host names in tests must include those hosts
in :setting:`ALLOWED_HOSTS`.
The first and simplest option to do so is to add the hosts to your settings
file. For example, the test suite for docs.djangoproject.com includes the
following::
from django.test import TestCase
class SearchFormTestCase(TestCase):
def test_empty_get(self):
response = self.client.get('/en/dev/search/', HTTP_HOST='docs.djangoproject.dev:8000')
self.assertEqual(response.status_code, 200)
and the settings file includes a list of the domains supported by the project::
ALLOWED_HOSTS = [
'www.djangoproject.dev',
'docs.djangoproject.dev',
...
]
Another option is to add the required hosts to :setting:`ALLOWED_HOSTS` using
:meth:`~django.test.override_settings()` or
:attr:`~django.test.SimpleTestCase.modify_settings()`. This option may be
preferable in standalone apps that can't package their own settings file or
for projects where the list of domains is not static (e.g., subdomains for
multitenancy). For example, you could write a test for the domain
``http://otherserver/`` as follows::
from django.test import TestCase, override_settings
class MultiDomainTestCase(TestCase):
@override_settings(ALLOWED_HOSTS=['otherserver'])
def test_other_domain(self):
response = self.client.get('http://otherserver/foo/bar/')
Disabling :setting:`ALLOWED_HOSTS` checking (``ALLOWED_HOSTS = ['*']``) when
running tests prevents the test client from raising a helpful error message if
you follow a redirect to an external URL.
.. versionchanged:: 1.11
Older versions didn't validate ``ALLOWED_HOSTS`` while testing so these
techniques weren't necessary.
.. _topics-testing-advanced-multidb:
Tests and multiple databases

View File

@ -1393,6 +1393,7 @@ class DefaultNonExpiringCacheKeyTests(SimpleTestCase):
},
},
USE_I18N=False,
ALLOWED_HOSTS=['.example.com'],
)
class CacheUtils(SimpleTestCase):
"""TestCase for django.utils.cache functions."""

View File

@ -142,6 +142,7 @@ class SitesFrameworkTests(TestCase):
with self.assertRaises(ValidationError):
site.full_clean()
@override_settings(ALLOWED_HOSTS=['example.com'])
def test_clear_site_cache(self):
request = HttpRequest()
request.META = {
@ -162,7 +163,7 @@ class SitesFrameworkTests(TestCase):
clear_site_cache(Site, instance=self.site, using='default')
self.assertEqual(models.SITE_CACHE, {})
@override_settings(SITE_ID='')
@override_settings(SITE_ID='', ALLOWED_HOSTS=['example2.com'])
def test_clear_site_cache_domain(self):
site = Site.objects.create(name='example2.com', domain='example2.com')
request = HttpRequest()
@ -191,6 +192,7 @@ class SitesFrameworkTests(TestCase):
self.assertEqual(Site.objects.get_by_natural_key(self.site.domain), self.site)
self.assertEqual(self.site.natural_key(), (self.site.domain,))
@override_settings(ALLOWED_HOSTS=['example.com'])
def test_requestsite_save_notimplemented_msg(self):
# Test response msg for RequestSite.save NotImplementedError
request = HttpRequest()
@ -201,6 +203,7 @@ class SitesFrameworkTests(TestCase):
with self.assertRaisesMessage(NotImplementedError, msg):
RequestSite(request).save()
@override_settings(ALLOWED_HOSTS=['example.com'])
def test_requestsite_delete_notimplemented_msg(self):
# Test response msg for RequestSite.delete NotImplementedError
request = HttpRequest()

View File

@ -35,9 +35,9 @@ class LiveServerBase(StaticLiveServerTestCase):
@classmethod
def tearDownClass(cls):
super(LiveServerBase, cls).tearDownClass()
# Restore original settings
cls.settings_override.disable()
super(LiveServerBase, cls).tearDownClass()
class StaticLiveServerChecks(LiveServerBase):

View File

@ -601,7 +601,13 @@ class ClientTest(TestCase):
a relevant ValueError rather than a non-descript AssertionError.
"""
response = self.client.get('/django_project_redirect/')
with self.assertRaisesMessage(ValueError, 'unable to fetch'):
msg = (
"The test client is unable to fetch remote URLs (got "
"https://www.djangoproject.com/). If the host is served by Django, "
"add 'www.djangoproject.com' to ALLOWED_HOSTS. "
"Otherwise, use assertRedirects(..., fetch_redirect_response=False)."
)
with self.assertRaisesMessage(ValueError, msg):
self.assertRedirects(response, 'https://www.djangoproject.com/')
def test_session_modifying_view(self):

View File

@ -15,7 +15,8 @@ from django.template import (
)
from django.template.response import SimpleTemplateResponse
from django.test import (
Client, SimpleTestCase, TestCase, ignore_warnings, override_settings,
Client, SimpleTestCase, TestCase, ignore_warnings, modify_settings,
override_settings,
)
from django.test.client import RedirectCycleError, RequestFactory, encode_file
from django.test.utils import ContextList, str_prefix
@ -455,6 +456,7 @@ class AssertRedirectsTests(SimpleTestCase):
self.assertRedirects(response, '/no_template_view/', 302, 200)
self.assertEqual(len(response.redirect_chain), 3)
@modify_settings(ALLOWED_HOSTS={'append': 'otherserver'})
def test_redirect_to_different_host(self):
"The test client will preserve scheme, host and port changes"
response = self.client.get('/redirect_other_host/', follow=True)
@ -467,6 +469,12 @@ class AssertRedirectsTests(SimpleTestCase):
self.assertEqual(response.request.get('wsgi.url_scheme'), 'https')
self.assertEqual(response.request.get('SERVER_NAME'), 'otherserver')
self.assertEqual(response.request.get('SERVER_PORT'), '8443')
# assertRedirects() can follow redirect to 'otherserver' too.
response = self.client.get('/redirect_other_host/', follow=False)
self.assertRedirects(
response, 'https://otherserver:8443/no_template_view/',
status_code=302, target_status_code=200
)
def test_redirect_chain_on_non_redirect_page(self):
"An assertion is raised if the original page couldn't be retrieved as expected"