authentik.enterprise.lifecycle.tests.test_api

  1from django.apps import apps
  2from django.contrib.contenttypes.models import ContentType
  3from django.urls import reverse
  4from rest_framework.test import APITestCase
  5
  6from authentik.core.models import Application, Group
  7from authentik.core.tests.utils import create_test_admin_user, create_test_user
  8from authentik.enterprise.lifecycle.models import LifecycleIteration, LifecycleRule, ReviewState
  9from authentik.enterprise.reports.tests.utils import patch_license
 10from authentik.lib.generators import generate_id
 11
 12
 13@patch_license
 14class TestLifecycleRuleAPI(APITestCase):
 15
 16    def setUp(self):
 17        self.user = create_test_admin_user()
 18        self.client.force_login(self.user)
 19        self.app = Application.objects.create(name=generate_id(), slug=generate_id())
 20        self.content_type = ContentType.objects.get_for_model(Application)
 21        self.reviewer_group = Group.objects.create(name=generate_id())
 22
 23    @classmethod
 24    def setUpTestData(cls):
 25        config = apps.get_app_config("authentik_tasks_schedules")
 26        config._on_startup_callback(None)
 27
 28    def test_list_rules(self):
 29        rule = LifecycleRule.objects.create(
 30            name=generate_id(),
 31            content_type=self.content_type,
 32            object_id=str(self.app.pk),
 33        )
 34        rule.reviewer_groups.add(self.reviewer_group)
 35
 36        response = self.client.get(reverse("authentik_api:lifecyclerule-list"))
 37        self.assertEqual(response.status_code, 200)
 38        self.assertGreaterEqual(len(response.data["results"]), 1)
 39
 40    def test_create_rule_with_reviewer_group(self):
 41        response = self.client.post(
 42            reverse("authentik_api:lifecyclerule-list"),
 43            {
 44                "name": generate_id(),
 45                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
 46                "object_id": str(self.app.pk),
 47                "interval": "days=30",
 48                "grace_period": "days=10",
 49                "reviewer_groups": [str(self.reviewer_group.pk)],
 50                "reviewers": [],
 51                "min_reviewers": 1,
 52            },
 53        )
 54        self.assertEqual(response.status_code, 201)
 55        self.assertEqual(response.data["object_id"], str(self.app.pk))
 56        self.assertEqual(response.data["interval"], "days=30")
 57
 58    def test_create_rule_with_explicit_reviewer(self):
 59        reviewer = create_test_user()
 60        response = self.client.post(
 61            reverse("authentik_api:lifecyclerule-list"),
 62            {
 63                "name": generate_id(),
 64                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
 65                "object_id": str(self.app.pk),
 66                "interval": "days=60",
 67                "grace_period": "days=15",
 68                "reviewer_groups": [],
 69                "reviewers": [str(reviewer.uuid)],
 70                "min_reviewers": 1,
 71            },
 72        )
 73        self.assertEqual(response.status_code, 201)
 74        self.assertIn(reviewer.uuid, response.data["reviewers"])
 75
 76    def test_create_rule_type_level(self):
 77        response = self.client.post(
 78            reverse("authentik_api:lifecyclerule-list"),
 79            {
 80                "name": generate_id(),
 81                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
 82                "object_id": None,
 83                "interval": "days=90",
 84                "grace_period": "days=30",
 85                "reviewer_groups": [str(self.reviewer_group.pk)],
 86                "reviewers": [],
 87                "min_reviewers": 1,
 88            },
 89        )
 90        self.assertEqual(response.status_code, 201)
 91        self.assertIsNone(response.data["object_id"])
 92
 93    def test_create_rule_fails_without_reviewers(self):
 94        response = self.client.post(
 95            reverse("authentik_api:lifecyclerule-list"),
 96            {
 97                "name": generate_id(),
 98                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
 99                "object_id": str(self.app.pk),
100                "interval": "days=30",
101                "grace_period": "days=10",
102                "reviewer_groups": [],
103                "reviewers": [],
104                "min_reviewers": 1,
105            },
106        )
107        self.assertEqual(response.status_code, 400)
108
109    def test_create_rule_fails_grace_period_longer_than_interval(self):
110        response = self.client.post(
111            reverse("authentik_api:lifecyclerule-list"),
112            {
113                "name": generate_id(),
114                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
115                "object_id": str(self.app.pk),
116                "interval": "days=10",
117                "grace_period": "days=30",
118                "reviewer_groups": [str(self.reviewer_group.pk)],
119                "reviewers": [],
120                "min_reviewers": 1,
121            },
122        )
123        self.assertEqual(response.status_code, 400)
124        self.assertIn("grace_period", response.data)
125
126    def test_create_rule_fails_invalid_object_id(self):
127        response = self.client.post(
128            reverse("authentik_api:lifecyclerule-list"),
129            {
130                "name": generate_id(),
131                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
132                "object_id": "00000000-0000-0000-0000-000000000000",
133                "interval": "days=30",
134                "grace_period": "days=10",
135                "reviewer_groups": [str(self.reviewer_group.pk)],
136                "reviewers": [],
137                "min_reviewers": 1,
138            },
139        )
140        self.assertEqual(response.status_code, 400)
141        self.assertIn("object_id", response.data)
142
143    def test_retrieve_rule(self):
144        rule = LifecycleRule.objects.create(
145            name=generate_id(),
146            content_type=self.content_type,
147            object_id=str(self.app.pk),
148        )
149        rule.reviewer_groups.add(self.reviewer_group)
150
151        response = self.client.get(
152            reverse("authentik_api:lifecyclerule-detail", kwargs={"pk": rule.pk})
153        )
154        self.assertEqual(response.status_code, 200)
155        self.assertEqual(response.data["id"], str(rule.pk))
156
157    def test_update_rule(self):
158        rule = LifecycleRule.objects.create(
159            name=generate_id(),
160            content_type=self.content_type,
161            object_id=str(self.app.pk),
162            interval="days=30",
163        )
164        rule.reviewer_groups.add(self.reviewer_group)
165
166        response = self.client.patch(
167            reverse("authentik_api:lifecyclerule-detail", kwargs={"pk": rule.pk}),
168            {"interval": "days=60"},
169        )
170        self.assertEqual(response.status_code, 200)
171        self.assertEqual(response.data["interval"], "days=60")
172
173    def test_delete_rule(self):
174        rule = LifecycleRule.objects.create(
175            name=generate_id(),
176            content_type=self.content_type,
177            object_id=str(self.app.pk),
178        )
179        rule.reviewer_groups.add(self.reviewer_group)
180
181        response = self.client.delete(
182            reverse("authentik_api:lifecyclerule-detail", kwargs={"pk": rule.pk})
183        )
184        self.assertEqual(response.status_code, 204)
185        self.assertFalse(LifecycleRule.objects.filter(pk=rule.pk).exists())
186
187
188@patch_license
189class TestIterationAPI(APITestCase):
190
191    def setUp(self):
192        self.user = create_test_admin_user()
193        self.client.force_login(self.user)
194        self.app = Application.objects.create(name=generate_id(), slug=generate_id())
195        self.content_type = ContentType.objects.get_for_model(Application)
196        self.reviewer_group = Group.objects.create(name=generate_id())
197        self.reviewer_group.users.add(self.user)
198
199    @classmethod
200    def setUpTestData(cls):
201        config = apps.get_app_config("authentik_tasks_schedules")
202        config._on_startup_callback(None)
203
204    def test_open_iterations(self):
205        rule = LifecycleRule.objects.create(
206            name=generate_id(),
207            content_type=self.content_type,
208            object_id=str(self.app.pk),
209        )
210        rule.reviewer_groups.add(self.reviewer_group)
211
212        response = self.client.get(reverse("authentik_api:lifecycleiteration-open-iterations"))
213        self.assertEqual(response.status_code, 200)
214        self.assertGreaterEqual(len(response.data["results"]), 1)
215
216        for iteration in response.data["results"]:
217            self.assertEqual(iteration["state"], ReviewState.PENDING)
218
219    def test_open_iterations_filter_user_is_reviewer(self):
220        rule = LifecycleRule.objects.create(
221            name=generate_id(),
222            content_type=self.content_type,
223            object_id=str(self.app.pk),
224        )
225        rule.reviewer_groups.add(self.reviewer_group)
226
227        response = self.client.get(
228            reverse("authentik_api:lifecycleiteration-open-iterations"),
229            {"user_is_reviewer": "true"},
230        )
231        self.assertEqual(response.status_code, 200)
232        # User is in reviewer_group, so should see the iteration
233        self.assertGreaterEqual(len(response.data["results"]), 1)
234
235    def test_latest_iteration(self):
236        rule = LifecycleRule.objects.create(
237            name=generate_id(),
238            content_type=self.content_type,
239            object_id=str(self.app.pk),
240        )
241        rule.reviewer_groups.add(self.reviewer_group)
242
243        response = self.client.get(
244            reverse(
245                "authentik_api:lifecycleiteration-latest-iterations",
246                kwargs={
247                    "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
248                    "object_id": str(self.app.pk),
249                },
250            )
251        )
252        self.assertEqual(response.status_code, 200)
253        self.assertEqual(len(response.data), 1)
254        self.assertEqual(response.data[0]["object_id"], str(self.app.pk))
255
256    def test_latest_iteration_not_found(self):
257        response = self.client.get(
258            reverse(
259                "authentik_api:lifecycleiteration-latest-iterations",
260                kwargs={
261                    "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
262                    "object_id": "00000000-0000-0000-0000-000000000000",
263                },
264            )
265        )
266        self.assertEqual(response.data, [])
267
268    def test_iteration_includes_user_can_review(self):
269        rule = LifecycleRule.objects.create(
270            name=generate_id(),
271            content_type=self.content_type,
272            object_id=str(self.app.pk),
273        )
274        rule.reviewer_groups.add(self.reviewer_group)
275
276        response = self.client.get(reverse("authentik_api:lifecycleiteration-open-iterations"))
277        self.assertEqual(response.status_code, 200)
278        self.assertGreaterEqual(len(response.data["results"]), 1)
279        # user_can_review should be present
280        self.assertIn("user_can_review", response.data["results"][0])
281
282
283@patch_license
284class TestReviewAPI(APITestCase):
285
286    def setUp(self):
287        self.user = create_test_admin_user()
288        self.client.force_login(self.user)
289        self.app = Application.objects.create(name=generate_id(), slug=generate_id())
290        self.content_type = ContentType.objects.get_for_model(Application)
291        self.reviewer_group = Group.objects.create(name=generate_id())
292        self.reviewer_group.users.add(self.user)
293
294    @classmethod
295    def setUpTestData(cls):
296        config = apps.get_app_config("authentik_tasks_schedules")
297        config._on_startup_callback(None)
298
299    def test_create_review(self):
300        rule = LifecycleRule.objects.create(
301            name=generate_id(),
302            content_type=self.content_type,
303            object_id=str(self.app.pk),
304            min_reviewers=1,
305        )
306        rule.reviewer_groups.add(self.reviewer_group)
307
308        # Get the auto-created iteration
309        iteration = LifecycleIteration.objects.get(
310            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
311        )
312
313        response = self.client.post(
314            reverse("authentik_api:review-list"),
315            {
316                "iteration": str(iteration.pk),
317                "note": "Reviewed and approved",
318            },
319        )
320        self.assertEqual(response.status_code, 201)
321        self.assertEqual(response.data["iteration"], iteration.pk)
322        self.assertEqual(response.data["note"], "Reviewed and approved")
323        self.assertEqual(response.data["reviewer"]["pk"], self.user.pk)
324
325    def test_create_review_completes_iteration(self):
326        rule = LifecycleRule.objects.create(
327            name=generate_id(),
328            content_type=self.content_type,
329            object_id=str(self.app.pk),
330            min_reviewers=1,
331        )
332        rule.reviewer_groups.add(self.reviewer_group)
333
334        iteration = LifecycleIteration.objects.get(
335            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
336        )
337        self.assertEqual(iteration.state, ReviewState.PENDING)
338
339        response = self.client.post(
340            reverse("authentik_api:review-list"),
341            {
342                "iteration": str(iteration.pk),
343            },
344        )
345        self.assertEqual(response.status_code, 201)
346
347        iteration.refresh_from_db()
348        self.assertEqual(iteration.state, ReviewState.REVIEWED)
349
350    def test_create_review_sets_reviewer_from_request(self):
351        rule = LifecycleRule.objects.create(
352            name=generate_id(),
353            content_type=self.content_type,
354            object_id=str(self.app.pk),
355            min_reviewers=1,
356        )
357        rule.reviewer_groups.add(self.reviewer_group)
358
359        iteration = LifecycleIteration.objects.get(
360            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
361        )
362
363        response = self.client.post(
364            reverse("authentik_api:review-list"),
365            {
366                "iteration": str(iteration.pk),
367            },
368        )
369        self.assertEqual(response.status_code, 201)
370        # Reviewer should be the logged-in user
371        self.assertEqual(response.data["reviewer"]["pk"], self.user.pk)
372
373    def test_non_reviewer_cannot_review(self):
374        other_group = Group.objects.create(name=generate_id())
375        other_user = create_test_user()
376        other_group.users.add(other_user)
377
378        rule = LifecycleRule.objects.create(
379            name=generate_id(),
380            content_type=self.content_type,
381            object_id=str(self.app.pk),
382            min_reviewers=1,
383        )
384        rule.reviewer_groups.add(other_group)
385
386        iteration = LifecycleIteration.objects.get(
387            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
388        )
389
390        # Current user is not in the reviewer group
391        self.assertFalse(iteration.user_can_review(self.user))
392
393    def test_non_reviewer_review_via_api_rejected(self):
394        other_group = Group.objects.create(name=generate_id())
395        other_user = create_test_user()
396        other_group.users.add(other_user)
397
398        rule = LifecycleRule.objects.create(
399            name=generate_id(),
400            content_type=self.content_type,
401            object_id=str(self.app.pk),
402            min_reviewers=1,
403        )
404        rule.reviewer_groups.add(other_group)
405
406        iteration = LifecycleIteration.objects.get(
407            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
408        )
409
410        # Current user (self.user) is NOT in the reviewer group
411        response = self.client.post(
412            reverse("authentik_api:review-list"),
413            {"iteration": str(iteration.pk)},
414        )
415        self.assertEqual(response.status_code, 400)
416
417    def test_duplicate_review_via_api_rejected(self):
418        rule = LifecycleRule.objects.create(
419            name=generate_id(),
420            content_type=self.content_type,
421            object_id=str(self.app.pk),
422            min_reviewers=2,
423        )
424        rule.reviewer_groups.add(self.reviewer_group)
425
426        iteration = LifecycleIteration.objects.get(
427            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
428        )
429
430        # First review should succeed
431        response = self.client.post(
432            reverse("authentik_api:review-list"),
433            {"iteration": str(iteration.pk)},
434        )
435        self.assertEqual(response.status_code, 201)
436
437        # Second review by same user should be rejected
438        response = self.client.post(
439            reverse("authentik_api:review-list"),
440            {"iteration": str(iteration.pk)},
441        )
442        self.assertEqual(response.status_code, 400)
@patch_license
class TestLifecycleRuleAPI(rest_framework.test.APITestCase):
 14@patch_license
 15class TestLifecycleRuleAPI(APITestCase):
 16
 17    def setUp(self):
 18        self.user = create_test_admin_user()
 19        self.client.force_login(self.user)
 20        self.app = Application.objects.create(name=generate_id(), slug=generate_id())
 21        self.content_type = ContentType.objects.get_for_model(Application)
 22        self.reviewer_group = Group.objects.create(name=generate_id())
 23
 24    @classmethod
 25    def setUpTestData(cls):
 26        config = apps.get_app_config("authentik_tasks_schedules")
 27        config._on_startup_callback(None)
 28
 29    def test_list_rules(self):
 30        rule = LifecycleRule.objects.create(
 31            name=generate_id(),
 32            content_type=self.content_type,
 33            object_id=str(self.app.pk),
 34        )
 35        rule.reviewer_groups.add(self.reviewer_group)
 36
 37        response = self.client.get(reverse("authentik_api:lifecyclerule-list"))
 38        self.assertEqual(response.status_code, 200)
 39        self.assertGreaterEqual(len(response.data["results"]), 1)
 40
 41    def test_create_rule_with_reviewer_group(self):
 42        response = self.client.post(
 43            reverse("authentik_api:lifecyclerule-list"),
 44            {
 45                "name": generate_id(),
 46                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
 47                "object_id": str(self.app.pk),
 48                "interval": "days=30",
 49                "grace_period": "days=10",
 50                "reviewer_groups": [str(self.reviewer_group.pk)],
 51                "reviewers": [],
 52                "min_reviewers": 1,
 53            },
 54        )
 55        self.assertEqual(response.status_code, 201)
 56        self.assertEqual(response.data["object_id"], str(self.app.pk))
 57        self.assertEqual(response.data["interval"], "days=30")
 58
 59    def test_create_rule_with_explicit_reviewer(self):
 60        reviewer = create_test_user()
 61        response = self.client.post(
 62            reverse("authentik_api:lifecyclerule-list"),
 63            {
 64                "name": generate_id(),
 65                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
 66                "object_id": str(self.app.pk),
 67                "interval": "days=60",
 68                "grace_period": "days=15",
 69                "reviewer_groups": [],
 70                "reviewers": [str(reviewer.uuid)],
 71                "min_reviewers": 1,
 72            },
 73        )
 74        self.assertEqual(response.status_code, 201)
 75        self.assertIn(reviewer.uuid, response.data["reviewers"])
 76
 77    def test_create_rule_type_level(self):
 78        response = self.client.post(
 79            reverse("authentik_api:lifecyclerule-list"),
 80            {
 81                "name": generate_id(),
 82                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
 83                "object_id": None,
 84                "interval": "days=90",
 85                "grace_period": "days=30",
 86                "reviewer_groups": [str(self.reviewer_group.pk)],
 87                "reviewers": [],
 88                "min_reviewers": 1,
 89            },
 90        )
 91        self.assertEqual(response.status_code, 201)
 92        self.assertIsNone(response.data["object_id"])
 93
 94    def test_create_rule_fails_without_reviewers(self):
 95        response = self.client.post(
 96            reverse("authentik_api:lifecyclerule-list"),
 97            {
 98                "name": generate_id(),
 99                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
100                "object_id": str(self.app.pk),
101                "interval": "days=30",
102                "grace_period": "days=10",
103                "reviewer_groups": [],
104                "reviewers": [],
105                "min_reviewers": 1,
106            },
107        )
108        self.assertEqual(response.status_code, 400)
109
110    def test_create_rule_fails_grace_period_longer_than_interval(self):
111        response = self.client.post(
112            reverse("authentik_api:lifecyclerule-list"),
113            {
114                "name": generate_id(),
115                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
116                "object_id": str(self.app.pk),
117                "interval": "days=10",
118                "grace_period": "days=30",
119                "reviewer_groups": [str(self.reviewer_group.pk)],
120                "reviewers": [],
121                "min_reviewers": 1,
122            },
123        )
124        self.assertEqual(response.status_code, 400)
125        self.assertIn("grace_period", response.data)
126
127    def test_create_rule_fails_invalid_object_id(self):
128        response = self.client.post(
129            reverse("authentik_api:lifecyclerule-list"),
130            {
131                "name": generate_id(),
132                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
133                "object_id": "00000000-0000-0000-0000-000000000000",
134                "interval": "days=30",
135                "grace_period": "days=10",
136                "reviewer_groups": [str(self.reviewer_group.pk)],
137                "reviewers": [],
138                "min_reviewers": 1,
139            },
140        )
141        self.assertEqual(response.status_code, 400)
142        self.assertIn("object_id", response.data)
143
144    def test_retrieve_rule(self):
145        rule = LifecycleRule.objects.create(
146            name=generate_id(),
147            content_type=self.content_type,
148            object_id=str(self.app.pk),
149        )
150        rule.reviewer_groups.add(self.reviewer_group)
151
152        response = self.client.get(
153            reverse("authentik_api:lifecyclerule-detail", kwargs={"pk": rule.pk})
154        )
155        self.assertEqual(response.status_code, 200)
156        self.assertEqual(response.data["id"], str(rule.pk))
157
158    def test_update_rule(self):
159        rule = LifecycleRule.objects.create(
160            name=generate_id(),
161            content_type=self.content_type,
162            object_id=str(self.app.pk),
163            interval="days=30",
164        )
165        rule.reviewer_groups.add(self.reviewer_group)
166
167        response = self.client.patch(
168            reverse("authentik_api:lifecyclerule-detail", kwargs={"pk": rule.pk}),
169            {"interval": "days=60"},
170        )
171        self.assertEqual(response.status_code, 200)
172        self.assertEqual(response.data["interval"], "days=60")
173
174    def test_delete_rule(self):
175        rule = LifecycleRule.objects.create(
176            name=generate_id(),
177            content_type=self.content_type,
178            object_id=str(self.app.pk),
179        )
180        rule.reviewer_groups.add(self.reviewer_group)
181
182        response = self.client.delete(
183            reverse("authentik_api:lifecyclerule-detail", kwargs={"pk": rule.pk})
184        )
185        self.assertEqual(response.status_code, 204)
186        self.assertFalse(LifecycleRule.objects.filter(pk=rule.pk).exists())

