arthexis 0.1.9__py3-none-any.whl → 0.1.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arthexis might be problematic. Click here for more details.

pages/tests.py CHANGED
@@ -1,5 +1,14 @@
1
- from django.test import Client, TestCase, override_settings
1
+ import os
2
+
3
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
4
+
5
+ import django
6
+
7
+ django.setup()
8
+
9
+ from django.test import Client, RequestFactory, TestCase, override_settings
2
10
  from django.urls import reverse
11
+ from django.templatetags.static import static
3
12
  from urllib.parse import quote
4
13
  from django.contrib.auth import get_user_model
5
14
  from django.contrib.sites.models import Site
@@ -16,7 +25,15 @@ from pages.screenshot_specs import (
16
25
  )
17
26
  from django.apps import apps as django_apps
18
27
  from core import mailer
19
- from core.models import AdminHistory, InviteLead, Package, ReleaseManager, Todo
28
+ from core.admin import ProfileAdminMixin
29
+ from core.models import (
30
+ AdminHistory,
31
+ InviteLead,
32
+ Package,
33
+ Reference,
34
+ ReleaseManager,
35
+ Todo,
36
+ )
20
37
  from django.core.files.uploadedfile import SimpleUploadedFile
21
38
  import base64
22
39
  import tempfile
@@ -33,6 +50,11 @@ from datetime import date, timedelta
33
50
  from django.core import mail
34
51
  from django.utils import timezone
35
52
  from django.utils.text import slugify
53
+ from django_otp import DEVICE_ID_SESSION_KEY
54
+ from django_otp.oath import TOTP
55
+ from django_otp.plugins.otp_totp.models import TOTPDevice
56
+ from core.backends import TOTP_DEVICE_NAME
57
+ import time
36
58
 
37
59
  from nodes.models import (
38
60
  EmailOutbox,
@@ -58,6 +80,10 @@ class LoginViewTests(TestCase):
58
80
  resp = self.client.get(reverse("pages:index"))
59
81
  self.assertContains(resp, 'href="/login/"')
60
82
 
83
+ def test_login_page_shows_authenticator_toggle(self):
84
+ resp = self.client.get(reverse("pages:login"))
85
+ self.assertContains(resp, "Use Authenticator app")
86
+
61
87
  def test_staff_login_redirects_admin(self):
62
88
  resp = self.client.post(
63
89
  reverse("pages:login"),
@@ -65,6 +91,49 @@ class LoginViewTests(TestCase):
65
91
  )
66
92
  self.assertRedirects(resp, reverse("admin:index"))
67
93
 
94
+ def test_login_with_authenticator_code(self):
95
+ device = TOTPDevice.objects.create(
96
+ user=self.staff,
97
+ name=TOTP_DEVICE_NAME,
98
+ confirmed=True,
99
+ )
100
+ totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift)
101
+ totp.time = time.time()
102
+ token = f"{totp.token():0{device.digits}d}"
103
+
104
+ resp = self.client.post(
105
+ reverse("pages:login"),
106
+ {
107
+ "username": "staff",
108
+ "auth_method": "otp",
109
+ "otp_token": token,
110
+ },
111
+ )
112
+
113
+ self.assertRedirects(resp, reverse("admin:index"))
114
+ session = self.client.session
115
+ self.assertIn(DEVICE_ID_SESSION_KEY, session)
116
+ self.assertEqual(session[DEVICE_ID_SESSION_KEY], device.persistent_id)
117
+
118
+ def test_login_with_invalid_authenticator_code(self):
119
+ TOTPDevice.objects.create(
120
+ user=self.staff,
121
+ name=TOTP_DEVICE_NAME,
122
+ confirmed=True,
123
+ )
124
+
125
+ resp = self.client.post(
126
+ reverse("pages:login"),
127
+ {
128
+ "username": "staff",
129
+ "auth_method": "otp",
130
+ "otp_token": "000000",
131
+ },
132
+ )
133
+
134
+ self.assertEqual(resp.status_code, 200)
135
+ self.assertContains(resp, "authenticator code is invalid", status_code=200)
136
+
68
137
  def test_already_logged_in_staff_redirects(self):
69
138
  self.client.force_login(self.staff)
70
139
  resp = self.client.get(reverse("pages:login"))
@@ -84,6 +153,8 @@ class LoginViewTests(TestCase):
84
153
  )
