authentik.enterprise.lifecycle.tests.test_api

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

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

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

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

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

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

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