learning-website-django1-1 Exercise 2: Test the Settings ========================================================== Write tests for the site map and the settings, so a mistake in either fails immediately. Then run Django's deployment check on the production settings. Save as tests/test_sites_config.py inside the project: import importlib import os from unittest import mock from django.conf import settings from django.test import SimpleTestCase from config import sites_config from config.sites_config import SITES, site_hosts class SiteMapTests(SimpleTestCase): def test_every_folder_belongs_to_exactly_one_site(self): seen = {} for site, info in SITES.items(): for folder in info["folders"]: self.assertNotIn(folder, seen, f"{folder} is in both {seen.get(folder)} and {site}") seen[folder] = site def test_there_are_eight_sites(self): self.assertEqual(len(SITES), 8) def test_site_names_are_valid_subdomain_labels(self): import re for name in SITES: self.assertRegex(name, r"^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$") class HostTests(SimpleTestCase): def test_prod_hosts_are_exact_names_with_no_wildcard(self): hosts = site_hosts("prod") self.assertEqual(len(hosts), len(SITES)) self.assertEqual(len(set(hosts)), len(hosts)) for host in hosts: self.assertTrue(host.endswith(".osztromok.com")) self.assertNotIn("*", host) self.assertFalse(host.startswith(".")) def test_dev_hosts_use_localhost(self): self.assertIn("languages.localhost", site_hosts("dev")) def test_pinning_gives_exactly_one_host(self): self.assertEqual(site_hosts("prod", "languages"), ["languages.osztromok.com"]) def test_unknown_pin_is_rejected(self): with self.assertRaises(ValueError): site_hosts("prod", "nonsense") class SettingsTests(SimpleTestCase): def test_dev_settings_allow_only_the_site_hosts(self): # Django's test runner adds "testserver" to ALLOWED_HOSTS, so leave it out of the comparison hosts = [h for h in settings.ALLOWED_HOSTS if h != "testserver"] self.assertEqual(hosts, site_hosts("dev")) def test_production_settings(self): env = {"DJANGO_SECRET_KEY": "x" * 60} with mock.patch.dict(os.environ, env): prod = importlib.import_module("config.settings.prod") prod = importlib.reload(prod) self.assertFalse(prod.DEBUG) self.assertEqual(prod.ALLOWED_HOSTS, site_hosts("prod")) self.assertTrue(all(o.startswith("https://") for o in prod.CSRF_TRUSTED_ORIGINS)) self.assertEqual(len(prod.CSRF_TRUSTED_ORIGINS), len(SITES)) self.assertIsNone(prod.SESSION_COOKIE_DOMAIN) # host-only cookies: no shared login self.assertTrue(prod.SESSION_COOKIE_SECURE and prod.CSRF_COOKIE_SECURE) def test_production_refuses_to_start_without_a_secret_key(self): with mock.patch.dict(os.environ, {}, clear=False): os.environ.pop("DJANGO_SECRET_KEY", None) with self.assertRaises(KeyError): importlib.reload(importlib.import_module("config.settings.prod")) Run the tests, and then the deployment check with the production settings: python manage.py test tests set DJANGO_SETTINGS_MODULE=config.settings.prod set DJANGO_SECRET_KEY= python manage.py check --deploy Output of the tests (checked by running them): Found 10 test(s). System check identified no issues (0 silenced). .......... ---------------------------------------------------------------------- Ran 10 tests in 0.004s OK Output of the deployment check (checked by running it): System check identified some issues: WARNINGS: ?: (security.W005) You have not set the SECURE_HSTS_INCLUDE_SUBDOMAINS setting to True. Without this, your site is potentially vulnerable to attack vi ?: (security.W021) You have not set the SECURE_HSTS_PRELOAD setting to True. Without this, your site cannot be submitted to the browser preload list. System check identified 2 issues (0 silenced). Reading the results ------------------- - The first version of the test that compared ALLOWED_HOSTS with the dev hosts FAILED: Django's test runner adds "testserver" to ALLOWED_HOSTS. The test now leaves that name out. Takeaway: when a test fails, read the difference before changing either side. - A first version of the production settings had four deployment warnings: no clickjacking middleware, no CSRF middleware, no HSTS and no SSL redirect. They were fixed by adding the two middleware classes, SECURE_HSTS_SECONDS and SECURE_SSL_REDIRECT. - Two warnings remain, ON PURPOSE: * W005, SECURE_HSTS_INCLUDE_SUBDOMAINS: it would tell browsers that EVERY subdomain must use HTTPS. Do not switch it on until every subdomain, including sites not yet migrated, really is on HTTPS. * W021, SECURE_HSTS_PRELOAD: submitting a domain to the browsers' preload list is very hard to undo. Start with a short HSTS time (3600 seconds, as in prod.py) and raise it in steps once everything works. - SESSION_COOKIE_DOMAIN stays None, so each site has its own login cookie. This is the safer default (Learning Website: Framework & Architecture 9). Sharing one login across subdomains is a deliberate choice, made later if at all. - ALLOWED_HOSTS lists exact host names. There is no "*" and no leading dot. Django's documentation allows both, but exact names mean a request for any other host name is refused. - CSRF_TRUSTED_ORIGINS needs the scheme (https://), which the test checks. WHY THIS WORKS AS AN ANSWER --------------------------- The settings are the part of a multi-site project where a small mistake is most expensive (a wildcard host, a missing scheme, a default secret key). Tests catch those in seconds, and the deployment check is a second opinion from Django itself.