85
154
  self.assertRedirects(resp, "/nodes/list/")
86
155
 
156
+
157
+
87
158
  @override_settings(EMAIL_BACKEND="django.core.mail.backends.dummy.EmailBackend")
88
159
  def test_login_page_hides_request_link_without_email_backend(self):
89
160
  resp = self.client.get(reverse("pages:login"))
@@ -97,6 +168,173 @@ class LoginViewTests(TestCase):
97
168
  self.assertTrue(resp.context["can_request_invite"])
98
169
  self.assertContains(resp, reverse("pages:request-invite"))
99
170
 
171
+ @override_settings(ALLOWED_HOSTS=["gway-qk32000"])
172
+ def test_login_allows_forwarded_https_origin(self):
173
+ secure_client = Client(enforce_csrf_checks=True)
174
+ login_url = reverse("pages:login")
175
+ response = secure_client.get(login_url, HTTP_HOST="gway-qk32000")
176
+ csrf_cookie = response.cookies["csrftoken"].value
177
+ submit = secure_client.post(
178
+ login_url,
179
+ {
180
+ "username": "staff",
181
+ "password": "pwd",
182
+ "csrfmiddlewaretoken": csrf_cookie,
183
+ },
184
+ HTTP_HOST="gway-qk32000",
185
+ HTTP_ORIGIN="https://gway-qk32000",
186
+ HTTP_X_FORWARDED_PROTO="https",
187
+ HTTP_REFERER="https://gway-qk32000/login/",
188
+ )
189
+ self.assertRedirects(submit, reverse("admin:index"))
190
+
191
+ @override_settings(ALLOWED_HOSTS=["10.42.0.0/16", "gway-qk32000"])
192
+ def test_login_allows_forwarded_origin_with_private_host_header(self):
193
+ secure_client = Client(enforce_csrf_checks=True)
194
+ login_url = reverse("pages:login")
195
+ response = secure_client.get(login_url, HTTP_HOST="10.42.0.2")
196
+ csrf_cookie = response.cookies["csrftoken"].value
197
+ submit = secure_client.post(
198
+ login_url,
199
+ {
200
+ "username": "staff",
201
+ "password": "pwd",
202
+ "csrfmiddlewaretoken": csrf_cookie,
203
+ },
204
+ HTTP_HOST="10.42.0.2",
205
+ HTTP_ORIGIN="https://gway-qk32000",
206
+ HTTP_X_FORWARDED_PROTO="https",
207
+ HTTP_X_FORWARDED_HOST="gway-qk32000",
208
+ HTTP_REFERER="https://gway-qk32000/login/",
209
+ )
210
+ self.assertRedirects(submit, reverse("admin:index"))
211
+
212
+ @override_settings(ALLOWED_HOSTS=["10.42.0.0/16", "gway-qk32000"])
213
+ def test_login_allows_forwarded_header_host_and_proto(self):
214
+ secure_client = Client(enforce_csrf_checks=True)
215
+ login_url = reverse("pages:login")
216
+ response = secure_client.get(login_url, HTTP_HOST="10.42.0.2")
217
+ csrf_cookie = response.cookies["csrftoken"].value
218
+ submit = secure_client.post(
219
+ login_url,
220
+ {
221
+ "username": "staff",
222
+ "password": "pwd",
223
+ "csrfmiddlewaretoken": csrf_cookie,
224
+ },
225
+ HTTP_HOST="10.42.0.2",
226
+ HTTP_ORIGIN="https://gway-qk32000",
227
+ HTTP_FORWARDED="proto=https;host=gway-qk32000",
228
+ HTTP_REFERER="https://gway-qk32000/login/",
229
+ )
230
+ self.assertRedirects(submit, reverse("admin:index"))
231
+
232
+ @override_settings(ALLOWED_HOSTS=["10.42.0.0/16", "gway-qk32000"])
233
+ def test_login_allows_forwarded_referer_without_origin(self):
234
+ secure_client = Client(enforce_csrf_checks=True)
235
+ login_url = reverse("pages:login")
236
+ response = secure_client.get(login_url, HTTP_HOST="10.42.0.2")
237
+ csrf_cookie = response.cookies["csrftoken"].value
238
+ submit = secure_client.post(
239
+ login_url,
240
+ {
241
+ "username": "staff",
242
+ "password": "pwd",
243
+ "csrfmiddlewaretoken": csrf_cookie,
244
+ },
245
+ HTTP_HOST="10.42.0.2",
246
+ HTTP_X_FORWARDED_PROTO="https",
247
+ HTTP_X_FORWARDED_HOST="gway-qk32000",
248
+ HTTP_REFERER="https://gway-qk32000/login/",
249
+ )
250
+ self.assertRedirects(submit, reverse("admin:index"))
251
+
252
+ @override_settings(ALLOWED_HOSTS=["gway-qk32000"])
253
+ def test_login_allows_forwarded_origin_with_explicit_port(self):
254
+ secure_client = Client(enforce_csrf_checks=True)
255
+ login_url = reverse("pages:login")
256
+ response = secure_client.get(login_url, HTTP_HOST="gway-qk32000")
257
+ csrf_cookie = response.cookies["csrftoken"].value
258
+ submit = secure_client.post(
259
+ login_url,
260
+ {
261
+ "username": "staff",
262
+ "password": "pwd",
263
+ "csrfmiddlewaretoken": csrf_cookie,
264
+ },
265
+ HTTP_HOST="gway-qk32000",
266
+ HTTP_ORIGIN="https://gway-qk32000:4443",
267
+ HTTP_X_FORWARDED_PROTO="https",
268
+ HTTP_X_FORWARDED_HOST="gway-qk32000:4443",
269
+ HTTP_REFERER="https://gway-qk32000:4443/login/",
270
+ )
271
+ self.assertRedirects(submit, reverse("admin:index"))
272
+
273
+
274
+ class AuthenticatorSetupTests(TestCase):
275
+ def setUp(self):
276
+ self.client = Client()
277
+ User = get_user_model()
278
+ self.staff = User.objects.create_user(
279
+ username="staffer", password="pwd", is_staff=True
280
+ )
281
+ Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
282
+ self.client.force_login(self.staff)
283
+
284
+ def _current_token(self, device):
285
+ totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift)
286
+ totp.time = time.time()
287
+ return f"{totp.token():0{device.digits}d}"
288
+
289
+ def test_generate_creates_pending_device(self):
290
+ resp = self.client.post(
291
+ reverse("pages:authenticator-setup"), {"action": "generate"}
292
+ )
293
+ self.assertRedirects(resp, reverse("pages:authenticator-setup"))
294
+ device = TOTPDevice.objects.get(user=self.staff)
295
+ self.assertFalse(device.confirmed)
296
+ self.assertEqual(device.name, TOTP_DEVICE_NAME)
297
+
298
+ def test_device_config_url_includes_issuer_prefix(self):
299
+ self.client.post(reverse("pages:authenticator-setup"), {"action": "generate"})
300
+ device = TOTPDevice.objects.get(user=self.staff)
301
+ config_url = device.config_url
302
+ label = quote(f"{settings.OTP_TOTP_ISSUER}:{self.staff.username}")
303
+ self.assertIn(label, config_url)
304
+ self.assertIn(f"issuer={quote(settings.OTP_TOTP_ISSUER)}", config_url)
305
+
306
+ def test_pending_device_context_includes_qr(self):
307
+ self.client.post(reverse("pages:authenticator-setup"), {"action": "generate"})
308
+ resp = self.client.get(reverse("pages:authenticator-setup"))
309
+ self.assertEqual(resp.status_code, 200)
310
+ self.assertTrue(resp.context["qr_data_uri"].startswith("data:image/png;base64,"))
311
+ self.assertTrue(resp.context["manual_key"])
312
+
313
+ def test_confirm_pending_device(self):
314
+ self.client.post(reverse("pages:authenticator-setup"), {"action": "generate"})
315
+ device = TOTPDevice.objects.get(user=self.staff)
316
+ token = self._current_token(device)
317
+ resp = self.client.post(
318
+ reverse("pages:authenticator-setup"),
319
+ {"action": "confirm", "token": token},
320
+ )
321
+ self.assertRedirects(resp, reverse("pages:authenticator-setup"))
322
+ device.refresh_from_db()
323
+ self.assertTrue(device.confirmed)
324
+
325
+ def test_remove_device(self):
326
+ self.client.post(reverse("pages:authenticator-setup"), {"action": "generate"})
327
+ device = TOTPDevice.objects.get(user=self.staff)
328
+ token = self._current_token(device)
329
+ self.client.post(
330
+ reverse("pages:authenticator-setup"),
331
+ {"action": "confirm", "token": token},
332
+ )
333
+ resp = self.client.post(
334
+ reverse("pages:authenticator-setup"), {"action": "remove"}
335
+ )
336
+ self.assertRedirects(resp, reverse("pages:authenticator-setup"))
337
+ self.assertFalse(TOTPDevice.objects.filter(user=self.staff).exists())
100
338
 