Similar to TransactionTestCase, but use transaction.atomic() to achieve test isolation.

In most situations, TestCase should be preferred to TransactionTestCase as it allows faster execution. However, there are some situations where using TransactionTestCase might be necessary (e.g. testing some transactional behavior).

On database backends with no transaction support, TestCase behaves as TransactionTestCase.

def setUp(self):
17    def setUp(self):
18        self.user = create_test_admin_user()
19        self.client.force_login(self.user)
20        self.app = Application.objects.create(name=generate_id(), slug=generate_id())
21        self.content_type = ContentType.objects.get_for_model(Application)
22        self.reviewer_group = Group.objects.create(name=generate_id())

Hook method for setting up the test fixture before exercising it.

@classmethod
def setUpTestData(cls):
24    @classmethod
25    def setUpTestData(cls):
26        config = apps.get_app_config("authentik_tasks_schedules")
27        config._on_startup_callback(None)

Load initial data for the TestCase.

def test_list_rules(self):
29    def test_list_rules(self):
30        rule = LifecycleRule.objects.create(
31            name=generate_id(),
32            content_type=self.content_type,
33            object_id=str(self.app.pk),
34        )
35        rule.reviewer_groups.add(self.reviewer_group)
36
37        response = self.client.get(reverse("authentik_api:lifecyclerule-list"))
38        self.assertEqual(response.status_code, 200)
39        self.assertGreaterEqual(len(response.data["results"]), 1)
def test_create_rule_with_reviewer_group(self):
41    def test_create_rule_with_reviewer_group(self):
42        response = self.client.post(
43            reverse("authentik_api:lifecyclerule-list"),
44            {
45                "name": generate_id(),
46                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
47                "object_id": str(self.app.pk),
48                "interval": "days=30",
49                "grace_period": "days=10",
50                "reviewer_groups": [str(self.reviewer_group.pk)],
51                "reviewers": [],
52                "min_reviewers": 1,
53            },
54        )
55        self.assertEqual(response.status_code, 201)
56        self.assertEqual(response.data["object_id"], str(self.app.pk))
57        self.assertEqual(response.data["interval"], "days=30")
def test_create_rule_with_explicit_reviewer(self):
59    def test_create_rule_with_explicit_reviewer(self):
60        reviewer = create_test_user()
61        response = self.client.post(
62            reverse("authentik_api:lifecyclerule-list"),
63            {
64                "name": generate_id(),
65                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
66                "object_id": str(self.app.pk),
67                "interval": "days=60",
68                "grace_period": "days=15",
69                "reviewer_groups": [],
70                "reviewers": [str(reviewer.uuid)],
71                "min_reviewers": 1,
72            },
73        )
74        self.assertEqual(response.status_code, 201)
75        self.assertIn(reviewer.uuid, response.data["reviewers"])
def test_create_rule_type_level(self):
77    def test_create_rule_type_level(self):
78        response = self.client.post(
79            reverse("authentik_api:lifecyclerule-list"),
80            {
81                "name": generate_id(),
82                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
83                "object_id": None,
84                "interval": "days=90",
85                "grace_period": "days=30",
86                "reviewer_groups": [str(self.reviewer_group.pk)],
87                "reviewers": [],
88                "min_reviewers": 1,
89            },
90        )
91        self.assertEqual(response.status_code, 201)
92        self.assertIsNone(response.data["object_id"])
def test_create_rule_fails_without_reviewers(self):
 94    def test_create_rule_fails_without_reviewers(self):
 95        response = self.client.post(
 96            reverse("authentik_api:lifecyclerule-list"),
 97            {
 98                "name": generate_id(),
 99                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
100                "object_id": str(self.app.pk),
101                "interval": "days=30",
102                "grace_period": "days=10",
103                "reviewer_groups": [],
104                "reviewers": [],
105                "min_reviewers": 1,
106            },
107        )
108        self.assertEqual(response.status_code, 400)
def test_create_rule_fails_grace_period_longer_than_interval(self):
110    def test_create_rule_fails_grace_period_longer_than_interval(self):
111        response = self.client.post(
112            reverse("authentik_api:lifecyclerule-list"),
113            {
114                "name": generate_id(),
115                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
116                "object_id": str(self.app.pk),
117                "interval": "days=10",
118                "grace_period": "days=30",
119                "reviewer_groups": [str(self.reviewer_group.pk)],
120                "reviewers": [],
121                "min_reviewers": 1,
122            },
123        )
124        self.assertEqual(response.status_code, 400)
125        self.assertIn("grace_period", response.data)
def test_create_rule_fails_invalid_object_id(self):
127    def test_create_rule_fails_invalid_object_id(self):
128        response = self.client.post(
129            reverse("authentik_api:lifecyclerule-list"),
130            {
131                "name": generate_id(),
132                "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
133                "object_id": "00000000-0000-0000-0000-000000000000",
134                "interval": "days=30",
135                "grace_period": "days=10",
136                "reviewer_groups": [str(self.reviewer_group.pk)],
137                "reviewers": [],
138                "min_reviewers": 1,
139            },
140        )
141        self.assertEqual(response.status_code, 400)
142        self.assertIn("object_id", response.data)
def test_retrieve_rule(self):
144    def test_retrieve_rule(self):
145        rule = LifecycleRule.objects.create(
146            name=generate_id(),
147            content_type=self.content_type,
148            object_id=str(self.app.pk),
149        )
150        rule.reviewer_groups.add(self.reviewer_group)
151
152        response = self.client.get(
153            reverse("authentik_api:lifecyclerule-detail", kwargs={"pk": rule.pk})
154        )
155        self.assertEqual(response.status_code, 200)
156        self.assertEqual(response.data["id"], str(rule.pk))
def test_update_rule(self):
158    def test_update_rule(self):
159        rule = LifecycleRule.objects.create(
160            name=generate_id(),
161            content_type=self.content_type,
162            object_id=str(self.app.pk),
163            interval="days=30",
164        )
165        rule.reviewer_groups.add(self.reviewer_group)
166
167        response = self.client.patch(
168            reverse("authentik_api:lifecyclerule-detail", kwargs={"pk": rule.pk}),
169            {"interval": "days=60"},
170        )
171        self.assertEqual(response.status_code, 200)
172        self.assertEqual(response.data["interval"], "days=60")
def test_delete_rule(self):
174    def test_delete_rule(self):
175        rule = LifecycleRule.objects.create(
176            name=generate_id(),
177            content_type=self.content_type,
178            object_id=str(self.app.pk),
179        )
180        rule.reviewer_groups.add(self.reviewer_group)
181
182        response = self.client.delete(
183            reverse("authentik_api:lifecyclerule-detail", kwargs={"pk": rule.pk})
184        )
185        self.assertEqual(response.status_code, 204)
186        self.assertFalse(LifecycleRule.objects.filter(pk=rule.pk).exists())
@patch_license
class TestIterationAPI(rest_framework.test.APITestCase):
189@patch_license
190class TestIterationAPI(APITestCase):
191
192    def setUp(self):
193        self.user = create_test_admin_user()
194        self.client.force_login(self.user)
195        self.app = Application.objects.create(name=generate_id(), slug=generate_id())
196        self.content_type = ContentType.objects.get_for_model(Application)
197        self.reviewer_group = Group.objects.create(name=generate_id())
198        self.reviewer_group.users.add(self.user)
199
200    @classmethod
201    def setUpTestData(cls):
202        config = apps.get_app_config("authentik_tasks_schedules")
203        config._on_startup_callback(None)
204
205    def test_open_iterations(self):
206        rule = LifecycleRule.objects.create(
207            name=generate_id(),
208            content_type=self.content_type,
209            object_id=str(self.app.pk),
210        )
211        rule.reviewer_groups.add(self.reviewer_group)
212
213        response = self.client.get(reverse("authentik_api:lifecycleiteration-open-iterations"))
214        self.assertEqual(response.status_code, 200)
215        self.assertGreaterEqual(len(response.data["results"]), 1)
216
217        for iteration in response.data["results"]:
218            self.assertEqual(iteration["state"], ReviewState.PENDING)
219
220    def test_open_iterations_filter_user_is_reviewer(self):
221        rule = LifecycleRule.objects.create(
222            name=generate_id(),
223            content_type=self.content_type,
224            object_id=str(self.app.pk),
225        )
226        rule.reviewer_groups.add(self.reviewer_group)
227
228        response = self.client.get(
229            reverse("authentik_api:lifecycleiteration-open-iterations"),
230            {"user_is_reviewer": "true"},
231        )
232        self.assertEqual(response.status_code, 200)
233        # User is in reviewer_group, so should see the iteration
234        self.assertGreaterEqual(len(response.data["results"]), 1)
235
236    def test_latest_iteration(self):
237        rule = LifecycleRule.objects.create(
238            name=generate_id(),
239            content_type=self.content_type,
240            object_id=str(self.app.pk),
241        )
242        rule.reviewer_groups.add(self.reviewer_group)
243
244        response = self.client.get(
245            reverse(
246                "authentik_api:lifecycleiteration-latest-iterations",
247                kwargs={
248                    "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
249                    "object_id": str(self.app.pk),
250                },
251            )
252        )
253        self.assertEqual(response.status_code, 200)
254        self.assertEqual(len(response.data), 1)
255        self.assertEqual(response.data[0]["object_id"], str(self.app.pk))
256
257    def test_latest_iteration_not_found(self):
258        response = self.client.get(
259            reverse(
260                "authentik_api:lifecycleiteration-latest-iterations",
261                kwargs={
262                    "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
263                    "object_id": "00000000-0000-0000-0000-000000000000",
264                },
265            )
266        )
267        self.assertEqual(response.data, [])
268
269    def test_iteration_includes_user_can_review(self):
270        rule = LifecycleRule.objects.create(
271            name=generate_id(),
272            content_type=self.content_type,
273            object_id=str(self.app.pk),
274        )
275        rule.reviewer_groups.add(self.reviewer_group)
276
277        response = self.client.get(reverse("authentik_api:lifecycleiteration-open-iterations"))
278        self.assertEqual(response.status_code, 200)
279        self.assertGreaterEqual(len(response.data["results"]), 1)
280        # user_can_review should be present
281        self.assertIn("user_can_review", response.data["results"][0])

