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_USER: ${POSTGRES_USER:-django}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-rad}
|
POSTGRES_DB: ${POSTGRES_DB:-rad}
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_test_data:/var/lib/postgresql/data
|
- postgres_test_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL","pg_isready -U ${POSTGRES_USER:-django}" ]
|
test: ["CMD-SHELL","pg_isready -U ${POSTGRES_USER:-django}" ]
|
||||||
interval: 10s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 10
|
||||||
|
networks:
|
||||||
|
- test_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
test_net:
|
||||||
|
name: rad_test_net
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_test_data:
|
postgres_test_data:
|
||||||
|
|||||||
+1
-2
@@ -4506,8 +4506,7 @@ class GenericViewBase:
|
|||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def author_detail(self, request, pk):
|
def author_detail(self, request, pk):
|
||||||
# logging.debug(Author.objects.all())
|
# logging.debug(Author.objects.all())
|
||||||
# author = get_object_or_404(Author, pk=pk)
|
author = get_object_or_404(User, pk=pk)
|
||||||
author = User.objects.get(pk=pk)
|
|
||||||
|
|
||||||
questions = self.question_object.objects.filter(author=pk)
|
questions = self.question_object.objects.filter(author=pk)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
DJANGO_SETTINGS_MODULE=rad.settings
|
DJANGO_SETTINGS_MODULE=rad.settings_test
|
||||||
python_files = tests.py test_*.py *_tests.py
|
python_files = tests.py test_*.py *_tests.py
|
||||||
addopts = --nomigrations
|
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