From ff996f798fc4f426f597487e39513434fa2b30d0 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 21 Feb 2023 21:52:30 +0100 Subject: [PATCH] start fixing tests Signed-off-by: Jens Langhammer --- authentik/core/api/users.py | 11 +++-- authentik/core/tests/test_impersonation.py | 2 +- authentik/flows/api/flows.py | 7 +++- authentik/flows/tests/test_views_helper.py | 12 +++++- authentik/interfaces/api.py | 1 + authentik/interfaces/tests.py | 12 ++++++ authentik/interfaces/views.py | 47 +++++++++++++--------- authentik/stages/email/stage.py | 9 +++-- authentik/stages/identification/tests.py | 14 ++++--- authentik/stages/password/stage.py | 14 ++++--- 10 files changed, 88 insertions(+), 41 deletions(-) create mode 100644 authentik/interfaces/tests.py diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 25346c469..61c3351d4 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -10,7 +10,6 @@ from django.db.models.functions import ExtractHour from django.db.models.query import QuerySet from django.db.transaction import atomic from django.db.utils import IntegrityError -from django.urls import reverse_lazy from django.utils.http import urlencode from django.utils.text import slugify from django.utils.timezone import now @@ -72,6 +71,8 @@ from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.models import FlowToken from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner from authentik.flows.views.executor import QS_KEY_TOKEN +from authentik.interfaces.models import InterfaceType +from authentik.interfaces.views import reverse_interface from authentik.stages.email.models import EmailStage from authentik.stages.email.tasks import send_mails from authentik.stages.email.utils import TemplateEmailMessage @@ -350,8 +351,12 @@ class UserViewSet(UsedByMixin, ModelViewSet): ) querystring = urlencode({QS_KEY_TOKEN: token.key}) link = self.request.build_absolute_uri( - reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) - + f"?{querystring}" + reverse_interface( + self.request, + InterfaceType.FLOW, + flow_slug=flow.slug, + ), + +f"?{querystring}", ) return link, token diff --git a/authentik/core/tests/test_impersonation.py b/authentik/core/tests/test_impersonation.py index 3c6125b89..ee54e9745 100644 --- a/authentik/core/tests/test_impersonation.py +++ b/authentik/core/tests/test_impersonation.py @@ -60,5 +60,5 @@ class TestImpersonation(TestCase): response = self.client.get(reverse("authentik_core:impersonate-end")) self.assertRedirects( - response, reverse("authentik_interfaces:if", kwargs={"if_name", "user"}) + response, reverse("authentik_interfaces:if", kwargs={"if_name": "user"}) ) diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py index c7b17baab..9949070ef 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -25,6 +25,8 @@ from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.models import Flow from authentik.flows.planner import CACHE_PREFIX, PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN +from authentik.interfaces.models import InterfaceType +from authentik.interfaces.views import reverse_interface from authentik.lib.utils.file import ( FilePathSerializer, FileUploadSerializer, @@ -294,7 +296,10 @@ class FlowViewSet(UsedByMixin, ModelViewSet): return Response( { "link": request._request.build_absolute_uri( - reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) + reverse_interface( + InterfaceType.FLOW, + flow_slug=flow.slug, + ), ) } ) diff --git a/authentik/flows/tests/test_views_helper.py b/authentik/flows/tests/test_views_helper.py index 1cf68fec2..bdc9e747c 100644 --- a/authentik/flows/tests/test_views_helper.py +++ b/authentik/flows/tests/test_views_helper.py @@ -7,6 +7,8 @@ from authentik.core.tests.utils import create_test_flow from authentik.flows.models import Flow, FlowDesignation from authentik.flows.planner import FlowPlan from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN +from authentik.interfaces.models import InterfaceType +from authentik.interfaces.tests import reverse_interface from authentik.lib.generators import generate_id from authentik.providers.oauth2.models import OAuth2Provider @@ -21,7 +23,10 @@ class TestHelperView(TestCase): response = self.client.get( reverse("authentik_flows:default-invalidation"), ) - expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) + expected_url = reverse_interface( + InterfaceType.FLOW, + flow_slug=flow.slug, + ) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, expected_url) @@ -72,6 +77,9 @@ class TestHelperView(TestCase): response = self.client.get( reverse("authentik_flows:default-invalidation"), ) - expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}) + expected_url = reverse_interface( + InterfaceType.FLOW, + flow_slug=flow.slug, + ) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, expected_url) diff --git a/authentik/interfaces/api.py b/authentik/interfaces/api.py index 88134d084..f033aca30 100644 --- a/authentik/interfaces/api.py +++ b/authentik/interfaces/api.py @@ -25,3 +25,4 @@ class InterfaceViewSet(UsedByMixin, ModelViewSet): queryset = Interface.objects.all() serializer_class = InterfaceSerializer filterset_fields = ["url_name", "type", "template"] + search_fields = ["url_name", "type", "template"] diff --git a/authentik/interfaces/tests.py b/authentik/interfaces/tests.py new file mode 100644 index 000000000..259855bba --- /dev/null +++ b/authentik/interfaces/tests.py @@ -0,0 +1,12 @@ +"""Interface tests""" +from django.test import RequestFactory + +from authentik.interfaces.models import InterfaceType +from authentik.interfaces.views import reverse_interface as full_reverse_interface + + +def reverse_interface(interface_type: InterfaceType, **kwargs): + """reverse_interface wrapper for tests""" + factory = RequestFactory() + request = factory.get("/") + return full_reverse_interface(request, interface_type, **kwargs) diff --git a/authentik/interfaces/views.py b/authentik/interfaces/views.py index afc048445..a0d096276 100644 --- a/authentik/interfaces/views.py +++ b/authentik/interfaces/views.py @@ -1,11 +1,13 @@ """Interface views""" from json import dumps from typing import Any, Optional +from urllib.parse import urlencode from django.http import Http404, HttpRequest, HttpResponse -from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.template import Template, TemplateSyntaxError, engines from django.template.response import TemplateResponse +from django.urls import reverse from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.cache import cache_page @@ -40,30 +42,37 @@ def redirect_to_default_interface(request: HttpRequest, interface_type: Interfac return RedirectToInterface.as_view(type=interface_type)(request, **kwargs) +def reverse_interface(request: HttpRequest, interface_type: InterfaceType, **kwargs): + """Reverse URL to configured default interface""" + tenant: Tenant = request.tenant + interface: Interface = None + + if interface_type == InterfaceType.USER: + interface = tenant.interface_user + if interface_type == InterfaceType.ADMIN: + interface = tenant.interface_admin + if interface_type == InterfaceType.FLOW: + interface = tenant.interface_flow + + if not interface: + raise Http404() + kwargs["if_name"] = interface.url_name + return reverse( + "authentik_interfaces:if", + kwargs=kwargs, + ) + + class RedirectToInterface(View): """Redirect to tenant's configured view for specified type""" type: Optional[InterfaceType] = None def dispatch(self, request: HttpRequest, **kwargs: Any) -> HttpResponse: - tenant: Tenant = request.tenant - interface: Interface = None - - if self.type == InterfaceType.USER: - interface = tenant.interface_user - if self.type == InterfaceType.ADMIN: - interface = tenant.interface_admin - if self.type == InterfaceType.FLOW: - interface = tenant.interface_flow - - if not interface: - raise Http404() - return redirect_with_qs( - "authentik_interfaces:if", - self.request.GET, - if_name=interface.url_name, - **kwargs, - ) + target = reverse_interface(request, self.type, **kwargs) + if self.request.GET: + target += "?" + urlencode(self.request.GET.items()) + return redirect(target) @method_decorator(ensure_csrf_cookie, name="dispatch") diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py index f116f7de0..752895729 100644 --- a/authentik/stages/email/stage.py +++ b/authentik/stages/email/stage.py @@ -16,6 +16,8 @@ from authentik.flows.models import FlowToken from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER from authentik.flows.stage import ChallengeStageView from authentik.flows.views.executor import QS_KEY_TOKEN +from authentik.interfaces.models import InterfaceType +from authentik.interfaces.views import reverse_interface from authentik.stages.email.models import EmailStage from authentik.stages.email.tasks import send_mails from authentik.stages.email.utils import TemplateEmailMessage @@ -47,9 +49,10 @@ class EmailStageView(ChallengeStageView): def get_full_url(self, **kwargs) -> str: """Get full URL to be used in template""" - base_url = reverse( - "authentik_core:if-flow", - kwargs={"flow_slug": self.executor.flow.slug}, + base_url = reverse_interface( + self.request, + InterfaceType.FLOW, + flow_slug=self.executor.flow.slug, ) relative_url = f"{base_url}?{urlencode(kwargs)}" return self.request.build_absolute_uri(relative_url) diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index 2b6481c88..3495e9065 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -5,6 +5,8 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.flows.challenge import ChallengeTypes from authentik.flows.models import FlowDesignation, FlowStageBinding from authentik.flows.tests import FlowTestCase +from authentik.interfaces.models import InterfaceType +from authentik.interfaces.tests import reverse_interface from authentik.sources.oauth.models import OAuthSource from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.password import BACKEND_INBUILT @@ -166,9 +168,9 @@ class TestIdentificationStage(FlowTestCase): component="ak-stage-identification", user_fields=["email"], password_fields=False, - enroll_url=reverse( - "authentik_core:if-flow", - kwargs={"flow_slug": flow.slug}, + enroll_url=reverse_interface( + InterfaceType.FLOW, + flow_slug=flow.slug, ), show_source_labels=False, primary_action="Log in", @@ -204,9 +206,9 @@ class TestIdentificationStage(FlowTestCase): component="ak-stage-identification", user_fields=["email"], password_fields=False, - recovery_url=reverse( - "authentik_core:if-flow", - kwargs={"flow_slug": flow.slug}, + recovery_url=reverse_interface( + InterfaceType.FLOW, + flow_slug=flow.slug, ), show_source_labels=False, primary_action="Log in", diff --git a/authentik/stages/password/stage.py b/authentik/stages/password/stage.py index 9b87a6691..a35c96c78 100644 --- a/authentik/stages/password/stage.py +++ b/authentik/stages/password/stage.py @@ -5,7 +5,6 @@ from django.contrib.auth import _clean_credentials from django.contrib.auth.backends import BaseBackend from django.core.exceptions import PermissionDenied from django.http import HttpRequest, HttpResponse -from django.urls import reverse from django.utils.translation import gettext as _ from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.fields import CharField @@ -23,6 +22,8 @@ from authentik.flows.challenge import ( from authentik.flows.models import Flow, FlowDesignation, Stage from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.stage import ChallengeStageView +from authentik.interfaces.models import InterfaceType +from authentik.interfaces.views import reverse_interface from authentik.lib.utils.reflection import path_to_class from authentik.stages.password.models import PasswordStage @@ -95,11 +96,12 @@ class PasswordStageView(ChallengeStageView): "type": ChallengeTypes.NATIVE.value, } ) - recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY) - if recovery_flow.exists(): - recover_url = reverse( - "authentik_core:if-flow", - kwargs={"flow_slug": recovery_flow.first().slug}, + recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY).first() + if recovery_flow: + recover_url = reverse_interface( + self.request, + InterfaceType.FLOW, + flow_slug=recovery_flow.slug, ) challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url) return challenge