From 5b9954ab981be693679a3354f320c4d51afda16c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Romain=20S=C3=A9bille?=
<20045330+rsebille@users.noreply.github.com>
Date: Thu, 30 Oct 2025 15:21:50 +0100
Subject: [PATCH 1/2] tests: Use `reverse_host()` over hardcoded value
---
docs/tests/test_views.py | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/docs/tests/test_views.py b/docs/tests/test_views.py
index a759793ba..5998df3b4 100644
--- a/docs/tests/test_views.py
+++ b/docs/tests/test_views.py
@@ -5,7 +5,7 @@
from django.test import SimpleTestCase, TestCase
from django.urls import reverse, set_urlconf
from django.utils.translation import activate, gettext as _
-from django_hosts.resolvers import reverse as reverse_with_host
+from django_hosts.resolvers import reverse as reverse_with_host, reverse_host
from djangoproject.urls import www as www_urls
from releases.models import Release
@@ -31,7 +31,7 @@ def test_team_url(self):
def test_internals_team(self):
response = self.client.get(
"/en/dev/internals/team/",
- headers={"host": "docs.djangoproject.localhost:8000"},
+ headers={"host": reverse_host("docs")},
)
self.assertRedirects(
response,
@@ -82,7 +82,7 @@ def tearDownClass(cls):
def test_empty_get(self):
response = self.client.get(
- "/en/dev/search/", headers={"host": "docs.djangoproject.localhost:8000"}
+ "/en/dev/search/", headers={"host": reverse_host("docs")}
)
self.assertEqual(response.status_code, 200)
# No header item is active.
@@ -93,7 +93,7 @@ def test_empty_get(self):
def test_search_type_filter_all(self):
response = self.client.get(
"/en/5.1/search/?q=generic",
- headers={"host": "docs.djangoproject.localhost:8000"},
+ headers={"host": reverse_host("docs")},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "5 results for generic", html=True)
@@ -105,7 +105,7 @@ def test_search_type_filter_by_doc_types(self):
with self.subTest(category=category):
response = self.client.get(
f"/en/5.1/search/?q=generic&category={category.value}",
- headers={"host": "docs.djangoproject.localhost:8000"},
+ headers={"host": reverse_host("docs")},
)
self.assertEqual(response.status_code, 200)
self.assertContains(
@@ -122,7 +122,7 @@ def test_search_type_filter_by_doc_types(self):
def test_search_category_filter_invalid_doc_categories(self):
response = self.client.get(
"/en/5.1/search/?q=generic&category=invalid-so-ignored",
- headers={"host": "docs.djangoproject.localhost:8000"},
+ headers={"host": reverse_host("docs")},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "5 results for generic", html=True)
@@ -132,7 +132,7 @@ def test_search_category_filter_invalid_doc_categories(self):
def test_search_category_filter_no_results(self):
response = self.client.get(
"/en/5.1/search/?q=potato&category=ref",
- headers={"host": "docs.djangoproject.localhost:8000"},
+ headers={"host": reverse_host("docs")},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.active_filter, count=1)
@@ -267,7 +267,7 @@ def test_code_links(self):
with self.subTest(query=query):
response = self.client.get(
f"/en/5.1/search/?q={query}",
- headers={"host": "docs.djangoproject.localhost:8000"},
+ headers={"host": reverse_host("docs")},
)
self.assertEqual(response.status_code, 200)
self.assertContains(
@@ -289,7 +289,7 @@ def tearDownClass(cls):
def test_sitemap_index(self):
response = self.client.get(
- "/sitemap.xml", headers={"host": "docs.djangoproject.localhost:8000"}
+ "/sitemap.xml", headers={"host": reverse_host("docs")}
)
self.assertContains(response, "", count=2)
en_sitemap_url = reverse_with_host(
@@ -318,7 +318,7 @@ def test_sitemap(self):
def test_sitemap_404(self):
response = self.client.get(
- "/sitemap-xx.xml", headers={"host": "docs.djangoproject.localhost:8000"}
+ "/sitemap-xx.xml", headers={"host": reverse_host("docs")}
)
self.assertEqual(response.status_code, 404)
self.assertEqual(
From 139d4144ec12c21ab1547fca6ff7414bcfb9e860 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Romain=20S=C3=A9bille?=
<20045330+rsebille@users.noreply.github.com>
Date: Thu, 30 Oct 2025 15:02:50 +0100
Subject: [PATCH 2/2] docs.tests.views: Add more tests
---
docs/tests/test_views.py | 270 ++++++++++++++++++++++++++++++++++++++-
docs/views.py | 3 +
2 files changed, 272 insertions(+), 1 deletion(-)
diff --git a/docs/tests/test_views.py b/docs/tests/test_views.py
index 5998df3b4..a2a101c59 100644
--- a/docs/tests/test_views.py
+++ b/docs/tests/test_views.py
@@ -1,13 +1,15 @@
+import unittest
from http import HTTPStatus
from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.test import SimpleTestCase, TestCase
from django.urls import reverse, set_urlconf
from django.utils.translation import activate, gettext as _
from django_hosts.resolvers import reverse as reverse_with_host, reverse_host
-from djangoproject.urls import www as www_urls
+from djangoproject.urls import docs as docs_urls, www as www_urls
from releases.models import Release
from ..models import Document, DocumentRelease
@@ -40,6 +42,146 @@ def test_internals_team(self):
fetch_redirect_response=False,
)
+ def test_redirect_index_view(self):
+ response = self.client.get(
+ "/en/dev/index/", # Route without name
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertRedirects(response, "/en/dev/", fetch_redirect_response=False)
+
+
+class LangAndReleaseRedirectTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.release = Release.objects.create(version="5.2")
+ cls.doc_release = DocumentRelease.objects.create(
+ release=cls.release, is_default=True
+ )
+
+ def test_index_view_redirect_to_current_document_release(self):
+ response = self.client.get(
+ reverse_with_host("homepage", host="docs"),
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertRedirects(
+ response, self.doc_release.get_absolute_url(), fetch_redirect_response=False
+ )
+
+ def test_language_view_redirect_to_current_document_release_with_the_same_language(
+ self,
+ ):
+ fr_doc_release = DocumentRelease.objects.create(release=self.release, lang="fr")
+ response = self.client.get(
+ "/fr/", # Route without name
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertRedirects(
+ response, fr_doc_release.get_absolute_url(), fetch_redirect_response=False
+ )
+
+ def test_stable_view_redirect_to_current_document_release(self):
+ response = self.client.get(
+ reverse_with_host(
+ # The stable view doesn't have a name but it's basically
+ # the document-detail route with a version set to "stable"
+ "document-detail",
+ kwargs={
+ "version": "stable",
+ "lang": self.doc_release.lang,
+ "url": "intro",
+ },
+ host="docs",
+ ),
+ headers={"host": reverse_host("docs")},
+ )
+ # Using Django's `reverse()` over django-hosts's `reverse_host()` as the later
+ # one return an absolute URL but the view redirect only using the path component
+ expected_url = reverse(
+ # The stable view route doesn't have a name but it's basically
+ # the `document-detail` route with a version set to "stable"
+ "document-detail",
+ kwargs={
+ "version": self.doc_release.version,
+ "lang": self.doc_release.lang,
+ "url": "intro",
+ },
+ urlconf=docs_urls,
+ )
+ self.assertRedirects(response, expected_url, fetch_redirect_response=False)
+
+
+class DocumentViewTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.doc_release = DocumentRelease.objects.create(is_default=True)
+
+ def test_document_index_view(self):
+ # Set up a release so we aren't in `dev` version
+ self.doc_release.release = Release.objects.create(version="5.2")
+ self.doc_release.save(update_fields=["release"])
+
+ response = self.client.get(
+ reverse_with_host(
+ "document-index",
+ kwargs={
+ "lang": self.doc_release.lang,
+ "version": self.doc_release.version,
+ },
+ host="docs",
+ ),
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context.get("docurl"), "")
+ self.assertEqual(
+ response.context.get("rtd_version"), f"{self.doc_release.version}.x"
+ )
+ # Check the header used for Fastly
+ self.assertEqual(response.headers.get("Surrogate-Control"), "max-age=604800")
+
+ def test_document_index_view_with_dev_version(self):
+ response = self.client.get(
+ reverse_with_host(
+ "document-index",
+ kwargs={"lang": self.doc_release.lang, "version": "dev"},
+ host="docs",
+ ),
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context.get("rtd_version"), "latest")
+
+ @unittest.expectedFailure
+ def test_document_index_view_with_stable_version(self):
+ response = self.client.get(
+ reverse_with_host(
+ "document-index",
+ kwargs={"lang": self.doc_release.lang, "version": "stable"},
+ host="docs",
+ ),
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context.get("rtd_version"), "latest")
+
+ def test_document_detail_view(self):
+ response = self.client.get(
+ reverse_with_host(
+ "document-detail",
+ kwargs={
+ "lang": self.doc_release.lang,
+ "version": "dev",
+ "url": "intro",
+ },
+ host="docs",
+ ),
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context.get("docurl"), "intro")
+ # Check the header used for Fastly
+ self.assertEqual(response.headers.get("Surrogate-Control"), "max-age=604800")
+
class SearchFormTestCase(TestCase):
fixtures = ["doc_test_fixtures"]
@@ -278,6 +420,31 @@ def test_code_links(self):
self.assertContains(response, expected_code_links, html=True)
+class SearchRedirectTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.doc_release = DocumentRelease.objects.create(is_default=True)
+
+ def test_redirect_search_view(self):
+ # With a `q` parameters
+ response = self.client.get(
+ "/search/?q=django", headers={"host": reverse_host("docs")}
+ )
+ self.assertRedirects(
+ response,
+ "http://" + reverse_host("docs") + "/en/dev/search/?q=django",
+ fetch_redirect_response=False,
+ )
+
+ # Without a `q` parameters
+ response = self.client.get("/search/", headers={"host": reverse_host("docs")})
+ self.assertRedirects(
+ response,
+ "http://" + reverse_host("docs") + "/en/dev/search/",
+ fetch_redirect_response=False,
+ )
+
+
class SitemapTests(TestCase):
fixtures = ["doc_test_fixtures"]
@@ -324,3 +491,104 @@ def test_sitemap_404(self):
self.assertEqual(
response.context["exception"], "No sitemap available for section: 'xx'"
)
+
+
+class OpenSearchTests(TestCase):
+ @classmethod
+ def setUpTestData(cls):
+ cls.doc_release = DocumentRelease.objects.create(
+ release=Release.objects.create(version="5.2"), is_default=True
+ )
+
+ def test_search_suggestions_view(self):
+ # Without `q` parameter
+ response = self.client.get(
+ reverse_with_host(
+ "document-search-suggestions",
+ kwargs={
+ "lang": self.doc_release.lang,
+ "version": self.doc_release.version,
+ },
+ host="docs",
+ ),
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.headers["Content-Type"], "application/json")
+ self.assertEqual(response.json(), [])
+
+ # With `q` parameter but no Document
+ response = self.client.get(
+ reverse_with_host(
+ "document-search-suggestions",
+ kwargs={
+ "lang": self.doc_release.lang,
+ "version": self.doc_release.version,
+ },
+ host="docs",
+ )
+ + "?q=test",
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.headers["Content-Type"], "application/json")
+ self.assertEqual(response.json(), ["test", [], [], []])
+
+ # # With `q` parameter and a Document
+ document = Document.objects.create(
+ release=self.doc_release,
+ path="test-document",
+ title="test title",
+ )
+ response = self.client.get(
+ reverse_with_host(
+ "document-search-suggestions",
+ kwargs={
+ "lang": self.doc_release.lang,
+ "version": self.doc_release.version,
+ },
+ host="docs",
+ )
+ + "?q=test",
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.headers["Content-Type"], "application/json")
+ self.assertEqual(
+ response.json(),
+ [
+ "test",
+ ["test title"],
+ [],
+ [
+ reverse_with_host(
+ "contenttypes-shortcut",
+ kwargs={
+ "content_type_id": ContentType.objects.get_for_model(
+ Document
+ ).pk,
+ "object_id": document.id,
+ },
+ )
+ ],
+ ],
+ )
+
+ def test_search_description(self):
+ response = self.client.get(
+ reverse_with_host(
+ "document-search-description",
+ kwargs={
+ "lang": self.doc_release.lang,
+ "version": self.doc_release.version,
+ },
+ host="docs",
+ ),
+ headers={"host": reverse_host("docs")},
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(
+ response.headers["Content-Type"], "application/opensearchdescription+xml"
+ )
+ self.assertTemplateUsed("docs/search_description.html")
+ self.assertContains(response, f"{self.doc_release.lang}")
diff --git a/docs/views.py b/docs/views.py
index 03c53cf42..2c02b5bef 100644
--- a/docs/views.py
+++ b/docs/views.py
@@ -50,6 +50,9 @@ def document(request, lang, version, url):
canonical_version = DocumentRelease.objects.current_version()
canonical = version == canonical_version
+ # FIXME: I think it's dead code.
+ # The stable view route is higher than document-* ones,
+ # and test_document_index_view_with_stable_version failed with a 302
if version == "stable":
version = canonical_version