Similar to TransactionTestCase, but use transaction.atomic() to achieve test isolation.

In most situations, TestCase should be preferred to TransactionTestCase as it allows faster execution. However, there are some situations where using TransactionTestCase might be necessary (e.g. testing some transactional behavior).

On database backends with no transaction support, TestCase behaves as TransactionTestCase.

def setUp(self):
192    def setUp(self):
193        self.user = create_test_admin_user()
194        self.client.force_login(self.user)
195        self.app = Application.objects.create(name=generate_id(), slug=generate_id())
196        self.content_type = ContentType.objects.get_for_model(Application)
197        self.reviewer_group = Group.objects.create(name=generate_id())
198        self.reviewer_group.users.add(self.user)

Hook method for setting up the test fixture before exercising it.

@classmethod
def setUpTestData(cls):
200    @classmethod
201    def setUpTestData(cls):
202        config = apps.get_app_config("authentik_tasks_schedules")
203        config._on_startup_callback(None)

Load initial data for the TestCase.

def test_open_iterations(self):
205    def test_open_iterations(self):
206        rule = LifecycleRule.objects.create(
207            name=generate_id(),
208            content_type=self.content_type,
209            object_id=str(self.app.pk),
210        )
211        rule.reviewer_groups.add(self.reviewer_group)
212
213        response = self.client.get(reverse("authentik_api:lifecycleiteration-open-iterations"))
214        self.assertEqual(response.status_code, 200)
215        self.assertGreaterEqual(len(response.data["results"]), 1)
216
217        for iteration in response.data["results"]:
218            self.assertEqual(iteration["state"], ReviewState.PENDING)
def test_open_iterations_filter_user_is_reviewer(self):
220    def test_open_iterations_filter_user_is_reviewer(self):
221        rule = LifecycleRule.objects.create(
222            name=generate_id(),
223            content_type=self.content_type,
224            object_id=str(self.app.pk),
225        )
226        rule.reviewer_groups.add(self.reviewer_group)
227
228        response = self.client.get(
229            reverse("authentik_api:lifecycleiteration-open-iterations"),
230            {"user_is_reviewer": "true"},
231        )
232        self.assertEqual(response.status_code, 200)
233        # User is in reviewer_group, so should see the iteration
234        self.assertGreaterEqual(len(response.data["results"]), 1)
def test_latest_iteration(self):
236    def test_latest_iteration(self):
237        rule = LifecycleRule.objects.create(
238            name=generate_id(),
239            content_type=self.content_type,
240            object_id=str(self.app.pk),
241        )
242        rule.reviewer_groups.add(self.reviewer_group)
243
244        response = self.client.get(
245            reverse(
246                "authentik_api:lifecycleiteration-latest-iterations",
247                kwargs={
248                    "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
249                    "object_id": str(self.app.pk),
250                },
251            )
252        )
253        self.assertEqual(response.status_code, 200)
254        self.assertEqual(len(response.data), 1)
255        self.assertEqual(response.data[0]["object_id"], str(self.app.pk))
def test_latest_iteration_not_found(self):
257    def test_latest_iteration_not_found(self):
258        response = self.client.get(
259            reverse(
260                "authentik_api:lifecycleiteration-latest-iterations",
261                kwargs={
262                    "content_type": f"{self.content_type.app_label}.{self.content_type.model}",
263                    "object_id": "00000000-0000-0000-0000-000000000000",
264                },
265            )
266        )
267        self.assertEqual(response.data, [])
def test_iteration_includes_user_can_review(self):
269    def test_iteration_includes_user_can_review(self):
270        rule = LifecycleRule.objects.create(
271            name=generate_id(),
272            content_type=self.content_type,
273            object_id=str(self.app.pk),
274        )
275        rule.reviewer_groups.add(self.reviewer_group)
276
277        response = self.client.get(reverse("authentik_api:lifecycleiteration-open-iterations"))
278        self.assertEqual(response.status_code, 200)
279        self.assertGreaterEqual(len(response.data["results"]), 1)
280        # user_can_review should be present
281        self.assertIn("user_can_review", response.data["results"][0])
@patch_license
class TestReviewAPI(rest_framework.test.APITestCase):
284@patch_license
285class TestReviewAPI(APITestCase):
286
287    def setUp(self):
288        self.user = create_test_admin_user()
289        self.client.force_login(self.user)
290        self.app = Application.objects.create(name=generate_id(), slug=generate_id())
291        self.content_type = ContentType.objects.get_for_model(Application)
292        self.reviewer_group = Group.objects.create(name=generate_id())
293        self.reviewer_group.users.add(self.user)
294
295    @classmethod
296    def setUpTestData(cls):
297        config = apps.get_app_config("authentik_tasks_schedules")
298        config._on_startup_callback(None)
299
300    def test_create_review(self):
301        rule = LifecycleRule.objects.create(
302            name=generate_id(),
303            content_type=self.content_type,
304            object_id=str(self.app.pk),
305            min_reviewers=1,
306        )
307        rule.reviewer_groups.add(self.reviewer_group)
308
309        # Get the auto-created iteration
310        iteration = LifecycleIteration.objects.get(
311            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
312        )
313
314        response = self.client.post(
315            reverse("authentik_api:review-list"),
316            {
317                "iteration": str(iteration.pk),
318                "note": "Reviewed and approved",
319            },
320        )
321        self.assertEqual(response.status_code, 201)
322        self.assertEqual(response.data["iteration"], iteration.pk)
323        self.assertEqual(response.data["note"], "Reviewed and approved")
324        self.assertEqual(response.data["reviewer"]["pk"], self.user.pk)
325
326    def test_create_review_completes_iteration(self):
327        rule = LifecycleRule.objects.create(
328            name=generate_id(),
329            content_type=self.content_type,
330            object_id=str(self.app.pk),
331            min_reviewers=1,
332        )
333        rule.reviewer_groups.add(self.reviewer_group)
334
335        iteration = LifecycleIteration.objects.get(
336            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
337        )
338        self.assertEqual(iteration.state, ReviewState.PENDING)
339
340        response = self.client.post(
341            reverse("authentik_api:review-list"),
342            {
343                "iteration": str(iteration.pk),
344            },
345        )
346        self.assertEqual(response.status_code, 201)
347
348        iteration.refresh_from_db()
349        self.assertEqual(iteration.state, ReviewState.REVIEWED)
350
351    def test_create_review_sets_reviewer_from_request(self):
352        rule = LifecycleRule.objects.create(
353            name=generate_id(),
354            content_type=self.content_type,
355            object_id=str(self.app.pk),
356            min_reviewers=1,
357        )
358        rule.reviewer_groups.add(self.reviewer_group)
359
360        iteration = LifecycleIteration.objects.get(
361            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
362        )
363
364        response = self.client.post(
365            reverse("authentik_api:review-list"),
366            {
367                "iteration": str(iteration.pk),
368            },
369        )
370        self.assertEqual(response.status_code, 201)
371        # Reviewer should be the logged-in user
372        self.assertEqual(response.data["reviewer"]["pk"], self.user.pk)
373
374    def test_non_reviewer_cannot_review(self):
375        other_group = Group.objects.create(name=generate_id())
376        other_user = create_test_user()
377        other_group.users.add(other_user)
378
379        rule = LifecycleRule.objects.create(
380            name=generate_id(),
381            content_type=self.content_type,
382            object_id=str(self.app.pk),
383            min_reviewers=1,
384        )
385        rule.reviewer_groups.add(other_group)
386
387        iteration = LifecycleIteration.objects.get(
388            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
389        )
390
391        # Current user is not in the reviewer group
392        self.assertFalse(iteration.user_can_review(self.user))
393
394    def test_non_reviewer_review_via_api_rejected(self):
395        other_group = Group.objects.create(name=generate_id())
396        other_user = create_test_user()
397        other_group.users.add(other_user)
398
399        rule = LifecycleRule.objects.create(
400            name=generate_id(),
401            content_type=self.content_type,
402            object_id=str(self.app.pk),
403            min_reviewers=1,
404        )
405        rule.reviewer_groups.add(other_group)
406
407        iteration = LifecycleIteration.objects.get(
408            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
409        )
410
411        # Current user (self.user) is NOT in the reviewer group
412        response = self.client.post(
413            reverse("authentik_api:review-list"),
414            {"iteration": str(iteration.pk)},
415        )
416        self.assertEqual(response.status_code, 400)
417
418    def test_duplicate_review_via_api_rejected(self):
419        rule = LifecycleRule.objects.create(
420            name=generate_id(),
421            content_type=self.content_type,
422            object_id=str(self.app.pk),
423            min_reviewers=2,
424        )
425        rule.reviewer_groups.add(self.reviewer_group)
426
427        iteration = LifecycleIteration.objects.get(
428            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
429        )
430
431        # First review should succeed
432        response = self.client.post(
433            reverse("authentik_api:review-list"),
434            {"iteration": str(iteration.pk)},
435        )
436        self.assertEqual(response.status_code, 201)
437
438        # Second review by same user should be rejected
439        response = self.client.post(
440            reverse("authentik_api:review-list"),
441            {"iteration": str(iteration.pk)},
442        )
443        self.assertEqual(response.status_code, 400)

