mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-14 06:55:08 +02:00
Add test that login is required for almost every URL
This commit is contained in:
parent
2e7644bdc3
commit
85a8b108de
1 changed files with 158 additions and 0 deletions
158
src/registrar/tests/test_url_auth.py
Normal file
158
src/registrar/tests/test_url_auth.py
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
"""Test that almost all URLs require authentication.
|
||||||
|
|
||||||
|
This uses deep Django URLConf pattern magic and was shamelessly lifted from
|
||||||
|
https://github.com/18F/tock/blob/main/tock/tock/tests/test_url_auth.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse, URLPattern
|
||||||
|
from django.urls.resolvers import URLResolver
|
||||||
|
|
||||||
|
import registrar.config.urls
|
||||||
|
|
||||||
|
from .common import less_console_noise
|
||||||
|
|
||||||
|
# When a URLconf pattern contains named capture groups, we'll use this
|
||||||
|
# dictionary to retrieve a sample value for it, which will be included
|
||||||
|
# in the sample URLs we generate, when attempting to perform a GET
|
||||||
|
# request on the view.
|
||||||
|
SAMPLE_KWARGS = {
|
||||||
|
"app_label": "registrar",
|
||||||
|
"pk": "1",
|
||||||
|
"id": "1",
|
||||||
|
"content_type_id": "2",
|
||||||
|
"object_id": "3",
|
||||||
|
"domain": "whitehouse.gov",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Our test suite will ignore some namespaces.
|
||||||
|
IGNORE_NAMESPACES = [
|
||||||
|
# The Django Debug Toolbar (DJDT) ends up in the URL config but it's always
|
||||||
|
# disabled in production, so don't worry about it.
|
||||||
|
"djdt"
|
||||||
|
]
|
||||||
|
|
||||||
|
# In general, we don't want to have any unnamed views, because that makes it
|
||||||
|
# impossible to generate sample URLs that point at them. We'll make exceptions
|
||||||
|
# for some namespaces that we don't have control over, though.
|
||||||
|
NAMESPACES_WITH_UNNAMED_VIEWS = ["admin", None]
|
||||||
|
|
||||||
|
|
||||||
|
def iter_patterns(urlconf, patterns=None, namespace=None):
|
||||||
|
"""
|
||||||
|
Iterate through all patterns in the given Django URLconf. Yields
|
||||||
|
`(viewname, route)` tuples, where `viewname` is the fully-qualified view name
|
||||||
|
(including its namespace, if any), and `route` is a regular expression that
|
||||||
|
corresponds to the part of the pattern that contains any capturing groups.
|
||||||
|
"""
|
||||||
|
if patterns is None:
|
||||||
|
patterns = urlconf.urlpatterns
|
||||||
|
for pattern in patterns:
|
||||||
|
# Resolve if it's a route or an include
|
||||||
|
if isinstance(pattern, URLPattern):
|
||||||
|
viewname = pattern.name
|
||||||
|
if viewname is None and namespace not in NAMESPACES_WITH_UNNAMED_VIEWS:
|
||||||
|
raise AssertionError(
|
||||||
|
f"namespace {namespace} cannot contain unnamed views"
|
||||||
|
)
|
||||||
|
if namespace and viewname is not None:
|
||||||
|
viewname = f"{namespace}:{viewname}"
|
||||||
|
yield (viewname, pattern.pattern)
|
||||||
|
elif isinstance(pattern, URLResolver):
|
||||||
|
if len(pattern.default_kwargs.keys()) > 0:
|
||||||
|
raise AssertionError("resolvers are not expected to have kwargs")
|
||||||
|
if pattern.namespace and namespace is not None:
|
||||||
|
raise AssertionError("nested namespaces are not currently supported")
|
||||||
|
if pattern.namespace in IGNORE_NAMESPACES:
|
||||||
|
continue
|
||||||
|
yield from iter_patterns(
|
||||||
|
urlconf, pattern.url_patterns, namespace or pattern.namespace
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise AssertionError("unknown pattern class")
|
||||||
|
|
||||||
|
|
||||||
|
def iter_sample_urls(urlconf):
|
||||||
|
"""
|
||||||
|
Yields sample URLs for all entries in the given Django URLconf.
|
||||||
|
This gets pretty deep into the muck of RoutePattern
|
||||||
|
https://docs.djangoproject.com/en/2.1/_modules/django/urls/resolvers/
|
||||||
|
"""
|
||||||
|
|
||||||
|
for viewname, route in iter_patterns(urlconf):
|
||||||
|
if not viewname:
|
||||||
|
continue
|
||||||
|
if viewname == "auth_user_password_change":
|
||||||
|
print(route)
|
||||||
|
break
|
||||||
|
named_groups = route.regex.groupindex.keys()
|
||||||
|
kwargs = {}
|
||||||
|
args = ()
|
||||||
|
|
||||||
|
for kwarg in named_groups:
|
||||||
|
if kwarg not in SAMPLE_KWARGS:
|
||||||
|
raise AssertionError(
|
||||||
|
f'Sample value for {kwarg} in pattern "{route}" not found'
|
||||||
|
)
|
||||||
|
kwargs[kwarg] = SAMPLE_KWARGS[kwarg]
|
||||||
|
|
||||||
|
url = reverse(viewname, args=args, kwargs=kwargs)
|
||||||
|
yield (viewname, url)
|
||||||
|
|
||||||
|
|
||||||
|
class TestURLAuth(TestCase):
|
||||||
|
"""
|
||||||
|
Tests to ensure that most URLs in a Django URLconf are protected by
|
||||||
|
authentication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We won't test that the following URLs are protected by auth.
|
||||||
|
# Note that the trailing slash is wobbly depending on how the URL was defined.
|
||||||
|
IGNORE_URLS = [
|
||||||
|
# These are the OIDC auth endpoints that always need
|
||||||
|
# to be public.
|
||||||
|
"/openid/login/",
|
||||||
|
"/openid/logout/",
|
||||||
|
"/openid/callback",
|
||||||
|
"/openid/callback/logout/",
|
||||||
|
]
|
||||||
|
|
||||||
|
def assertURLIsProtectedByAuth(self, url):
|
||||||
|
"""
|
||||||
|
Make a GET request to the given URL, and ensure that it either redirects
|
||||||
|
to login or denies access outright.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
with less_console_noise():
|
||||||
|
response = self.client.get(url)
|
||||||
|
except Exception as e:
|
||||||
|
# It'll be helpful to provide information on what URL was being
|
||||||
|
# accessed at the time the exception occurred. Python 3 will
|
||||||
|
# also include a full traceback of the original exception, so
|
||||||
|
# we don't need to worry about hiding the original cause.
|
||||||
|
raise AssertionError(f'Accessing {url} raised "{e}"', e)
|
||||||
|
|
||||||
|
code = response.status_code
|
||||||
|
if code == 302:
|
||||||
|
redirect = response["location"]
|
||||||
|
self.assertRegex(
|
||||||
|
redirect,
|
||||||
|
r"^\/openid\/login",
|
||||||
|
f"GET {url} should redirect to login or deny access, but instead "
|
||||||
|
f"it redirects to {redirect}",
|
||||||
|
)
|
||||||
|
elif code == 401 or code == 403:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError(
|
||||||
|
f"GET {url} returned HTTP {code}, but should redirect to login or "
|
||||||
|
"deny access",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_login_required_all_urls(self):
|
||||||
|
"""All URLs redirect to the login view."""
|
||||||
|
for viewname, url in iter_sample_urls(registrar.config.urls):
|
||||||
|
if url not in self.IGNORE_URLS:
|
||||||
|
with self.subTest(viewname=viewname):
|
||||||
|
self.assertURLIsProtectedByAuth(url)
|
Loading…
Add table
Add a link
Reference in a new issue