101
339
  @override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
102
340
  class InvitationTests(TestCase):
@@ -336,6 +574,56 @@ class AdminBadgesTests(TestCase):
336
574
  self.assertContains(resp, f'href="{node_change}"')
337
575
 
338
576
 
577
+ class AdminDashboardAppListTests(TestCase):
578
+ def setUp(self):
579
+ self.client = Client()
580
+ User = get_user_model()
581
+ self.admin = User.objects.create_superuser(
582
+ username="dashboard_admin", password="pwd", email="admin@example.com"
583
+ )
584
+ self.client.force_login(self.admin)
585
+ Site.objects.update_or_create(
586
+ id=1, defaults={"name": "test", "domain": "testserver"}
587
+ )
588
+ self.locks_dir = Path(settings.BASE_DIR) / "locks"
589
+ self.locks_dir.mkdir(parents=True, exist_ok=True)
590
+ self.celery_lock = self.locks_dir / "celery.lck"
591
+ if self.celery_lock.exists():
592
+ self.celery_lock.unlink()
593
+ self.addCleanup(self._remove_celery_lock)
594
+ self.node, _ = Node.objects.update_or_create(
595
+ mac_address=Node.get_current_mac(),
596
+ defaults={
597
+ "hostname": socket.gethostname(),
598
+ "address": socket.gethostbyname(socket.gethostname()),
599
+ "base_path": settings.BASE_DIR,
600
+ "port": 8000,
601
+ },
602
+ )
603
+ self.node.features.clear()
604
+
605
+ def _remove_celery_lock(self):
606
+ try:
607
+ self.celery_lock.unlink()
608
+ except FileNotFoundError:
609
+ pass
610
+
611
+ def test_horologia_hidden_without_celery_feature(self):
612
+ resp = self.client.get(reverse("admin:index"))
613
+ self.assertNotContains(resp, "5. Horologia MODELS")
614
+
615
+ def test_horologia_visible_with_celery_feature(self):
616
+ feature = NodeFeature.objects.create(slug="celery-queue", display="Celery Queue")
617
+ NodeFeatureAssignment.objects.create(node=self.node, feature=feature)
618
+ resp = self.client.get(reverse("admin:index"))
619
+ self.assertContains(resp, "5. Horologia MODELS")
620
+
621
+ def test_horologia_visible_with_celery_lock(self):
622
+ self.celery_lock.write_text("")
623
+ resp = self.client.get(reverse("admin:index"))
624
+ self.assertContains(resp, "5. Horologia MODELS")
625
+
626
+
339
627
  class AdminSidebarTests(TestCase):
