Files
penracourses/rad/tests/test_view_smoke.py
T

215 lines
5.4 KiB
Python

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.
}
SKIP_NAMESPACES = {
"admin",
"tinymce",
"cookies",
}
STANDARD_NAME_HINTS = {
"index",
"home",
"help",
"about",
"privacy",
"stats",
"list",
"detail",
"view",
"overview",
"profile",
"inbox",
"messages",
"people",
"server",
"logs",
"trainees",
"normals",
"categories",
"search_page",
}
NON_STANDARD_NAME_HINTS = {
"create",
"update",
"delete",
"remove",
"add",
"edit",
"toggle",
"submit",
"save",
"bulk",
"json",
"dicom",
"viewer",
"download",
"upload",
"anonymise",
"migrate",
"truncate",
"image_size",
"series",
"mark",
"review",
"sync",
"reset",
"api",
}
@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)))
def _is_standard_route(route_name: str) -> bool:
parts = route_name.split(":")
namespace = parts[0] if len(parts) > 1 else ""
leaf_name = parts[-1]
if namespace in SKIP_NAMESPACES:
return False
if route_name in SKIP_NAMES:
return False
if any(hint in leaf_name for hint in NON_STANDARD_NAME_HINTS):
return False
return any(hint in leaf_name for hint in STANDARD_NAME_HINTS)
ALL_NAMED_ROUTES = _all_named_routes()
STANDARD_NAMED_ROUTES = [route for route in ALL_NAMED_ROUTES if _is_standard_route(route.name)]
@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", STANDARD_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", STANDARD_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}"
)