feat: Update test configurations and add smoke tests for named view routes

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Ross
2026-04-30 22:35:11 +01:00
parent d12933e62a
commit 26a73d1d3b
5 changed files with 179 additions and 5 deletions
+10 -2
View File
@@ -14,13 +14,21 @@ services:
POSTGRES_USER: ${POSTGRES_USER:-django}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-rad}
ports:
- "5432:5432"
volumes:
- postgres_test_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL","pg_isready -U ${POSTGRES_USER:-django}" ]
interval: 10s
interval: 5s
timeout: 5s
retries: 5
retries: 10
networks:
- test_net
networks:
test_net:
name: rad_test_net
volumes:
postgres_test_data:
+1 -2
View File
@@ -4506,8 +4506,7 @@ class GenericViewBase:
@method_decorator(login_required)
def author_detail(self, request, pk):
# logging.debug(Author.objects.all())
# author = get_object_or_404(Author, pk=pk)
author = User.objects.get(pk=pk)
author = get_object_or_404(User, pk=pk)
questions = self.question_object.objects.filter(author=pk)
+1 -1
View File
@@ -1,4 +1,4 @@
[pytest]
DJANGO_SETTINGS_MODULE=rad.settings
DJANGO_SETTINGS_MODULE=rad.settings_test
python_files = tests.py test_*.py *_tests.py
addopts = --nomigrations
+26
View File
@@ -0,0 +1,26 @@
import os
from .settings import *
# Test environment should be deterministic and isolated from remote services.
DEBUG = False
# Local Postgres defaults align with docker-compose.test.yml.
DATABASES["default"] = {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("TEST_DB_NAME", os.environ.get("POSTGRES_DB", "rad")),
"USER": os.environ.get("TEST_DB_USER", os.environ.get("POSTGRES_USER", "django")),
"PASSWORD": os.environ.get("TEST_DB_PASSWORD", os.environ.get("POSTGRES_PASSWORD", "postgres")),
"HOST": os.environ.get("TEST_DB_HOST", "127.0.0.1"),
"PORT": os.environ.get("TEST_DB_PORT", "5432"),
"TEST": {
"NAME": os.environ.get("TEST_DB_TEST_NAME", "test_rad"),
},
}
# Keep tests fast and avoid external side effects.
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
CACHES["default"] = {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
}
+141
View File
@@ -0,0 +1,141 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable
from uuid import UUID
import pytest
from django.contrib.auth import get_user_model
from django.test import Client
from django.urls import URLPattern, URLResolver, get_resolver, reverse
SAFE_STATUS_CODES = {
200,
201,
202,
204,
301,
302,
303,
304,
307,
308,
400,
401,
403,
404,
405,
}
SKIP_NAMES = {
"admin:index", # admin app has its own dedicated tests and redirects heavily.
}
@dataclass(frozen=True)
class NamedRoute:
name: str
kwargs: dict[str, object]
def _default_for_converter(converter_name: str) -> object:
if converter_name == "int":
return 1
if converter_name == "str":
return "smoke"
if converter_name == "slug":
return "smoke"
if converter_name == "uuid":
return UUID("00000000-0000-0000-0000-000000000001")
if converter_name == "path":
return "smoke/path"
return "smoke"
def _iter_named_patterns(
patterns: Iterable[URLPattern | URLResolver],
namespace: str = "",
) -> Iterable[NamedRoute]:
for pattern in patterns:
if isinstance(pattern, URLResolver):
nested_namespace = namespace
if pattern.namespace:
nested_namespace = f"{namespace}:{pattern.namespace}" if namespace else pattern.namespace
yield from _iter_named_patterns(pattern.url_patterns, nested_namespace)
continue
if not pattern.name:
continue
full_name = f"{namespace}:{pattern.name}" if namespace else pattern.name
converters = getattr(pattern.pattern, "converters", {})
kwargs = {key: _default_for_converter(converter.__class__.__name__.replace("Converter", "").lower()) for key, converter in converters.items()}
yield NamedRoute(name=full_name, kwargs=kwargs)
def _all_named_routes() -> list[NamedRoute]:
resolver = get_resolver()
routes = list(_iter_named_patterns(resolver.url_patterns))
unique_routes: dict[tuple[str, tuple[tuple[str, object], ...]], NamedRoute] = {}
for route in routes:
key = (route.name, tuple(sorted(route.kwargs.items())))
unique_routes[key] = route
return sorted(unique_routes.values(), key=lambda route: (route.name, sorted(route.kwargs)))
ALL_NAMED_ROUTES = _all_named_routes()
@pytest.fixture
def smoke_client(db) -> Client:
user_model = get_user_model()
user = user_model.objects.create_user(
username="view_smoke_user",
password="testpassword",
email="view_smoke_user@example.com",
)
client = Client()
client.raise_request_exception = False
client.force_login(user)
return client
@pytest.mark.django_db
@pytest.mark.parametrize("route", ALL_NAMED_ROUTES, ids=lambda route: route.name)
def test_named_view_routes_do_not_return_server_errors(smoke_client: Client, route: NamedRoute):
if route.name in SKIP_NAMES:
pytest.skip("Route is intentionally excluded from this smoke suite.")
try:
url = reverse(route.name, kwargs=route.kwargs or None)
except Exception as exc: # pragma: no cover
pytest.skip(f"Unable to reverse route {route.name}: {exc}")
response = smoke_client.get(url, follow=False)
assert response.status_code in SAFE_STATUS_CODES, (
f"Unexpected status for {route.name} ({url}): {response.status_code}"
)
@pytest.mark.django_db
@pytest.mark.parametrize("route", ALL_NAMED_ROUTES, ids=lambda route: route.name)
def test_named_view_routes_anonymous_do_not_return_server_errors(route: NamedRoute):
if route.name in SKIP_NAMES:
pytest.skip("Route is intentionally excluded from this smoke suite.")
try:
url = reverse(route.name, kwargs=route.kwargs or None)
except Exception as exc: # pragma: no cover
pytest.skip(f"Unable to reverse route {route.name}: {exc}")
client = Client()
client.raise_request_exception = False
response = client.get(url, follow=False)
assert response.status_code in SAFE_STATUS_CODES, (
f"Unexpected anonymous status for {route.name} ({url}): {response.status_code}"
)