340
628
  def setUp(self):
341
629
  self.client = Client()
@@ -440,6 +728,7 @@ class ViewHistoryAdminTests(TestCase):
440
728
  resp = self.client.get(reverse("admin:pages_viewhistory_traffic_graph"))
441
729
  self.assertContains(resp, "viewhistory-chart")
442
730
  self.assertContains(resp, reverse("admin:pages_viewhistory_changelist"))
731
+ self.assertContains(resp, static("core/vendor/chart.umd.min.js"))
443
732
 
444
733
  def test_graph_data_endpoint(self):
445
734
  self._create_history("/", count=2)
@@ -461,6 +750,7 @@ class ViewHistoryAdminTests(TestCase):
461
750
  resp = self.client.get(reverse("admin:index"))
462
751
  self.assertContains(resp, "viewhistory-mini-module")
463
752
  self.assertContains(resp, reverse("admin:pages_viewhistory_traffic_graph"))
753
+ self.assertContains(resp, static("core/vendor/chart.umd.min.js"))
464
754
 
465
755
 
466
756
  class AdminModelStatusTests(TestCase):
@@ -657,12 +947,6 @@ class ConstellationNavTests(TestCase):
657
947
  "fixtures",
658
948
  "constellation__module_ocpp.json",
659
949
  ),
