authentik.stages.invitation.tests
invitation tests
1"""invitation tests""" 2 3from datetime import timedelta 4from unittest.mock import MagicMock, patch 5 6from django.urls import reverse 7from django.utils.http import urlencode 8from django.utils.timezone import now 9from guardian.shortcuts import get_anonymous_user 10from rest_framework.test import APITestCase 11 12from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT 13from authentik.core.tests.utils import create_test_admin_user, create_test_flow 14from authentik.flows.markers import StageMarker 15from authentik.flows.models import FlowDesignation, FlowStageBinding 16from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan 17from authentik.flows.tests import FlowTestCase 18from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK 19from authentik.flows.views.executor import SESSION_KEY_PLAN 20from authentik.stages.invitation.api import InvitationSerializer 21from authentik.stages.invitation.models import Invitation, InvitationStage 22from authentik.stages.invitation.stage import ( 23 PLAN_CONTEXT_INVITATION_TOKEN, 24 PLAN_CONTEXT_PROMPT, 25 QS_INVITATION_TOKEN_KEY, 26) 27from authentik.stages.password import BACKEND_INBUILT 28from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND 29 30 31class TestInvitationStage(FlowTestCase): 32 """Login tests""" 33 34 def setUp(self): 35 super().setUp() 36 self.user = create_test_admin_user() 37 self.flow = create_test_flow(FlowDesignation.AUTHENTICATION) 38 self.stage = InvitationStage.objects.create(name="invitation") 39 self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) 40 41 @patch( 42 "authentik.flows.views.executor.to_stage_response", 43 TO_STAGE_RESPONSE_MOCK, 44 ) 45 def test_without_invitation_fail(self): 46 """Test without any invitation, continue_flow_without_invitation not set.""" 47 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 48 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 49 plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT 50 session = self.client.session 51 session[SESSION_KEY_PLAN] = plan 52 session.save() 53 54 response = self.client.get( 55 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 56 ) 57 self.assertStageResponse( 58 response, 59 flow=self.flow, 60 component="ak-stage-access-denied", 61 ) 62 63 def test_without_invitation_continue(self): 64 """Test without any invitation, continue_flow_without_invitation is set.""" 65 self.stage.continue_flow_without_invitation = True 66 self.stage.save() 67 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 68 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 69 plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT 70 session = self.client.session 71 session[SESSION_KEY_PLAN] = plan 72 session.save() 73 74 response = self.client.get( 75 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 76 ) 77 78 self.assertEqual(response.status_code, 200) 79 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 80 81 self.stage.continue_flow_without_invitation = False 82 self.stage.save() 83 84 def test_with_invitation_expired(self): 85 """Test with invitation, expired""" 86 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 87 session = self.client.session 88 session[SESSION_KEY_PLAN] = plan 89 session.save() 90 91 data = {"foo": "bar"} 92 invite = Invitation.objects.create( 93 created_by=get_anonymous_user(), 94 fixed_data=data, 95 expires=now() - timedelta(hours=1), 96 ) 97 98 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 99 args = urlencode({QS_INVITATION_TOKEN_KEY: invite.pk.hex}) 100 response = self.client.get(base_url + f"?query={args}") 101 102 self.assertEqual(response.status_code, 200) 103 self.assertStageResponse( 104 response, 105 flow=self.flow, 106 component="ak-stage-access-denied", 107 ) 108 109 def test_with_invitation_get(self): 110 """Test with invitation, check data in session""" 111 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 112 session = self.client.session 113 session[SESSION_KEY_PLAN] = plan 114 session.save() 115 116 data = {"foo": "bar"} 117 invite = Invitation.objects.create(created_by=get_anonymous_user(), fixed_data=data) 118 119 with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 120 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 121 args = urlencode({QS_INVITATION_TOKEN_KEY: invite.pk.hex}) 122 response = self.client.get(base_url + f"?query={args}") 123 124 session = self.client.session 125 plan: FlowPlan = session[SESSION_KEY_PLAN] 126 self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data) 127 128 self.assertEqual(response.status_code, 200) 129 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 130 131 def test_invalid_flow(self): 132 """Test with invitation, invalid flow limit""" 133 invalid_flow = create_test_flow(FlowDesignation.ENROLLMENT) 134 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 135 session = self.client.session 136 session[SESSION_KEY_PLAN] = plan 137 session.save() 138 139 data = {"foo": "bar"} 140 invite = Invitation.objects.create( 141 created_by=get_anonymous_user(), fixed_data=data, flow=invalid_flow 142 ) 143 144 with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 145 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 146 args = urlencode({QS_INVITATION_TOKEN_KEY: invite.pk.hex}) 147 response = self.client.get(base_url + f"?query={args}") 148 149 session = self.client.session 150 plan: FlowPlan = session[SESSION_KEY_PLAN] 151 152 self.assertStageResponse( 153 response, 154 flow=self.flow, 155 component="ak-stage-access-denied", 156 ) 157 158 def test_with_invitation_prompt_data(self): 159 """Test with invitation, check data in session""" 160 data = {"foo": "bar"} 161 invite = Invitation.objects.create( 162 created_by=get_anonymous_user(), fixed_data=data, single_use=True 163 ) 164 165 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 166 plan.context[PLAN_CONTEXT_PROMPT] = {PLAN_CONTEXT_INVITATION_TOKEN: invite.pk.hex} 167 session = self.client.session 168 session[SESSION_KEY_PLAN] = plan 169 session.save() 170 171 with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 172 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 173 response = self.client.get(base_url, follow=True) 174 175 session = self.client.session 176 plan: FlowPlan = session[SESSION_KEY_PLAN] 177 self.assertEqual( 178 plan.context[PLAN_CONTEXT_PROMPT], data | plan.context[PLAN_CONTEXT_PROMPT] 179 ) 180 181 self.assertEqual(response.status_code, 200) 182 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 183 self.assertFalse(Invitation.objects.filter(pk=invite.pk)) 184 185 186class TestInvitationsAPI(APITestCase): 187 """Test Invitations API""" 188 189 def setUp(self) -> None: 190 super().setUp() 191 self.user = create_test_admin_user() 192 self.client.force_login(self.user) 193 194 def test_invite_create(self): 195 """Test Invitations creation endpoint""" 196 response = self.client.post( 197 reverse("authentik_api:invitation-list"), 198 {"name": "test-token", "fixed_data": {}}, 199 format="json", 200 ) 201 self.assertEqual(response.status_code, 201) 202 self.assertEqual(Invitation.objects.first().created_by, self.user) 203 204 def test_invite_create_blueprint_context(self): 205 """Test Invitations creation via blueprint context""" 206 207 flow = create_test_flow(FlowDesignation.ENROLLMENT) 208 data = { 209 "name": "test-blueprint-invitation", 210 "flow": flow.pk.hex, 211 "single_use": True, 212 "fixed_data": {"email": "test@example.com"}, 213 } 214 serializer = InvitationSerializer(data=data, context={SERIALIZER_CONTEXT_BLUEPRINT: True}) 215 self.assertTrue(serializer.is_valid()) 216 invitation = serializer.save() 217 self.assertEqual(invitation.created_by, get_anonymous_user()) 218 self.assertEqual(invitation.name, "test-blueprint-invitation") 219 self.assertEqual(invitation.fixed_data, {"email": "test@example.com"}) 220 221 def test_send_email_no_addresses(self): 222 """Test send_email endpoint with no email addresses""" 223 flow = create_test_flow(FlowDesignation.ENROLLMENT) 224 invite = Invitation.objects.create( 225 name="test-invite", 226 created_by=self.user, 227 flow=flow, 228 ) 229 230 response = self.client.post( 231 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 232 {"email_addresses": []}, 233 format="json", 234 ) 235 self.assertEqual(response.status_code, 400) 236 self.assertIn("error", response.data) 237 238 def test_send_email_no_flow(self): 239 """Test send_email endpoint with invitation without flow""" 240 invite = Invitation.objects.create( 241 name="test-invite-no-flow", 242 created_by=self.user, 243 flow=None, 244 ) 245 246 response = self.client.post( 247 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 248 {"email_addresses": ["test@example.com"]}, 249 format="json", 250 ) 251 self.assertEqual(response.status_code, 400) 252 self.assertIn("error", response.data) 253 254 @patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email") 255 def test_send_email_success(self, mock_send_email: MagicMock): 256 """Test send_email endpoint successfully queues emails""" 257 flow = create_test_flow(FlowDesignation.ENROLLMENT) 258 invite = Invitation.objects.create( 259 name="test-invite", 260 created_by=self.user, 261 flow=flow, 262 ) 263 264 response = self.client.post( 265 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 266 { 267 "email_addresses": ["user1@example.com", "user2@example.com"], 268 "template": "email/invitation.html", 269 }, 270 format="json", 271 ) 272 self.assertEqual(response.status_code, 204) 273 self.assertEqual(mock_send_email.call_count, 2) 274 275 @patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email") 276 def test_send_email_with_cc_bcc(self, mock_send_email: MagicMock): 277 """Test send_email endpoint with CC and BCC addresses""" 278 flow = create_test_flow(FlowDesignation.ENROLLMENT) 279 invite = Invitation.objects.create( 280 name="test-invite", 281 created_by=self.user, 282 flow=flow, 283 ) 284 285 response = self.client.post( 286 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 287 { 288 "email_addresses": ["user@example.com"], 289 "cc_addresses": ["cc@example.com"], 290 "bcc_addresses": ["bcc@example.com"], 291 "template": "email/invitation.html", 292 }, 293 format="json", 294 ) 295 self.assertEqual(response.status_code, 204) 296 mock_send_email.assert_called_once() 297 call_kwargs = mock_send_email.call_args.kwargs 298 self.assertEqual(call_kwargs["cc"], ["cc@example.com"]) 299 self.assertEqual(call_kwargs["bcc"], ["bcc@example.com"]) 300 301 @patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email") 302 def test_send_email_context(self, mock_send_email: MagicMock): 303 """Test send_email endpoint passes correct context to email""" 304 flow = create_test_flow(FlowDesignation.ENROLLMENT) 305 invite = Invitation.objects.create( 306 name="test-invite", 307 created_by=self.user, 308 flow=flow, 309 ) 310 311 response = self.client.post( 312 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 313 {"email_addresses": ["user@example.com"]}, 314 format="json", 315 ) 316 self.assertEqual(response.status_code, 204) 317 mock_send_email.assert_called_once() 318 call_kwargs = mock_send_email.call_args.kwargs 319 self.assertIn("url", call_kwargs["context"]) 320 self.assertIn(str(invite.pk), call_kwargs["context"]["url"]) 321 self.assertIn(flow.slug, call_kwargs["context"]["url"])
32class TestInvitationStage(FlowTestCase): 33 """Login tests""" 34 35 def setUp(self): 36 super().setUp() 37 self.user = create_test_admin_user() 38 self.flow = create_test_flow(FlowDesignation.AUTHENTICATION) 39 self.stage = InvitationStage.objects.create(name="invitation") 40 self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2) 41 42 @patch( 43 "authentik.flows.views.executor.to_stage_response", 44 TO_STAGE_RESPONSE_MOCK, 45 ) 46 def test_without_invitation_fail(self): 47 """Test without any invitation, continue_flow_without_invitation not set.""" 48 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 49 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 50 plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT 51 session = self.client.session 52 session[SESSION_KEY_PLAN] = plan 53 session.save() 54 55 response = self.client.get( 56 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 57 ) 58 self.assertStageResponse( 59 response, 60 flow=self.flow, 61 component="ak-stage-access-denied", 62 ) 63 64 def test_without_invitation_continue(self): 65 """Test without any invitation, continue_flow_without_invitation is set.""" 66 self.stage.continue_flow_without_invitation = True 67 self.stage.save() 68 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 69 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 70 plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT 71 session = self.client.session 72 session[SESSION_KEY_PLAN] = plan 73 session.save() 74 75 response = self.client.get( 76 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 77 ) 78 79 self.assertEqual(response.status_code, 200) 80 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 81 82 self.stage.continue_flow_without_invitation = False 83 self.stage.save() 84 85 def test_with_invitation_expired(self): 86 """Test with invitation, expired""" 87 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 88 session = self.client.session 89 session[SESSION_KEY_PLAN] = plan 90 session.save() 91 92 data = {"foo": "bar"} 93 invite = Invitation.objects.create( 94 created_by=get_anonymous_user(), 95 fixed_data=data, 96 expires=now() - timedelta(hours=1), 97 ) 98 99 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 100 args = urlencode({QS_INVITATION_TOKEN_KEY: invite.pk.hex}) 101 response = self.client.get(base_url + f"?query={args}") 102 103 self.assertEqual(response.status_code, 200) 104 self.assertStageResponse( 105 response, 106 flow=self.flow, 107 component="ak-stage-access-denied", 108 ) 109 110 def test_with_invitation_get(self): 111 """Test with invitation, check data in session""" 112 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 113 session = self.client.session 114 session[SESSION_KEY_PLAN] = plan 115 session.save() 116 117 data = {"foo": "bar"} 118 invite = Invitation.objects.create(created_by=get_anonymous_user(), fixed_data=data) 119 120 with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 121 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 122 args = urlencode({QS_INVITATION_TOKEN_KEY: invite.pk.hex}) 123 response = self.client.get(base_url + f"?query={args}") 124 125 session = self.client.session 126 plan: FlowPlan = session[SESSION_KEY_PLAN] 127 self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data) 128 129 self.assertEqual(response.status_code, 200) 130 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 131 132 def test_invalid_flow(self): 133 """Test with invitation, invalid flow limit""" 134 invalid_flow = create_test_flow(FlowDesignation.ENROLLMENT) 135 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 136 session = self.client.session 137 session[SESSION_KEY_PLAN] = plan 138 session.save() 139 140 data = {"foo": "bar"} 141 invite = Invitation.objects.create( 142 created_by=get_anonymous_user(), fixed_data=data, flow=invalid_flow 143 ) 144 145 with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 146 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 147 args = urlencode({QS_INVITATION_TOKEN_KEY: invite.pk.hex}) 148 response = self.client.get(base_url + f"?query={args}") 149 150 session = self.client.session 151 plan: FlowPlan = session[SESSION_KEY_PLAN] 152 153 self.assertStageResponse( 154 response, 155 flow=self.flow, 156 component="ak-stage-access-denied", 157 ) 158 159 def test_with_invitation_prompt_data(self): 160 """Test with invitation, check data in session""" 161 data = {"foo": "bar"} 162 invite = Invitation.objects.create( 163 created_by=get_anonymous_user(), fixed_data=data, single_use=True 164 ) 165 166 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 167 plan.context[PLAN_CONTEXT_PROMPT] = {PLAN_CONTEXT_INVITATION_TOKEN: invite.pk.hex} 168 session = self.client.session 169 session[SESSION_KEY_PLAN] = plan 170 session.save() 171 172 with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 173 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 174 response = self.client.get(base_url, follow=True) 175 176 session = self.client.session 177 plan: FlowPlan = session[SESSION_KEY_PLAN] 178 self.assertEqual( 179 plan.context[PLAN_CONTEXT_PROMPT], data | plan.context[PLAN_CONTEXT_PROMPT] 180 ) 181 182 self.assertEqual(response.status_code, 200) 183 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 184 self.assertFalse(Invitation.objects.filter(pk=invite.pk))
Login tests
35 def setUp(self): 36 super().setUp() 37 self.user = create_test_admin_user() 38 self.flow = create_test_flow(FlowDesignation.AUTHENTICATION) 39 self.stage = InvitationStage.objects.create(name="invitation") 40 self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
Hook method for setting up the test fixture before exercising it.
42 @patch( 43 "authentik.flows.views.executor.to_stage_response", 44 TO_STAGE_RESPONSE_MOCK, 45 ) 46 def test_without_invitation_fail(self): 47 """Test without any invitation, continue_flow_without_invitation not set.""" 48 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 49 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 50 plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT 51 session = self.client.session 52 session[SESSION_KEY_PLAN] = plan 53 session.save() 54 55 response = self.client.get( 56 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 57 ) 58 self.assertStageResponse( 59 response, 60 flow=self.flow, 61 component="ak-stage-access-denied", 62 )
Test without any invitation, continue_flow_without_invitation not set.
64 def test_without_invitation_continue(self): 65 """Test without any invitation, continue_flow_without_invitation is set.""" 66 self.stage.continue_flow_without_invitation = True 67 self.stage.save() 68 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 69 plan.context[PLAN_CONTEXT_PENDING_USER] = self.user 70 plan.context[PLAN_CONTEXT_AUTHENTICATION_BACKEND] = BACKEND_INBUILT 71 session = self.client.session 72 session[SESSION_KEY_PLAN] = plan 73 session.save() 74 75 response = self.client.get( 76 reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 77 ) 78 79 self.assertEqual(response.status_code, 200) 80 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 81 82 self.stage.continue_flow_without_invitation = False 83 self.stage.save()
Test without any invitation, continue_flow_without_invitation is set.
85 def test_with_invitation_expired(self): 86 """Test with invitation, expired""" 87 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 88 session = self.client.session 89 session[SESSION_KEY_PLAN] = plan 90 session.save() 91 92 data = {"foo": "bar"} 93 invite = Invitation.objects.create( 94 created_by=get_anonymous_user(), 95 fixed_data=data, 96 expires=now() - timedelta(hours=1), 97 ) 98 99 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 100 args = urlencode({QS_INVITATION_TOKEN_KEY: invite.pk.hex}) 101 response = self.client.get(base_url + f"?query={args}") 102 103 self.assertEqual(response.status_code, 200) 104 self.assertStageResponse( 105 response, 106 flow=self.flow, 107 component="ak-stage-access-denied", 108 )
Test with invitation, expired
110 def test_with_invitation_get(self): 111 """Test with invitation, check data in session""" 112 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 113 session = self.client.session 114 session[SESSION_KEY_PLAN] = plan 115 session.save() 116 117 data = {"foo": "bar"} 118 invite = Invitation.objects.create(created_by=get_anonymous_user(), fixed_data=data) 119 120 with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 121 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 122 args = urlencode({QS_INVITATION_TOKEN_KEY: invite.pk.hex}) 123 response = self.client.get(base_url + f"?query={args}") 124 125 session = self.client.session 126 plan: FlowPlan = session[SESSION_KEY_PLAN] 127 self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data) 128 129 self.assertEqual(response.status_code, 200) 130 self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
Test with invitation, check data in session
132 def test_invalid_flow(self): 133 """Test with invitation, invalid flow limit""" 134 invalid_flow = create_test_flow(FlowDesignation.ENROLLMENT) 135 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 136 session = self.client.session 137 session[SESSION_KEY_PLAN] = plan 138 session.save() 139 140 data = {"foo": "bar"} 141 invite = Invitation.objects.create( 142 created_by=get_anonymous_user(), fixed_data=data, flow=invalid_flow 143 ) 144 145 with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 146 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 147 args = urlencode({QS_INVITATION_TOKEN_KEY: invite.pk.hex}) 148 response = self.client.get(base_url + f"?query={args}") 149 150 session = self.client.session 151 plan: FlowPlan = session[SESSION_KEY_PLAN] 152 153 self.assertStageResponse( 154 response, 155 flow=self.flow, 156 component="ak-stage-access-denied", 157 )
Test with invitation, invalid flow limit
159 def test_with_invitation_prompt_data(self): 160 """Test with invitation, check data in session""" 161 data = {"foo": "bar"} 162 invite = Invitation.objects.create( 163 created_by=get_anonymous_user(), fixed_data=data, single_use=True 164 ) 165 166 plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) 167 plan.context[PLAN_CONTEXT_PROMPT] = {PLAN_CONTEXT_INVITATION_TOKEN: invite.pk.hex} 168 session = self.client.session 169 session[SESSION_KEY_PLAN] = plan 170 session.save() 171 172 with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()): 173 base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) 174 response = self.client.get(base_url, follow=True) 175 176 session = self.client.session 177 plan: FlowPlan = session[SESSION_KEY_PLAN] 178 self.assertEqual( 179 plan.context[PLAN_CONTEXT_PROMPT], data | plan.context[PLAN_CONTEXT_PROMPT] 180 ) 181 182 self.assertEqual(response.status_code, 200) 183 self.assertStageRedirects(response, reverse("authentik_core:root-redirect")) 184 self.assertFalse(Invitation.objects.filter(pk=invite.pk))
Test with invitation, check data in session
187class TestInvitationsAPI(APITestCase): 188 """Test Invitations API""" 189 190 def setUp(self) -> None: 191 super().setUp() 192 self.user = create_test_admin_user() 193 self.client.force_login(self.user) 194 195 def test_invite_create(self): 196 """Test Invitations creation endpoint""" 197 response = self.client.post( 198 reverse("authentik_api:invitation-list"), 199 {"name": "test-token", "fixed_data": {}}, 200 format="json", 201 ) 202 self.assertEqual(response.status_code, 201) 203 self.assertEqual(Invitation.objects.first().created_by, self.user) 204 205 def test_invite_create_blueprint_context(self): 206 """Test Invitations creation via blueprint context""" 207 208 flow = create_test_flow(FlowDesignation.ENROLLMENT) 209 data = { 210 "name": "test-blueprint-invitation", 211 "flow": flow.pk.hex, 212 "single_use": True, 213 "fixed_data": {"email": "test@example.com"}, 214 } 215 serializer = InvitationSerializer(data=data, context={SERIALIZER_CONTEXT_BLUEPRINT: True}) 216 self.assertTrue(serializer.is_valid()) 217 invitation = serializer.save() 218 self.assertEqual(invitation.created_by, get_anonymous_user()) 219 self.assertEqual(invitation.name, "test-blueprint-invitation") 220 self.assertEqual(invitation.fixed_data, {"email": "test@example.com"}) 221 222 def test_send_email_no_addresses(self): 223 """Test send_email endpoint with no email addresses""" 224 flow = create_test_flow(FlowDesignation.ENROLLMENT) 225 invite = Invitation.objects.create( 226 name="test-invite", 227 created_by=self.user, 228 flow=flow, 229 ) 230 231 response = self.client.post( 232 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 233 {"email_addresses": []}, 234 format="json", 235 ) 236 self.assertEqual(response.status_code, 400) 237 self.assertIn("error", response.data) 238 239 def test_send_email_no_flow(self): 240 """Test send_email endpoint with invitation without flow""" 241 invite = Invitation.objects.create( 242 name="test-invite-no-flow", 243 created_by=self.user, 244 flow=None, 245 ) 246 247 response = self.client.post( 248 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 249 {"email_addresses": ["test@example.com"]}, 250 format="json", 251 ) 252 self.assertEqual(response.status_code, 400) 253 self.assertIn("error", response.data) 254 255 @patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email") 256 def test_send_email_success(self, mock_send_email: MagicMock): 257 """Test send_email endpoint successfully queues emails""" 258 flow = create_test_flow(FlowDesignation.ENROLLMENT) 259 invite = Invitation.objects.create( 260 name="test-invite", 261 created_by=self.user, 262 flow=flow, 263 ) 264 265 response = self.client.post( 266 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 267 { 268 "email_addresses": ["user1@example.com", "user2@example.com"], 269 "template": "email/invitation.html", 270 }, 271 format="json", 272 ) 273 self.assertEqual(response.status_code, 204) 274 self.assertEqual(mock_send_email.call_count, 2) 275 276 @patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email") 277 def test_send_email_with_cc_bcc(self, mock_send_email: MagicMock): 278 """Test send_email endpoint with CC and BCC addresses""" 279 flow = create_test_flow(FlowDesignation.ENROLLMENT) 280 invite = Invitation.objects.create( 281 name="test-invite", 282 created_by=self.user, 283 flow=flow, 284 ) 285 286 response = self.client.post( 287 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 288 { 289 "email_addresses": ["user@example.com"], 290 "cc_addresses": ["cc@example.com"], 291 "bcc_addresses": ["bcc@example.com"], 292 "template": "email/invitation.html", 293 }, 294 format="json", 295 ) 296 self.assertEqual(response.status_code, 204) 297 mock_send_email.assert_called_once() 298 call_kwargs = mock_send_email.call_args.kwargs 299 self.assertEqual(call_kwargs["cc"], ["cc@example.com"]) 300 self.assertEqual(call_kwargs["bcc"], ["bcc@example.com"]) 301 302 @patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email") 303 def test_send_email_context(self, mock_send_email: MagicMock): 304 """Test send_email endpoint passes correct context to email""" 305 flow = create_test_flow(FlowDesignation.ENROLLMENT) 306 invite = Invitation.objects.create( 307 name="test-invite", 308 created_by=self.user, 309 flow=flow, 310 ) 311 312 response = self.client.post( 313 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 314 {"email_addresses": ["user@example.com"]}, 315 format="json", 316 ) 317 self.assertEqual(response.status_code, 204) 318 mock_send_email.assert_called_once() 319 call_kwargs = mock_send_email.call_args.kwargs 320 self.assertIn("url", call_kwargs["context"]) 321 self.assertIn(str(invite.pk), call_kwargs["context"]["url"]) 322 self.assertIn(flow.slug, call_kwargs["context"]["url"])
Test Invitations API
190 def setUp(self) -> None: 191 super().setUp() 192 self.user = create_test_admin_user() 193 self.client.force_login(self.user)
Hook method for setting up the test fixture before exercising it.
195 def test_invite_create(self): 196 """Test Invitations creation endpoint""" 197 response = self.client.post( 198 reverse("authentik_api:invitation-list"), 199 {"name": "test-token", "fixed_data": {}}, 200 format="json", 201 ) 202 self.assertEqual(response.status_code, 201) 203 self.assertEqual(Invitation.objects.first().created_by, self.user)
Test Invitations creation endpoint
205 def test_invite_create_blueprint_context(self): 206 """Test Invitations creation via blueprint context""" 207 208 flow = create_test_flow(FlowDesignation.ENROLLMENT) 209 data = { 210 "name": "test-blueprint-invitation", 211 "flow": flow.pk.hex, 212 "single_use": True, 213 "fixed_data": {"email": "test@example.com"}, 214 } 215 serializer = InvitationSerializer(data=data, context={SERIALIZER_CONTEXT_BLUEPRINT: True}) 216 self.assertTrue(serializer.is_valid()) 217 invitation = serializer.save() 218 self.assertEqual(invitation.created_by, get_anonymous_user()) 219 self.assertEqual(invitation.name, "test-blueprint-invitation") 220 self.assertEqual(invitation.fixed_data, {"email": "test@example.com"})
Test Invitations creation via blueprint context
222 def test_send_email_no_addresses(self): 223 """Test send_email endpoint with no email addresses""" 224 flow = create_test_flow(FlowDesignation.ENROLLMENT) 225 invite = Invitation.objects.create( 226 name="test-invite", 227 created_by=self.user, 228 flow=flow, 229 ) 230 231 response = self.client.post( 232 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 233 {"email_addresses": []}, 234 format="json", 235 ) 236 self.assertEqual(response.status_code, 400) 237 self.assertIn("error", response.data)
Test send_email endpoint with no email addresses
239 def test_send_email_no_flow(self): 240 """Test send_email endpoint with invitation without flow""" 241 invite = Invitation.objects.create( 242 name="test-invite-no-flow", 243 created_by=self.user, 244 flow=None, 245 ) 246 247 response = self.client.post( 248 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 249 {"email_addresses": ["test@example.com"]}, 250 format="json", 251 ) 252 self.assertEqual(response.status_code, 400) 253 self.assertIn("error", response.data)
Test send_email endpoint with invitation without flow
255 @patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email") 256 def test_send_email_success(self, mock_send_email: MagicMock): 257 """Test send_email endpoint successfully queues emails""" 258 flow = create_test_flow(FlowDesignation.ENROLLMENT) 259 invite = Invitation.objects.create( 260 name="test-invite", 261 created_by=self.user, 262 flow=flow, 263 ) 264 265 response = self.client.post( 266 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 267 { 268 "email_addresses": ["user1@example.com", "user2@example.com"], 269 "template": "email/invitation.html", 270 }, 271 format="json", 272 ) 273 self.assertEqual(response.status_code, 204) 274 self.assertEqual(mock_send_email.call_count, 2)
Test send_email endpoint successfully queues emails
276 @patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email") 277 def test_send_email_with_cc_bcc(self, mock_send_email: MagicMock): 278 """Test send_email endpoint with CC and BCC addresses""" 279 flow = create_test_flow(FlowDesignation.ENROLLMENT) 280 invite = Invitation.objects.create( 281 name="test-invite", 282 created_by=self.user, 283 flow=flow, 284 ) 285 286 response = self.client.post( 287 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 288 { 289 "email_addresses": ["user@example.com"], 290 "cc_addresses": ["cc@example.com"], 291 "bcc_addresses": ["bcc@example.com"], 292 "template": "email/invitation.html", 293 }, 294 format="json", 295 ) 296 self.assertEqual(response.status_code, 204) 297 mock_send_email.assert_called_once() 298 call_kwargs = mock_send_email.call_args.kwargs 299 self.assertEqual(call_kwargs["cc"], ["cc@example.com"]) 300 self.assertEqual(call_kwargs["bcc"], ["bcc@example.com"])
Test send_email endpoint with CC and BCC addresses
302 @patch("authentik.stages.invitation.api.BaseEvaluator.expr_send_email") 303 def test_send_email_context(self, mock_send_email: MagicMock): 304 """Test send_email endpoint passes correct context to email""" 305 flow = create_test_flow(FlowDesignation.ENROLLMENT) 306 invite = Invitation.objects.create( 307 name="test-invite", 308 created_by=self.user, 309 flow=flow, 310 ) 311 312 response = self.client.post( 313 reverse("authentik_api:invitation-send-email", kwargs={"pk": invite.pk}), 314 {"email_addresses": ["user@example.com"]}, 315 format="json", 316 ) 317 self.assertEqual(response.status_code, 204) 318 mock_send_email.assert_called_once() 319 call_kwargs = mock_send_email.call_args.kwargs 320 self.assertIn("url", call_kwargs["context"]) 321 self.assertIn(str(invite.pk), call_kwargs["context"]["url"]) 322 self.assertIn(flow.slug, call_kwargs["context"]["url"])
Test send_email endpoint passes correct context to email