feat: Update test configurations and add smoke tests for named view routes
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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}"
|
||||
)
|
||||
Reference in New Issue
Block a user