660
- Path(
661
- settings.BASE_DIR,
662
- "pages",
663
- "fixtures",
664
- "constellation__module_rfid.json",
665
- ),
666
950
  Path(
667
951
  settings.BASE_DIR,
668
952
  "pages",
@@ -702,6 +986,106 @@ class ConstellationNavTests(TestCase):
702
986
  is_deleted=False,
703
987
  ).exists()
704
988
  )
989
+ ocpp_module = next(
990
+ module
991
+ for module in resp.context["nav_modules"]
992
+ if module.menu_label.upper() == "CHARGERS"
993
+ )
994
+ landing_labels = [landing.label for landing in ocpp_module.enabled_landings]
995
+ self.assertIn("RFID Tag Validator", landing_labels)
996
+
997
+ def test_ocpp_dashboard_visible(self):
998
+ resp = self.client.get(reverse("pages:index"))
999
+ self.assertContains(resp, 'href="/ocpp/"')
1000
+
1001
+ def test_header_links_visible_when_defined(self):
1002
+ Reference.objects.create(
1003
+ alt_text="Console",
1004
+ value="https://example.com/console",
1005
+ show_in_header=True,
1006
+ )
1007
+
1008
+ resp = self.client.get(reverse("pages:index"))
1009
+
1010
+ self.assertIn("header_references", resp.context)
1011
+ self.assertTrue(resp.context["header_references"])
1012
+ self.assertContains(resp, "LINKS")
1013
+ self.assertContains(resp, 'href="https://example.com/console"')
1014
+
1015
+ def test_header_links_hidden_when_flag_false(self):
1016
+ Reference.objects.create(
1017
+ alt_text="Hidden",
1018
+ value="https://example.com/hidden",
1019
+ show_in_header=False,
1020
+ )
1021
+
1022
+ resp = self.client.get(reverse("pages:index"))
1023
+
1024
+ self.assertIn("header_references", resp.context)
1025
+ self.assertFalse(resp.context["header_references"])
1026
+ self.assertNotContains(resp, "https://example.com/hidden")
1027
+
1028
+
1029
+ class PowerNavTests(TestCase):
1030
+ def setUp(self):
1031
+ self.client = Client()
1032
+ role, _ = NodeRole.objects.get_or_create(name="Terminal")
1033
+ Node.objects.update_or_create(
1034
+ mac_address=Node.get_current_mac(),
1035
+ defaults={"hostname": "localhost", "address": "127.0.0.1", "role": role},
1036
+ )
1037
+ Site.objects.update_or_create(
1038
+ id=1, defaults={"domain": "testserver", "name": ""}
1039
+ )
1040
+ awg_app, _ = Application.objects.get_or_create(name="awg")
1041
+ awg_module, _ = Module.objects.get_or_create(
1042
+ node_role=role, application=awg_app, path="/awg/"
1043
+ )
1044
+ awg_module.create_landings()
1045
+ man_app, _ = Application.objects.get_or_create(name="man")
1046
+ man_module, _ = Module.objects.get_or_create(
1047
+ node_role=role, application=man_app, path="/man/"
1048
+ )
1049
+ man_module.create_landings()
1050
+ User = get_user_model()
1051
+ self.user = User.objects.create_user("user", password="pw")
1052
+
1053
+ def test_power_pill_lists_calculators(self):
1054
+ resp = self.client.get(reverse("pages:index"))
1055
+ power_module = None
1056
+ for module in resp.context["nav_modules"]:
1057
+ if module.path == "/awg/":
1058
+ power_module = module
1059
+ break
1060
+ self.assertIsNotNone(power_module)
1061
+ self.assertEqual(power_module.menu_label.upper(), "CALCULATE")
1062
+ landing_labels = {landing.label for landing in power_module.enabled_landings}
1063
+ self.assertIn("AWG Calculator", landing_labels)
1064
+
1065
+ def test_manual_pill_label(self):
1066
+ resp = self.client.get(reverse("pages:index"))
1067
+ manuals_module = None
1068
+ for module in resp.context["nav_modules"]:
1069
+ if module.path == "/man/":
1070
+ manuals_module = module
1071
+ break
1072
+ self.assertIsNotNone(manuals_module)
1073
+ self.assertEqual(manuals_module.menu_label.upper(), "MANUALS")
1074
+ landing_labels = {landing.label for landing in manuals_module.enabled_landings}
1075
+ self.assertIn("Manuals", landing_labels)
1076
+
1077
+ def test_energy_tariff_visible_when_logged_in(self):
1078
+ self.client.force_login(self.user)
1079
+ resp = self.client.get(reverse("pages:index"))
1080
+ power_module = None
1081
+ for module in resp.context["nav_modules"]:
1082
+ if module.path == "/awg/":
1083
+ power_module = module
1084
+ break
1085
+ self.assertIsNotNone(power_module)
1086
+ landing_labels = {landing.label for landing in power_module.enabled_landings}
1087
+ self.assertIn("AWG Calculator", landing_labels)
1088
+ self.assertIn("Energy Tariff Calculator", landing_labels)
705
1089
 
