From 90c31c22148e15c2d8f1e4d0f9dc143921ee8c08 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 1 Jan 2022 19:56:35 +0100 Subject: [PATCH] flows: add test helpers to simplify and improve checking of stages, remove force_str Signed-off-by: Jens Langhammer --- authentik/core/tests/test_applications_api.py | 9 +- .../tests/test_authenticated_sessions_api.py | 3 +- authentik/flows/tests/__init__.py | 51 ++++ authentik/flows/tests/test_executor.py | 115 ++------- .../policies/password/tests/test_flows.py | 47 ++-- .../providers/oauth2/tests/test_authorize.py | 5 +- authentik/providers/oauth2/tests/test_jwks.py | 5 +- .../providers/oauth2/tests/test_token.py | 7 +- .../providers/oauth2/tests/test_userinfo.py | 5 +- .../stages/authenticator_validate/tests.py | 31 +-- authentik/stages/captcha/tests.py | 15 +- authentik/stages/consent/tests.py | 34 +-- authentik/stages/deny/tests.py | 34 +-- authentik/stages/dummy/tests.py | 15 +- authentik/stages/email/tests/test_stage.py | 15 +- authentik/stages/identification/tests.py | 231 +++++++----------- authentik/stages/invitation/tests.py | 48 +--- authentik/stages/password/tests.py | 51 ++-- authentik/stages/prompt/tests.py | 21 +- authentik/stages/user_delete/tests.py | 38 +-- authentik/stages/user_login/tests.py | 58 +---- authentik/stages/user_logout/tests.py | 26 +- authentik/stages/user_write/tests.py | 72 ++---- authentik/tenants/tests.py | 13 +- schema.yml | 6 + 25 files changed, 302 insertions(+), 653 deletions(-) diff --git a/authentik/core/tests/test_applications_api.py b/authentik/core/tests/test_applications_api.py index ecdace6d8..bec6632ff 100644 --- a/authentik/core/tests/test_applications_api.py +++ b/authentik/core/tests/test_applications_api.py @@ -1,6 +1,5 @@ """Test Applications API""" from django.urls import reverse -from django.utils.encoding import force_str from rest_framework.test import APITestCase from authentik.core.models import Application @@ -32,7 +31,7 @@ class TestApplicationsAPI(APITestCase): ) ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual(force_str(response.content), {"messages": [], "passing": True}) + self.assertJSONEqual(response.content.decode(), {"messages": [], "passing": True}) response = self.client.get( reverse( "authentik_api:application-check-access", @@ -40,14 +39,14 @@ class TestApplicationsAPI(APITestCase): ) ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual(force_str(response.content), {"messages": ["dummy"], "passing": False}) + self.assertJSONEqual(response.content.decode(), {"messages": ["dummy"], "passing": False}) def test_list(self): """Test list operation without superuser_full_list""" self.client.force_login(self.user) response = self.client.get(reverse("authentik_api:application-list")) self.assertJSONEqual( - force_str(response.content), + response.content.decode(), { "pagination": { "next": 0, @@ -83,7 +82,7 @@ class TestApplicationsAPI(APITestCase): reverse("authentik_api:application-list") + "?superuser_full_list=true" ) self.assertJSONEqual( - force_str(response.content), + response.content.decode(), { "pagination": { "next": 0, diff --git a/authentik/core/tests/test_authenticated_sessions_api.py b/authentik/core/tests/test_authenticated_sessions_api.py index b204dfe1f..51430346b 100644 --- a/authentik/core/tests/test_authenticated_sessions_api.py +++ b/authentik/core/tests/test_authenticated_sessions_api.py @@ -2,7 +2,6 @@ from json import loads from django.urls.base import reverse -from django.utils.encoding import force_str from rest_framework.test import APITestCase from authentik.core.models import User @@ -28,5 +27,5 @@ class TestAuthenticatedSessionsAPI(APITestCase): self.client.force_login(self.other_user) response = self.client.get(reverse("authentik_api:authenticatedsession-list")) self.assertEqual(response.status_code, 200) - body = loads(force_str(response.content)) + body = loads(response.content.decode()) self.assertEqual(body["pagination"]["count"], 1) diff --git a/authentik/flows/tests/__init__.py b/authentik/flows/tests/__init__.py index e69de29bb..da21f8e88 100644 --- a/authentik/flows/tests/__init__.py +++ b/authentik/flows/tests/__init__.py @@ -0,0 +1,51 @@ +"""Test helpers""" +from json import loads +from typing import Any, Optional + +from django.http.response import HttpResponse +from django.urls.base import reverse +from rest_framework.test import APITestCase + +from authentik.core.models import User +from authentik.flows.challenge import ChallengeTypes +from authentik.flows.models import Flow + + +class FlowTestCase(APITestCase): + """Helpers for testing flows and stages.""" + + # pylint: disable=invalid-name + def assertStageResponse( + self, + response: HttpResponse, + flow: Optional[Flow] = None, + user: Optional[User] = None, + **kwargs, + ) -> dict[str, Any]: + """Assert various attributes of a stage response""" + raw_response = loads(response.content.decode()) + self.assertIsNotNone(raw_response["component"]) + self.assertIsNotNone(raw_response["type"]) + if flow: + self.assertIn("flow_info", raw_response) + self.assertEqual(raw_response["flow_info"]["background"], flow.background_url) + self.assertEqual( + raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel") + ) + # We don't check the flow title since it will most likely go + # through ChallengeStageView.format_title() so might not match 1:1 + # self.assertEqual(raw_response["flow_info"]["title"], flow.title) + self.assertIsNotNone(raw_response["flow_info"]["title"]) + if user: + self.assertEqual(raw_response["pending_user"], user.username) + self.assertEqual(raw_response["pending_user_avatar"], user.avatar) + for key, expected in kwargs.items(): + self.assertEqual(raw_response[key], expected) + return raw_response + + # pylint: disable=invalid-name + def assertStageRedirects(self, response: HttpResponse, to: str) -> dict[str, Any]: + """Wrapper around assertStageResponse that checks for a redirect""" + return self.assertStageResponse( + response, component="xak-flow-redirect", to=to, type=ChallengeTypes.REDIRECT.value + ) diff --git a/authentik/flows/tests/test_executor.py b/authentik/flows/tests/test_executor.py index 968eb163a..01017819e 100644 --- a/authentik/flows/tests/test_executor.py +++ b/authentik/flows/tests/test_executor.py @@ -4,16 +4,14 @@ from unittest.mock import MagicMock, PropertyMock, patch from django.http import HttpRequest, HttpResponse from django.test.client import RequestFactory from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction from authentik.flows.planner import FlowPlan, FlowPlanner from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView +from authentik.flows.tests import FlowTestCase from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView from authentik.lib.config import CONFIG from authentik.policies.dummy.models import DummyPolicy @@ -37,7 +35,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse): TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response) -class TestFlowExecutor(APITestCase): +class TestFlowExecutor(FlowTestCase): """Test executor""" def setUp(self): @@ -90,18 +88,11 @@ class TestFlowExecutor(APITestCase): reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": FlowNonApplicableException.__doc__, - "flow_info": { - "background": flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - "type": ChallengeTypes.NATIVE.value, - }, + self.assertStageResponse( + response, + flow=flow, + error_message=FlowNonApplicableException.__doc__, + component="ak-stage-access-denied", ) @patch( @@ -283,14 +274,7 @@ class TestFlowExecutor(APITestCase): # We do this request without the patch, so the policy results in false response = self.client.post(exec_url) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_reevaluate_keep(self): """Test planner with re-evaluate (everything is kept)""" @@ -360,14 +344,7 @@ class TestFlowExecutor(APITestCase): # We do this request without the patch, so the policy results in false response = self.client.post(exec_url) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_reevaluate_remove_consecutive(self): """Test planner with re-evaluate (consecutive stages are removed)""" @@ -407,18 +384,7 @@ class TestFlowExecutor(APITestCase): # First request, run the planner response = self.client.get(exec_url) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "type": ChallengeTypes.NATIVE.value, - "component": "ak-stage-dummy", - "flow_info": { - "background": flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - }, - ) + self.assertStageResponse(response, flow, component="ak-stage-dummy") plan: FlowPlan = self.client.session[SESSION_KEY_PLAN] @@ -441,31 +407,13 @@ class TestFlowExecutor(APITestCase): # but it won't save it, hence we can't check the plan response = self.client.get(exec_url) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "type": ChallengeTypes.NATIVE.value, - "component": "ak-stage-dummy", - "flow_info": { - "background": flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - }, - ) + self.assertStageResponse(response, flow, component="ak-stage-dummy") # fourth request, this confirms the last stage (dummy4) # We do this request without the patch, so the policy results in false response = self.client.post(exec_url) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_stageview_user_identifier(self): """Test PLAN_CONTEXT_PENDING_USER_IDENTIFIER""" @@ -532,35 +480,16 @@ class TestFlowExecutor(APITestCase): # First request, run the planner response = self.client.get(exec_url) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "type": ChallengeTypes.NATIVE.value, - "component": "ak-stage-identification", - "flow_info": { - "background": flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - "password_fields": False, - "primary_action": "Log in", - "sources": [], - "show_source_labels": False, - "user_fields": [UserFields.E_MAIL], - }, + self.assertStageResponse( + response, + flow, + component="ak-stage-identification", + password_fields=False, + primary_action="Log in", + sources=[], + show_source_labels=False, + user_fields=[UserFields.E_MAIL], ) response = self.client.post(exec_url, {"uid_field": "invalid-string"}, follow=True) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "flow_info": { - "background": flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - "type": ChallengeTypes.NATIVE.value, - }, - ) + self.assertStageResponse(response, flow, component="ak-stage-access-denied") diff --git a/authentik/policies/password/tests/test_flows.py b/authentik/policies/password/tests/test_flows.py index ca2322f05..58b6c8d9c 100644 --- a/authentik/policies/password/tests/test_flows.py +++ b/authentik/policies/password/tests/test_flows.py @@ -1,16 +1,14 @@ """Password flow tests""" from django.urls.base import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding +from authentik.flows.tests import FlowTestCase from authentik.policies.password.models import PasswordPolicy from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage -class TestPasswordPolicyFlow(APITestCase): +class TestPasswordPolicyFlow(FlowTestCase): """Test Password Policy""" def setUp(self) -> None: @@ -53,29 +51,22 @@ class TestPasswordPolicyFlow(APITestCase): {"password": "akadmin"}, ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-prompt", - "fields": [ - { - "field_key": "password", - "label": "PASSWORD_LABEL", - "order": 0, - "placeholder": "PASSWORD_PLACEHOLDER", - "required": True, - "type": "password", - "sub_text": "", - } - ], - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - "response_errors": { - "non_field_errors": [{"code": "invalid", "string": self.policy.error_message}] - }, - "type": ChallengeTypes.NATIVE.value, + self.assertStageResponse( + response, + self.flow, + component="ak-stage-prompt", + fields=[ + { + "field_key": "password", + "label": "PASSWORD_LABEL", + "order": 0, + "placeholder": "PASSWORD_PLACEHOLDER", + "required": True, + "type": "password", + "sub_text": "", + } + ], + response_errors={ + "non_field_errors": [{"code": "invalid", "string": self.policy.error_message}] }, ) diff --git a/authentik/providers/oauth2/tests/test_authorize.py b/authentik/providers/oauth2/tests/test_authorize.py index cd5b160fc..660b90a79 100644 --- a/authentik/providers/oauth2/tests/test_authorize.py +++ b/authentik/providers/oauth2/tests/test_authorize.py @@ -1,7 +1,6 @@ """Test authorize view""" from django.test import RequestFactory from django.urls import reverse -from django.utils.encoding import force_str from authentik.core.models import Application from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow @@ -201,7 +200,7 @@ class TestAuthorize(OAuthTestCase): ) code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first() self.assertJSONEqual( - force_str(response.content), + response.content.decode(), { "component": "xak-flow-redirect", "type": ChallengeTypes.REDIRECT.value, @@ -240,7 +239,7 @@ class TestAuthorize(OAuthTestCase): ) token: RefreshToken = RefreshToken.objects.filter(user=user).first() self.assertJSONEqual( - force_str(response.content), + response.content.decode(), { "component": "xak-flow-redirect", "type": ChallengeTypes.REDIRECT.value, diff --git a/authentik/providers/oauth2/tests/test_jwks.py b/authentik/providers/oauth2/tests/test_jwks.py index 329323855..209a0215b 100644 --- a/authentik/providers/oauth2/tests/test_jwks.py +++ b/authentik/providers/oauth2/tests/test_jwks.py @@ -3,7 +3,6 @@ import json from django.test import RequestFactory from django.urls.base import reverse -from django.utils.encoding import force_str from authentik.core.models import Application from authentik.core.tests.utils import create_test_cert, create_test_flow @@ -31,7 +30,7 @@ class TestJWKS(OAuthTestCase): response = self.client.get( reverse("authentik_providers_oauth2:jwks", kwargs={"application_slug": app.slug}) ) - body = json.loads(force_str(response.content)) + body = json.loads(response.content.decode()) self.assertEqual(len(body["keys"]), 1) def test_hs256(self): @@ -46,4 +45,4 @@ class TestJWKS(OAuthTestCase): response = self.client.get( reverse("authentik_providers_oauth2:jwks", kwargs={"application_slug": app.slug}) ) - self.assertJSONEqual(force_str(response.content), {}) + self.assertJSONEqual(response.content.decode(), {}) diff --git a/authentik/providers/oauth2/tests/test_token.py b/authentik/providers/oauth2/tests/test_token.py index bb2479a8f..bc1dba975 100644 --- a/authentik/providers/oauth2/tests/test_token.py +++ b/authentik/providers/oauth2/tests/test_token.py @@ -3,7 +3,6 @@ from base64 import b64encode from django.test import RequestFactory from django.urls import reverse -from django.utils.encoding import force_str from authentik.core.models import Application from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow @@ -135,7 +134,7 @@ class TestToken(OAuthTestCase): ) new_token: RefreshToken = RefreshToken.objects.filter(user=user).first() self.assertJSONEqual( - force_str(response.content), + response.content.decode(), { "access_token": new_token.access_token, "refresh_token": new_token.refresh_token, @@ -184,7 +183,7 @@ class TestToken(OAuthTestCase): self.assertEqual(response["Access-Control-Allow-Credentials"], "true") self.assertEqual(response["Access-Control-Allow-Origin"], "http://local.invalid") self.assertJSONEqual( - force_str(response.content), + response.content.decode(), { "access_token": new_token.access_token, "refresh_token": new_token.refresh_token, @@ -230,7 +229,7 @@ class TestToken(OAuthTestCase): self.assertNotIn("Access-Control-Allow-Credentials", response) self.assertNotIn("Access-Control-Allow-Origin", response) self.assertJSONEqual( - force_str(response.content), + response.content.decode(), { "access_token": new_token.access_token, "refresh_token": new_token.refresh_token, diff --git a/authentik/providers/oauth2/tests/test_userinfo.py b/authentik/providers/oauth2/tests/test_userinfo.py index 159f97386..25756a836 100644 --- a/authentik/providers/oauth2/tests/test_userinfo.py +++ b/authentik/providers/oauth2/tests/test_userinfo.py @@ -3,7 +3,6 @@ import json from dataclasses import asdict from django.urls import reverse -from django.utils.encoding import force_str from authentik.core.models import Application from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow @@ -54,7 +53,7 @@ class TestUserinfo(OAuthTestCase): HTTP_AUTHORIZATION=f"Bearer {self.token.access_token}", ) self.assertJSONEqual( - force_str(res.content), + res.content.decode(), { "name": self.user.name, "given_name": self.user.name, @@ -77,7 +76,7 @@ class TestUserinfo(OAuthTestCase): HTTP_AUTHORIZATION=f"Bearer {self.token.access_token}", ) self.assertJSONEqual( - force_str(res.content), + res.content.decode(), { "name": self.user.name, "given_name": self.user.name, diff --git a/authentik/stages/authenticator_validate/tests.py b/authentik/stages/authenticator_validate/tests.py index 29643df0a..3b9ee8879 100644 --- a/authentik/stages/authenticator_validate/tests.py +++ b/authentik/stages/authenticator_validate/tests.py @@ -3,15 +3,13 @@ from unittest.mock import MagicMock, patch from django.test.client import RequestFactory from django.urls.base import reverse -from django.utils.encoding import force_str from django_otp.plugins.otp_totp.models import TOTPDevice from rest_framework.exceptions import ValidationError -from rest_framework.test import APITestCase from webauthn.helpers import bytes_to_base64url from authentik.core.tests.utils import create_test_admin_user -from authentik.flows.challenge import ChallengeTypes from authentik.flows.models import Flow, FlowStageBinding, NotConfiguredAction +from authentik.flows.tests import FlowTestCase from authentik.lib.generators import generate_id, generate_key from authentik.lib.tests.utils import get_request from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice @@ -27,7 +25,7 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice from authentik.stages.identification.models import IdentificationStage, UserFields -class AuthenticatorValidateStageTests(APITestCase): +class AuthenticatorValidateStageTests(FlowTestCase): """Test validator stage""" def setUp(self) -> None: @@ -61,22 +59,15 @@ class AuthenticatorValidateStageTests(APITestCase): follow=True, ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "type": ChallengeTypes.NATIVE.value, - "component": "ak-stage-identification", - "password_fields": False, - "primary_action": "Log in", - "flow_info": { - "background": flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": flow.title, - }, - "user_fields": ["username"], - "sources": [], - "show_source_labels": False, - }, + self.assertStageResponse( + response, + flow, + component="ak-stage-identification", + password_fields=False, + primary_action="Log in", + user_fields=["username"], + sources=[], + show_source_labels=False, ) def test_stage_validation(self): diff --git a/authentik/stages/captcha/tests.py b/authentik/stages/captcha/tests.py index a2def4666..9af1edc76 100644 --- a/authentik/stages/captcha/tests.py +++ b/authentik/stages/captcha/tests.py @@ -1,13 +1,11 @@ """captcha tests""" from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.stages.captcha.models import CaptchaStage @@ -16,7 +14,7 @@ RECAPTCHA_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI" RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" -class TestCaptchaStage(APITestCase): +class TestCaptchaStage(FlowTestCase): """Captcha tests""" def setUp(self): @@ -46,11 +44,4 @@ class TestCaptchaStage(APITestCase): {"token": "PASSED"}, ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) diff --git a/authentik/stages/consent/tests.py b/authentik/stages/consent/tests.py index 3d8522c45..97f616ae3 100644 --- a/authentik/stages/consent/tests.py +++ b/authentik/stages/consent/tests.py @@ -2,20 +2,18 @@ from time import sleep from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import Application, User from authentik.core.tasks import clean_expired_models -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent -class TestConsentStage(APITestCase): +class TestConsentStage(FlowTestCase): """Consent tests""" def setUp(self): @@ -46,15 +44,7 @@ class TestConsentStage(APITestCase): ) # pylint: disable=no-member self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - # pylint: disable=no-member - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertFalse(UserConsent.objects.filter(user=self.user).exists()) def test_permanent(self): @@ -82,14 +72,7 @@ class TestConsentStage(APITestCase): {}, ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertTrue( UserConsent.objects.filter(user=self.user, application=self.application).exists() ) @@ -121,14 +104,7 @@ class TestConsentStage(APITestCase): {}, ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertTrue( UserConsent.objects.filter(user=self.user, application=self.application).exists() ) diff --git a/authentik/stages/deny/tests.py b/authentik/stages/deny/tests.py index b87559158..15acd50b8 100644 --- a/authentik/stages/deny/tests.py +++ b/authentik/stages/deny/tests.py @@ -1,18 +1,16 @@ """deny tests""" from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.stages.deny.models import DenyStage -class TestUserDenyStage(APITestCase): +class TestUserDenyStage(FlowTestCase): """Deny tests""" def setUp(self): @@ -39,19 +37,7 @@ class TestUserDenyStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - "type": ChallengeTypes.NATIVE.value, - }, - ) + self.assertStageResponse(response, self.flow, component="ak-stage-access-denied") def test_valid_post(self): """Test with a valid pending user and backend""" @@ -65,16 +51,4 @@ class TestUserDenyStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - "type": ChallengeTypes.NATIVE.value, - }, - ) + self.assertStageResponse(response, self.flow, component="ak-stage-access-denied") diff --git a/authentik/stages/dummy/tests.py b/authentik/stages/dummy/tests.py index 6a16be491..2367edff2 100644 --- a/authentik/stages/dummy/tests.py +++ b/authentik/stages/dummy/tests.py @@ -1,15 +1,13 @@ """dummy tests""" from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding +from authentik.flows.tests import FlowTestCase from authentik.stages.dummy.models import DummyStage -class TestDummyStage(APITestCase): +class TestDummyStage(FlowTestCase): """Dummy tests""" def setUp(self): @@ -42,11 +40,4 @@ class TestDummyStage(APITestCase): url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) response = self.client.post(url, {}) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) diff --git a/authentik/stages/email/tests/test_stage.py b/authentik/stages/email/tests/test_stage.py index cfced006d..79bea37aa 100644 --- a/authentik/stages/email/tests/test_stage.py +++ b/authentik/stages/email/tests/test_stage.py @@ -5,21 +5,19 @@ from django.core import mail from django.core.mail.backends.locmem import EmailBackend from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend from django.urls import reverse -from django.utils.encoding import force_str from django.utils.http import urlencode -from rest_framework.test import APITestCase from authentik.core.models import Token, User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.stages.email.models import EmailStage from authentik.stages.email.stage import QS_KEY_TOKEN -class TestEmailStage(APITestCase): +class TestEmailStage(FlowTestCase): """Email tests""" def setUp(self): @@ -123,14 +121,7 @@ class TestEmailStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) session = self.client.session plan: FlowPlan = session[SESSION_KEY_PLAN] diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index f85ebca78..5203da0ed 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -1,11 +1,10 @@ """identification tests""" from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User from authentik.flows.challenge import ChallengeTypes from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding +from authentik.flows.tests import FlowTestCase from authentik.lib.generators import generate_key from authentik.sources.oauth.models import OAuthSource from authentik.stages.identification.models import IdentificationStage, UserFields @@ -13,7 +12,7 @@ from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.models import PasswordStage -class TestIdentificationStage(APITestCase): +class TestIdentificationStage(FlowTestCase): """Identification tests""" def setUp(self): @@ -56,14 +55,7 @@ class TestIdentificationStage(APITestCase): url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) response = self.client.post(url, form_data) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_valid_with_password(self): """Test with valid email and password in single step""" @@ -74,14 +66,7 @@ class TestIdentificationStage(APITestCase): url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) response = self.client.post(url, form_data) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_invalid_with_password(self): """Test with valid email and invalid password in single step""" @@ -95,37 +80,28 @@ class TestIdentificationStage(APITestCase): url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) response = self.client.post(url, form_data) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "type": ChallengeTypes.NATIVE.value, - "component": "ak-stage-identification", - "password_fields": True, - "primary_action": "Log in", - "response_errors": { - "non_field_errors": [ - {"code": "invalid", "string": "Failed to " "authenticate."} - ] - }, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - "sources": [ - { - "challenge": { - "component": "xak-flow-redirect", - "to": "/source/oauth/login/test/", - "type": ChallengeTypes.REDIRECT.value, - }, - "icon_url": "/static/authentik/sources/default.svg", - "name": "test", - } - ], - "show_source_labels": False, - "user_fields": ["email"], + self.assertStageResponse( + response, + self.flow, + component="ak-stage-identification", + password_fields=True, + primary_action="Log in", + response_errors={ + "non_field_errors": [{"code": "invalid", "string": "Failed to " "authenticate."}] }, + sources=[ + { + "challenge": { + "component": "xak-flow-redirect", + "to": "/source/oauth/login/test/", + "type": ChallengeTypes.REDIRECT.value, + }, + "icon_url": "/static/authentik/sources/default.svg", + "name": "test", + } + ], + show_source_labels=False, + user_fields=["email"], ) def test_invalid_with_username(self): @@ -147,37 +123,28 @@ class TestIdentificationStage(APITestCase): form_data, ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "type": ChallengeTypes.NATIVE.value, - "component": "ak-stage-identification", - "password_fields": False, - "primary_action": "Log in", - "response_errors": { - "non_field_errors": [ - {"code": "invalid", "string": "Failed to " "authenticate."} - ] - }, - "show_source_labels": False, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - "sources": [ - { - "challenge": { - "component": "xak-flow-redirect", - "to": "/source/oauth/login/test/", - "type": ChallengeTypes.REDIRECT.value, - }, - "icon_url": "/static/authentik/sources/default.svg", - "name": "test", - } - ], - "user_fields": [], + self.assertStageResponse( + response, + self.flow, + component="ak-stage-identification", + password_fields=False, + primary_action="Log in", + response_errors={ + "non_field_errors": [{"code": "invalid", "string": "Failed to " "authenticate."}] }, + show_source_labels=False, + sources=[ + { + "challenge": { + "component": "xak-flow-redirect", + "to": "/source/oauth/login/test/", + "type": ChallengeTypes.REDIRECT.value, + }, + "icon_url": "/static/authentik/sources/default.svg", + "name": "test", + } + ], + user_fields=[], ) def test_invalid_with_invalid_email(self): @@ -209,36 +176,29 @@ class TestIdentificationStage(APITestCase): reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "type": ChallengeTypes.NATIVE.value, - "component": "ak-stage-identification", - "user_fields": ["email"], - "password_fields": False, - "enroll_url": reverse( - "authentik_core:if-flow", - kwargs={"flow_slug": "unique-enrollment-string"}, - ), - "show_source_labels": False, - "primary_action": "Log in", - "flow_info": { - "background": flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": self.flow.title, - }, - "sources": [ - { - "icon_url": "/static/authentik/sources/default.svg", - "name": "test", - "challenge": { - "component": "xak-flow-redirect", - "to": "/source/oauth/login/test/", - "type": ChallengeTypes.REDIRECT.value, - }, - } - ], - }, + self.assertStageResponse( + response, + self.flow, + component="ak-stage-identification", + user_fields=["email"], + password_fields=False, + enroll_url=reverse( + "authentik_core:if-flow", + kwargs={"flow_slug": "unique-enrollment-string"}, + ), + show_source_labels=False, + primary_action="Log in", + sources=[ + { + "icon_url": "/static/authentik/sources/default.svg", + "name": "test", + "challenge": { + "component": "xak-flow-redirect", + "to": "/source/oauth/login/test/", + "type": ChallengeTypes.REDIRECT.value, + }, + } + ], ) def test_recovery_flow(self): @@ -259,34 +219,27 @@ class TestIdentificationStage(APITestCase): reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "type": ChallengeTypes.NATIVE.value, - "component": "ak-stage-identification", - "user_fields": ["email"], - "password_fields": False, - "recovery_url": reverse( - "authentik_core:if-flow", - kwargs={"flow_slug": "unique-recovery-string"}, - ), - "show_source_labels": False, - "primary_action": "Log in", - "flow_info": { - "background": flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": self.flow.title, - }, - "sources": [ - { - "challenge": { - "component": "xak-flow-redirect", - "to": "/source/oauth/login/test/", - "type": ChallengeTypes.REDIRECT.value, - }, - "icon_url": "/static/authentik/sources/default.svg", - "name": "test", - } - ], - }, + self.assertStageResponse( + response, + self.flow, + component="ak-stage-identification", + user_fields=["email"], + password_fields=False, + recovery_url=reverse( + "authentik_core:if-flow", + kwargs={"flow_slug": "unique-recovery-string"}, + ), + show_source_labels=False, + primary_action="Log in", + sources=[ + { + "challenge": { + "component": "xak-flow-redirect", + "to": "/source/oauth/login/test/", + "type": ChallengeTypes.REDIRECT.value, + }, + "icon_url": "/static/authentik/sources/default.svg", + "name": "test", + } + ], ) diff --git a/authentik/stages/invitation/tests.py b/authentik/stages/invitation/tests.py index fc279f37b..66f46b0e6 100644 --- a/authentik/stages/invitation/tests.py +++ b/authentik/stages/invitation/tests.py @@ -2,17 +2,16 @@ from unittest.mock import MagicMock, patch from django.urls import reverse -from django.utils.encoding import force_str from django.utils.http import urlencode from guardian.shortcuts import get_anonymous_user from rest_framework.test import APITestCase from authentik.core.models import User from authentik.core.tests.utils import create_test_admin_user -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.stages.invitation.models import Invitation, InvitationStage @@ -25,7 +24,7 @@ from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND -class TestUserLoginStage(APITestCase): +class TestUserLoginStage(FlowTestCase): """Login tests""" def setUp(self): @@ -57,18 +56,10 @@ class TestUserLoginStage(APITestCase): reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "type": ChallengeTypes.NATIVE.value, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - }, + self.assertStageResponse( + response, + flow=self.flow, + component="ak-stage-access-denied", ) def test_without_invitation_continue(self): @@ -87,14 +78,7 @@ class TestUserLoginStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.stage.continue_flow_without_invitation = False self.stage.save() @@ -119,14 +103,7 @@ class TestUserLoginStage(APITestCase): self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_with_invitation_prompt_data(self): """Test with invitation, check data in session""" @@ -152,14 +129,7 @@ class TestUserLoginStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertFalse(Invitation.objects.filter(pk=invite.pk)) diff --git a/authentik/stages/password/tests.py b/authentik/stages/password/tests.py index abb37e310..1792f22b3 100644 --- a/authentik/stages/password/tests.py +++ b/authentik/stages/password/tests.py @@ -3,14 +3,12 @@ from unittest.mock import MagicMock, patch from django.core.exceptions import PermissionDenied from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.lib.generators import generate_key @@ -20,7 +18,7 @@ from authentik.stages.password.models import PasswordStage MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test")) -class TestPasswordStage(APITestCase): +class TestPasswordStage(FlowTestCase): """Password tests""" def setUp(self): @@ -56,18 +54,11 @@ class TestPasswordStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "type": ChallengeTypes.NATIVE.value, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - }, + self.assertStageResponse( + response, + self.flow, + component="ak-stage-access-denied", + error_message="Unknown error", ) def test_recovery_flow_link(self): @@ -83,7 +74,7 @@ class TestPasswordStage(APITestCase): reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), ) self.assertEqual(response.status_code, 200) - self.assertIn(flow.slug, force_str(response.content)) + self.assertIn(flow.slug, response.content.decode()) def test_valid_password(self): """Test with a valid pending user and valid password""" @@ -100,14 +91,7 @@ class TestPasswordStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_invalid_password(self): """Test with a valid pending user and invalid password""" @@ -176,16 +160,9 @@ class TestPasswordStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - "type": ChallengeTypes.NATIVE.value, - }, + self.assertStageResponse( + response, + self.flow, + component="ak-stage-access-denied", + error_message="Unknown error", ) diff --git a/authentik/stages/prompt/tests.py b/authentik/stages/prompt/tests.py index 95643d539..94084a69a 100644 --- a/authentik/stages/prompt/tests.py +++ b/authentik/stages/prompt/tests.py @@ -2,23 +2,21 @@ from unittest.mock import MagicMock, patch from django.urls import reverse -from django.utils.encoding import force_str from rest_framework.exceptions import ErrorDetail -from rest_framework.test import APITestCase from authentik.core.models import User from authentik.core.tests.utils import create_test_admin_user -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.policies.expression.models import ExpressionPolicy from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse -class TestPromptStage(APITestCase): +class TestPromptStage(FlowTestCase): """Prompt tests""" def setUp(self): @@ -123,9 +121,9 @@ class TestPromptStage(APITestCase): ) self.assertEqual(response.status_code, 200) for prompt in self.stage.fields.all(): - self.assertIn(prompt.field_key, force_str(response.content)) - self.assertIn(prompt.label, force_str(response.content)) - self.assertIn(prompt.placeholder, force_str(response.content)) + self.assertIn(prompt.field_key, response.content.decode()) + self.assertIn(prompt.label, response.content.decode()) + self.assertIn(prompt.placeholder, response.content.decode()) def test_valid_challenge_with_policy(self) -> PromptChallengeResponse: """Test challenge_response validation""" @@ -171,14 +169,7 @@ class TestPromptStage(APITestCase): challenge_response.validated_data, ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) # Check that valid data has been saved session = self.client.session diff --git a/authentik/stages/user_delete/tests.py b/authentik/stages/user_delete/tests.py index 2b40eceb9..2ae3b276d 100644 --- a/authentik/stages/user_delete/tests.py +++ b/authentik/stages/user_delete/tests.py @@ -2,20 +2,18 @@ from unittest.mock import patch from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.stages.user_delete.models import UserDeleteStage -class TestUserDeleteStage(APITestCase): +class TestUserDeleteStage(FlowTestCase): """Delete tests""" def setUp(self): @@ -46,19 +44,7 @@ class TestUserDeleteStage(APITestCase): reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "type": ChallengeTypes.NATIVE.value, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - }, - ) + self.assertStageResponse(response, self.flow, component="ak-stage-access-denied") def test_user_delete_get(self): """Test Form render""" @@ -72,14 +58,7 @@ class TestUserDeleteStage(APITestCase): reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertFalse(User.objects.filter(username=self.username).exists()) @@ -95,13 +74,6 @@ class TestUserDeleteStage(APITestCase): reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertFalse(User.objects.filter(username=self.username).exists()) diff --git a/authentik/stages/user_login/tests.py b/authentik/stages/user_login/tests.py index e44c6e937..b01974bf8 100644 --- a/authentik/stages/user_login/tests.py +++ b/authentik/stages/user_login/tests.py @@ -3,20 +3,18 @@ from time import sleep from unittest.mock import patch from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.stages.user_login.models import UserLoginStage -class TestUserLoginStage(APITestCase): +class TestUserLoginStage(FlowTestCase): """Login tests""" def setUp(self): @@ -44,14 +42,7 @@ class TestUserLoginStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_valid_post(self): """Test with a valid pending user and backend""" @@ -66,14 +57,7 @@ class TestUserLoginStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_expiry(self): """Test with expiry""" @@ -89,14 +73,7 @@ class TestUserLoginStage(APITestCase): reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) self.assertNotEqual(list(self.client.session.keys()), []) sleep(3) self.client.session.clear_expired() @@ -118,18 +95,10 @@ class TestUserLoginStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "type": ChallengeTypes.NATIVE.value, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - }, + self.assertStageResponse( + response, + self.flow, + component="ak-stage-access-denied", ) def test_inactive_account(self): @@ -147,13 +116,6 @@ class TestUserLoginStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) response = self.client.get(reverse("authentik_api:application-list")) self.assertEqual(response.status_code, 403) diff --git a/authentik/stages/user_logout/tests.py b/authentik/stages/user_logout/tests.py index 0d137f2f8..d48f035bf 100644 --- a/authentik/stages/user_logout/tests.py +++ b/authentik/stages/user_logout/tests.py @@ -1,20 +1,18 @@ """logout tests""" from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import User -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.stages.password import BACKEND_INBUILT from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND from authentik.stages.user_logout.models import UserLogoutStage -class TestUserLogoutStage(APITestCase): +class TestUserLogoutStage(FlowTestCase): """Logout tests""" def setUp(self): @@ -44,15 +42,7 @@ class TestUserLogoutStage(APITestCase): # pylint: disable=no-member self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - # pylint: disable=no-member - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) def test_valid_post(self): """Test with a valid pending user and backend""" @@ -69,12 +59,4 @@ class TestUserLogoutStage(APITestCase): # pylint: disable=no-member self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - # pylint: disable=no-member - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) diff --git a/authentik/stages/user_write/tests.py b/authentik/stages/user_write/tests.py index 17e33fad9..7c3abcedb 100644 --- a/authentik/stages/user_write/tests.py +++ b/authentik/stages/user_write/tests.py @@ -4,23 +4,21 @@ from random import SystemRandom from unittest.mock import patch from django.urls import reverse -from django.utils.encoding import force_str -from rest_framework.test import APITestCase from authentik.core.models import USER_ATTRIBUTE_SOURCES, Group, Source, User, UserSourceConnection from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION from authentik.core.tests.utils import create_test_admin_user -from authentik.flows.challenge import ChallengeTypes from authentik.flows.markers import StageMarker from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan +from authentik.flows.tests import FlowTestCase from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT from authentik.stages.user_write.models import UserWriteStage -class TestUserWriteStage(APITestCase): +class TestUserWriteStage(FlowTestCase): """Write tests""" def setUp(self): @@ -60,14 +58,7 @@ class TestUserWriteStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"]) self.assertTrue(user_qs.exists()) self.assertTrue(user_qs.first().check_password(password)) @@ -98,14 +89,7 @@ class TestUserWriteStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "xak-flow-redirect", - "to": reverse("authentik_core:root-redirect"), - "type": ChallengeTypes.REDIRECT.value, - }, - ) + self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"]) self.assertTrue(user_qs.exists()) self.assertTrue(user_qs.first().check_password(new_password)) @@ -128,18 +112,10 @@ class TestUserWriteStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "type": ChallengeTypes.NATIVE.value, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - }, + self.assertStageResponse( + response, + self.flow, + component="ak-stage-access-denied", ) @patch( @@ -163,18 +139,10 @@ class TestUserWriteStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "type": ChallengeTypes.NATIVE.value, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - }, + self.assertStageResponse( + response, + self.flow, + component="ak-stage-access-denied", ) @patch( @@ -199,16 +167,8 @@ class TestUserWriteStage(APITestCase): ) self.assertEqual(response.status_code, 200) - self.assertJSONEqual( - force_str(response.content), - { - "component": "ak-stage-access-denied", - "error_message": None, - "type": ChallengeTypes.NATIVE.value, - "flow_info": { - "background": self.flow.background_url, - "cancel_url": reverse("authentik_flows:cancel"), - "title": "", - }, - }, + self.assertStageResponse( + response, + self.flow, + component="ak-stage-access-denied", ) diff --git a/authentik/tenants/tests.py b/authentik/tenants/tests.py index 63d948832..ce7098e70 100644 --- a/authentik/tenants/tests.py +++ b/authentik/tenants/tests.py @@ -2,7 +2,6 @@ from django.test import TestCase from django.test.client import RequestFactory from django.urls import reverse -from django.utils.encoding import force_str from authentik.core.tests.utils import create_test_tenant from authentik.events.models import Event, EventAction @@ -18,7 +17,7 @@ class TestTenants(TestCase): """Test Current tenant API""" tenant = create_test_tenant() self.assertJSONEqual( - force_str(self.client.get(reverse("authentik_api:tenant-current")).content), + self.client.get(reverse("authentik_api:tenant-current")).content.decode(), { "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_favicon": "/static/dist/assets/icons/icon.png", @@ -33,11 +32,9 @@ class TestTenants(TestCase): Tenant.objects.all().delete() Tenant.objects.create(domain="bar.baz", branding_title="custom") self.assertJSONEqual( - force_str( - self.client.get( - reverse("authentik_api:tenant-current"), HTTP_HOST="foo.bar.baz" - ).content - ), + self.client.get( + reverse("authentik_api:tenant-current"), HTTP_HOST="foo.bar.baz" + ).content.decode(), { "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_favicon": "/static/dist/assets/icons/icon.png", @@ -51,7 +48,7 @@ class TestTenants(TestCase): """Test fallback tenant""" Tenant.objects.all().delete() self.assertJSONEqual( - force_str(self.client.get(reverse("authentik_api:tenant-current")).content), + self.client.get(reverse("authentik_api:tenant-current")).content.decode(), { "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", "branding_favicon": "/static/dist/assets/icons/icon.png", diff --git a/schema.yml b/schema.yml index 9d8a463d4..53d51b0bf 100644 --- a/schema.yml +++ b/schema.yml @@ -19036,9 +19036,15 @@ components: type: array items: $ref: '#/components/schemas/ErrorDetail' + pending_user: + type: string + pending_user_avatar: + type: string error_message: type: string required: + - pending_user + - pending_user_avatar - type App: type: object