Similar to TransactionTestCase, but use transaction.atomic() to achieve test isolation.

In most situations, TestCase should be preferred to TransactionTestCase as it allows faster execution. However, there are some situations where using TransactionTestCase might be necessary (e.g. testing some transactional behavior).

On database backends with no transaction support, TestCase behaves as TransactionTestCase.

def setUp(self):
287    def setUp(self):
288        self.user = create_test_admin_user()
289        self.client.force_login(self.user)
290        self.app = Application.objects.create(name=generate_id(), slug=generate_id())
291        self.content_type = ContentType.objects.get_for_model(Application)
292        self.reviewer_group = Group.objects.create(name=generate_id())
293        self.reviewer_group.users.add(self.user)

Hook method for setting up the test fixture before exercising it.

@classmethod
def setUpTestData(cls):
295    @classmethod
296    def setUpTestData(cls):
297        config = apps.get_app_config("authentik_tasks_schedules")
298        config._on_startup_callback(None)

Load initial data for the TestCase.

def test_create_review(self):
300    def test_create_review(self):
301        rule = LifecycleRule.objects.create(
302            name=generate_id(),
303            content_type=self.content_type,
304            object_id=str(self.app.pk),
305            min_reviewers=1,
306        )
307        rule.reviewer_groups.add(self.reviewer_group)
308
309        # Get the auto-created iteration
310        iteration = LifecycleIteration.objects.get(
311            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
312        )
313
314        response = self.client.post(
315            reverse("authentik_api:review-list"),
316            {
317                "iteration": str(iteration.pk),
318                "note": "Reviewed and approved",
319            },
320        )
321        self.assertEqual(response.status_code, 201)
322        self.assertEqual(response.data["iteration"], iteration.pk)
323        self.assertEqual(response.data["note"], "Reviewed and approved")
324        self.assertEqual(response.data["reviewer"]["pk"], self.user.pk)
def test_create_review_completes_iteration(self):
326    def test_create_review_completes_iteration(self):
327        rule = LifecycleRule.objects.create(
328            name=generate_id(),
329            content_type=self.content_type,
330            object_id=str(self.app.pk),
331            min_reviewers=1,
332        )
333        rule.reviewer_groups.add(self.reviewer_group)
334
335        iteration = LifecycleIteration.objects.get(
336            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
337        )
338        self.assertEqual(iteration.state, ReviewState.PENDING)
339
340        response = self.client.post(
341            reverse("authentik_api:review-list"),
342            {
343                "iteration": str(iteration.pk),
344            },
345        )
346        self.assertEqual(response.status_code, 201)
347
348        iteration.refresh_from_db()
349        self.assertEqual(iteration.state, ReviewState.REVIEWED)
def test_create_review_sets_reviewer_from_request(self):
351    def test_create_review_sets_reviewer_from_request(self):
352        rule = LifecycleRule.objects.create(
353            name=generate_id(),
354            content_type=self.content_type,
355            object_id=str(self.app.pk),
356            min_reviewers=1,
357        )
358        rule.reviewer_groups.add(self.reviewer_group)
359
360        iteration = LifecycleIteration.objects.get(
361            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
362        )
363
364        response = self.client.post(
365            reverse("authentik_api:review-list"),
366            {
367                "iteration": str(iteration.pk),
368            },
369        )
370        self.assertEqual(response.status_code, 201)
371        # Reviewer should be the logged-in user
372        self.assertEqual(response.data["reviewer"]["pk"], self.user.pk)
def test_non_reviewer_cannot_review(self):
374    def test_non_reviewer_cannot_review(self):
375        other_group = Group.objects.create(name=generate_id())
376        other_user = create_test_user()
377        other_group.users.add(other_user)
378
379        rule = LifecycleRule.objects.create(
380            name=generate_id(),
381            content_type=self.content_type,
382            object_id=str(self.app.pk),
383            min_reviewers=1,
384        )
385        rule.reviewer_groups.add(other_group)
386
387        iteration = LifecycleIteration.objects.get(
388            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
389        )
390
391        # Current user is not in the reviewer group
392        self.assertFalse(iteration.user_can_review(self.user))
def test_non_reviewer_review_via_api_rejected(self):
394    def test_non_reviewer_review_via_api_rejected(self):
395        other_group = Group.objects.create(name=generate_id())
396        other_user = create_test_user()
397        other_group.users.add(other_user)
398
399        rule = LifecycleRule.objects.create(
400            name=generate_id(),
401            content_type=self.content_type,
402            object_id=str(self.app.pk),
403            min_reviewers=1,
404        )
405        rule.reviewer_groups.add(other_group)
406
407        iteration = LifecycleIteration.objects.get(
408            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
409        )
410
411        # Current user (self.user) is NOT in the reviewer group
412        response = self.client.post(
413            reverse("authentik_api:review-list"),
414            {"iteration": str(iteration.pk)},
415        )
416        self.assertEqual(response.status_code, 400)
def test_duplicate_review_via_api_rejected(self):
418    def test_duplicate_review_via_api_rejected(self):
419        rule = LifecycleRule.objects.create(
420            name=generate_id(),
421            content_type=self.content_type,
422            object_id=str(self.app.pk),
423            min_reviewers=2,
424        )
425        rule.reviewer_groups.add(self.reviewer_group)
426
427        iteration = LifecycleIteration.objects.get(
428            content_type=self.content_type, object_id=str(self.app.pk), rule=rule
429        )
430
431        # First review should succeed
432        response = self.client.post(
433            reverse("authentik_api:review-list"),
434            {"iteration": str(iteration.pk)},
435        )
436        self.assertEqual(response.status_code, 201)
437
438        # Second review by same user should be rejected
439        response = self.client.post(
440            reverse("authentik_api:review-list"),
441            {"iteration": str(iteration.pk)},
442        )
443        self.assertEqual(response.status_code, 400)