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:
parent
00551c3eff
commit
17e661641d
|
@ -29,6 +29,7 @@ from django.core.servers.basehttp import WSGIRequestHandler, WSGIServer
|
||||||
from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
|
from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
|
||||||
from django.forms.fields import CharField
|
from django.forms.fields import CharField
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
|
from django.http.request import split_domain_port, validate_host
|
||||||
from django.test.client import Client
|
from django.test.client import Client
|
||||||
from django.test.html import HTMLParseError, parse_html
|
from django.test.html import HTMLParseError, parse_html
|
||||||
from django.test.signals import setting_changed, template_rendered
|
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
|
# netloc might be empty, or in cases where Django tests the
|
||||||
# HTTP scheme, the convention is for netloc to be 'testserver'.
|
# HTTP scheme, the convention is for netloc to be 'testserver'.
|
||||||
# Trust both as "internal" URLs here.
|
# 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(
|
raise ValueError(
|
||||||
"The Django test client is unable to fetch remote URLs (got %s). "
|
"The test client is unable to fetch remote URLs (got %s). "
|
||||||
"Use assertRedirects(..., fetch_redirect_response=False) instead." % url
|
"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'))
|
redirect_response = response.client.get(path, QueryDict(query), secure=(scheme == 'https'))
|
||||||
|
|
||||||
|
@ -1324,9 +1328,12 @@ class LiveServerTestCase(TransactionTestCase):
|
||||||
conn.allow_thread_sharing = True
|
conn.allow_thread_sharing = True
|
||||||
connections_override[conn.alias] = conn
|
connections_override[conn.alias] = conn
|
||||||
|
|
||||||
# Launch the live server's thread
|
|
||||||
specified_address = os.environ.get(
|
specified_address = os.environ.get(
|
||||||
'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081-8179')
|
'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'
|
# 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
|
# i.e. a comma-separated list of ports or ranges of ports, so we break
|
||||||
|
@ -1348,6 +1355,7 @@ class LiveServerTestCase(TransactionTestCase):
|
||||||
except Exception:
|
except Exception:
|
||||||
msg = 'Invalid address ("%s") for live server.' % specified_address
|
msg = 'Invalid address ("%s") for live server.' % specified_address
|
||||||
six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), sys.exc_info()[2])
|
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 = cls._create_server_thread(host, possible_ports, connections_override)
|
||||||
cls.server_thread.daemon = True
|
cls.server_thread.daemon = True
|
||||||
cls.server_thread.start()
|
cls.server_thread.start()
|
||||||
|
@ -1386,6 +1394,7 @@ class LiveServerTestCase(TransactionTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
cls._tearDownClassInternal()
|
cls._tearDownClassInternal()
|
||||||
|
cls._live_server_modified_settings.disable()
|
||||||
super(LiveServerTestCase, cls).tearDownClass()
|
super(LiveServerTestCase, cls).tearDownClass()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,8 @@ def setup_test_environment():
|
||||||
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
|
|
||||||
request._original_allowed_hosts = settings.ALLOWED_HOSTS
|
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 = []
|
mail.outbox = []
|
||||||
|
|
||||||
|
|
|
@ -90,14 +90,18 @@ If the ``Host`` header (or ``X-Forwarded-Host`` if
|
||||||
list, the :meth:`django.http.HttpRequest.get_host()` method will raise
|
list, the :meth:`django.http.HttpRequest.get_host()` method will raise
|
||||||
:exc:`~django.core.exceptions.SuspiciousOperation`.
|
:exc:`~django.core.exceptions.SuspiciousOperation`.
|
||||||
|
|
||||||
When :setting:`DEBUG` is ``True`` or when running tests, host validation is
|
When :setting:`DEBUG` is ``True``, host validation is disabled; any host will
|
||||||
disabled; any host will be accepted. Thus it's usually only necessary to set it
|
be accepted. ``ALLOWED_HOSTS`` is :ref:`checked when running tests
|
||||||
in production.
|
<topics-testing-advanced-multiple-hosts>`.
|
||||||
|
|
||||||
This validation only applies via :meth:`~django.http.HttpRequest.get_host()`;
|
This validation only applies via :meth:`~django.http.HttpRequest.get_host()`;
|
||||||
if your code accesses the ``Host`` header directly from ``request.META`` you
|
if your code accesses the ``Host`` header directly from ``request.META`` you
|
||||||
are bypassing this security protection.
|
are bypassing this security protection.
|
||||||
|
|
||||||
|
.. versionchanged:: 1.11
|
||||||
|
|
||||||
|
In older versions, ``ALLOWED_HOSTS`` wasn't checked when running tests.
|
||||||
|
|
||||||
.. setting:: APPEND_SLASH
|
.. setting:: APPEND_SLASH
|
||||||
|
|
||||||
``APPEND_SLASH``
|
``APPEND_SLASH``
|
||||||
|
|
|
@ -262,6 +262,11 @@ Miscellaneous
|
||||||
* CSRF failures are logged to the ``django.security.csrf ``` logger instead of
|
* CSRF failures are logged to the ``django.security.csrf ``` logger instead of
|
||||||
``django.request``.
|
``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``
|
* 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
|
displays the related object's ID. Remove the ``_id`` suffix if you want the
|
||||||
old behavior of the string representation of the object.
|
old behavior of the string representation of the object.
|
||||||
|
|
|
@ -498,6 +498,7 @@ multiline
|
||||||
multilinestring
|
multilinestring
|
||||||
multipart
|
multipart
|
||||||
multipolygon
|
multipolygon
|
||||||
|
multitenancy
|
||||||
multithreaded
|
multithreaded
|
||||||
multithreading
|
multithreading
|
||||||
Mumbai
|
Mumbai
|
||||||
|
|
|
@ -67,6 +67,61 @@ The following is a simple unit test using the request factory::
|
||||||
response = MyView.as_view()(request)
|
response = MyView.as_view()(request)
|
||||||
self.assertEqual(response.status_code, 200)
|
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:
|
.. _topics-testing-advanced-multidb:
|
||||||
|
|
||||||
Tests and multiple databases
|
Tests and multiple databases
|
||||||
|
|
|
@ -1393,6 +1393,7 @@ class DefaultNonExpiringCacheKeyTests(SimpleTestCase):
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
USE_I18N=False,
|
USE_I18N=False,
|
||||||
|
ALLOWED_HOSTS=['.example.com'],
|
||||||
)
|
)
|
||||||
class CacheUtils(SimpleTestCase):
|
class CacheUtils(SimpleTestCase):
|
||||||
"""TestCase for django.utils.cache functions."""
|
"""TestCase for django.utils.cache functions."""
|
||||||
|
|
|
@ -142,6 +142,7 @@ class SitesFrameworkTests(TestCase):
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
site.full_clean()
|
site.full_clean()
|
||||||
|
|
||||||
|
@override_settings(ALLOWED_HOSTS=['example.com'])
|
||||||
def test_clear_site_cache(self):
|
def test_clear_site_cache(self):
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
request.META = {
|
request.META = {
|
||||||
|
@ -162,7 +163,7 @@ class SitesFrameworkTests(TestCase):
|
||||||
clear_site_cache(Site, instance=self.site, using='default')
|
clear_site_cache(Site, instance=self.site, using='default')
|
||||||
self.assertEqual(models.SITE_CACHE, {})
|
self.assertEqual(models.SITE_CACHE, {})
|
||||||
|
|
||||||
@override_settings(SITE_ID='')
|
@override_settings(SITE_ID='', ALLOWED_HOSTS=['example2.com'])
|
||||||
def test_clear_site_cache_domain(self):
|
def test_clear_site_cache_domain(self):
|
||||||
site = Site.objects.create(name='example2.com', domain='example2.com')
|
site = Site.objects.create(name='example2.com', domain='example2.com')
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
|
@ -191,6 +192,7 @@ class SitesFrameworkTests(TestCase):
|
||||||
self.assertEqual(Site.objects.get_by_natural_key(self.site.domain), self.site)
|
self.assertEqual(Site.objects.get_by_natural_key(self.site.domain), self.site)
|
||||||
self.assertEqual(self.site.natural_key(), (self.site.domain,))
|
self.assertEqual(self.site.natural_key(), (self.site.domain,))
|
||||||
|
|
||||||
|
@override_settings(ALLOWED_HOSTS=['example.com'])
|
||||||
def test_requestsite_save_notimplemented_msg(self):
|
def test_requestsite_save_notimplemented_msg(self):
|
||||||
# Test response msg for RequestSite.save NotImplementedError
|
# Test response msg for RequestSite.save NotImplementedError
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
|
@ -201,6 +203,7 @@ class SitesFrameworkTests(TestCase):
|
||||||
with self.assertRaisesMessage(NotImplementedError, msg):
|
with self.assertRaisesMessage(NotImplementedError, msg):
|
||||||
RequestSite(request).save()
|
RequestSite(request).save()
|
||||||
|
|
||||||
|
@override_settings(ALLOWED_HOSTS=['example.com'])
|
||||||
def test_requestsite_delete_notimplemented_msg(self):
|
def test_requestsite_delete_notimplemented_msg(self):
|
||||||
# Test response msg for RequestSite.delete NotImplementedError
|
# Test response msg for RequestSite.delete NotImplementedError
|
||||||
request = HttpRequest()
|
request = HttpRequest()
|
||||||
|
|
|
@ -35,9 +35,9 @@ class LiveServerBase(StaticLiveServerTestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
|
super(LiveServerBase, cls).tearDownClass()
|
||||||
# Restore original settings
|
# Restore original settings
|
||||||
cls.settings_override.disable()
|
cls.settings_override.disable()
|
||||||
super(LiveServerBase, cls).tearDownClass()
|
|
||||||
|
|
||||||
|
|
||||||
class StaticLiveServerChecks(LiveServerBase):
|
class StaticLiveServerChecks(LiveServerBase):
|
||||||
|
|
|
@ -601,7 +601,13 @@ class ClientTest(TestCase):
|
||||||
a relevant ValueError rather than a non-descript AssertionError.
|
a relevant ValueError rather than a non-descript AssertionError.
|
||||||
"""
|
"""
|
||||||
response = self.client.get('/django_project_redirect/')
|
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/')
|
self.assertRedirects(response, 'https://www.djangoproject.com/')
|
||||||
|
|
||||||
def test_session_modifying_view(self):
|
def test_session_modifying_view(self):
|
||||||
|
|
|
@ -15,7 +15,8 @@ from django.template import (
|
||||||
)
|
)
|
||||||
from django.template.response import SimpleTemplateResponse
|
from django.template.response import SimpleTemplateResponse
|
||||||
from django.test import (
|
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.client import RedirectCycleError, RequestFactory, encode_file
|
||||||
from django.test.utils import ContextList, str_prefix
|
from django.test.utils import ContextList, str_prefix
|
||||||
|
@ -455,6 +456,7 @@ class AssertRedirectsTests(SimpleTestCase):
|
||||||
self.assertRedirects(response, '/no_template_view/', 302, 200)
|
self.assertRedirects(response, '/no_template_view/', 302, 200)
|
||||||
self.assertEqual(len(response.redirect_chain), 3)
|
self.assertEqual(len(response.redirect_chain), 3)
|
||||||
|
|
||||||
|
@modify_settings(ALLOWED_HOSTS={'append': 'otherserver'})
|
||||||
def test_redirect_to_different_host(self):
|
def test_redirect_to_different_host(self):
|
||||||
"The test client will preserve scheme, host and port changes"
|
"The test client will preserve scheme, host and port changes"
|
||||||
response = self.client.get('/redirect_other_host/', follow=True)
|
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('wsgi.url_scheme'), 'https')
|
||||||
self.assertEqual(response.request.get('SERVER_NAME'), 'otherserver')
|
self.assertEqual(response.request.get('SERVER_NAME'), 'otherserver')
|
||||||
self.assertEqual(response.request.get('SERVER_PORT'), '8443')
|
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):
|
def test_redirect_chain_on_non_redirect_page(self):
|
||||||
"An assertion is raised if the original page couldn't be retrieved as expected"
|
"An assertion is raised if the original page couldn't be retrieved as expected"
|
||||||
|
|
Loading…
Reference in New Issue