706
1090
 
707
1091
  class StaffNavVisibilityTests(TestCase):
@@ -1116,16 +1500,22 @@ class FavoriteTests(TestCase):
1116
1500
  ).exists()
1117
1501
  )
1118
1502
 
1119
- def test_dashboard_uses_browse_label(self):
1503
+ def test_dashboard_uses_change_label(self):
1120
1504
  ct = ContentType.objects.get_by_natural_key("pages", "application")
1121
1505
  Favorite.objects.create(user=self.user, content_type=ct)
1122
1506
  resp = self.client.get(reverse("admin:index"))
1123
- self.assertContains(resp, "Browse Applications")
1507
+ self.assertContains(resp, "Change Applications")
1508
+ self.assertContains(resp, 'target="_blank" rel="noopener noreferrer"')
1124
1509
 
1125
- def test_dashboard_uses_todo_url_if_set(self):
1126
- Todo.objects.create(request="Check docs", url="/docs/")
1510
+ def test_dashboard_links_to_focus_view(self):
1511
+ todo = Todo.objects.create(request="Check docs", url="/docs/")
1127
1512
  resp = self.client.get(reverse("admin:index"))
1128
- self.assertContains(resp, 'href="/docs/"')
1513
+ focus_url = reverse("todo-focus", args=[todo.pk])
1514
+ expected_next = quote(reverse("admin:index"))
1515
+ self.assertContains(
1516
+ resp,
1517
+ f'href="{focus_url}?next={expected_next}"',
1518
+ )
1129
1519
 
1130
1520
  def test_dashboard_shows_todo_with_done_button(self):
1131
1521
  todo = Todo.objects.create(request="Do thing")
@@ -1200,6 +1590,37 @@ class FavoriteTests(TestCase):
1200
1590
  self.assertContains(resp, todo.request)
1201
1591
 
1202
1592
 
1593
+ class AdminActionListTests(TestCase):
1594
+ def setUp(self):
1595
+ User = get_user_model()
1596
+ User.objects.filter(username="action-admin").delete()
1597
+ self.user = User.objects.create_superuser(
1598
+ username="action-admin",
1599
+ password="pwd",
1600
+ email="action@example.com",
1601
+ )
1602
+ self.factory = RequestFactory()
1603
+
1604
+ def test_profile_actions_available_without_selection(self):
1605
+ from pages.templatetags.admin_extras import model_admin_actions
1606
+
1607
+ request = self.factory.get("/")
1608
+ request.user = self.user
1609
+ context = {"request": request}
1610
+
1611
+ registered = [
1612
+ (model._meta.app_label, model._meta.object_name)
1613
+ for model, admin_instance in admin.site._registry.items()
1614
+ if isinstance(admin_instance, ProfileAdminMixin)
1615
+ ]
1616
+
1617
+ for app_label, object_name in registered:
1618
+ with self.subTest(model=f"{app_label}.{object_name}"):
1619
+ actions = model_admin_actions(context, app_label, object_name)
1620
+ labels = {action["label"] for action in actions}
1621
+ self.assertIn("Active Profile", labels)
1622
+
1623
+
1203
1624
  class AdminModelGraphViewTests(TestCase):
1204
1625
  def setUp(self):
1205
1626
  self.client = Client()
pages/urls.py CHANGED
@@ -10,6 +10,7 @@ urlpatterns = [
10
10
  path("client-report/", views.client_report, name="client-report"),
11
11
  path("release-checklist", views.release_checklist, name="release-checklist"),
12
12
  path("login/", views.login_view, name="login"),
13
+ path("authenticator/setup/", views.authenticator_setup, name="authenticator-setup"),
13
14
  path("request-invite/", views.request_invite, name="request-invite"),
14
15
  path(
15
16
  "invitation/<uidb64>/<token>/",