arthexis 0.1.8__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.
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/METADATA +87 -6
- arthexis-0.1.10.dist-info/RECORD +95 -0
- arthexis-0.1.10.dist-info/licenses/LICENSE +674 -0
- config/__init__.py +0 -1
- config/auth_app.py +0 -1
- config/celery.py +1 -2
- config/context_processors.py +1 -1
- config/offline.py +2 -0
- config/settings.py +352 -37
- config/urls.py +71 -6
- core/admin.py +1601 -200
- core/admin_history.py +50 -0
- core/admindocs.py +108 -1
- core/apps.py +161 -3
- core/auto_upgrade.py +57 -0
- core/backends.py +123 -7
- core/entity.py +62 -48
- core/fields.py +98 -0
- core/github_helper.py +25 -0
- core/github_issues.py +172 -0
- core/lcd_screen.py +1 -0
- core/liveupdate.py +25 -0
- core/log_paths.py +100 -0
- core/mailer.py +83 -0
- core/middleware.py +57 -0
- core/models.py +1279 -267
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/reference_utils.py +97 -0
- core/release.py +27 -20
- core/sigil_builder.py +144 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +162 -29
- core/tasks.py +269 -27
- core/test_system_info.py +59 -1
- core/tests.py +644 -73
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +425 -168
- core/views.py +627 -59
- core/widgets.py +51 -0
- core/workgroup_urls.py +7 -3
- core/workgroup_views.py +43 -6
- nodes/actions.py +0 -2
- nodes/admin.py +168 -285
- nodes/apps.py +9 -15
- nodes/backends.py +145 -0
- nodes/lcd.py +24 -10
- nodes/models.py +579 -179
- nodes/tasks.py +1 -5
- nodes/tests.py +894 -130
- nodes/utils.py +13 -2
- nodes/views.py +204 -28
- ocpp/admin.py +212 -63
- ocpp/apps.py +1 -1
- ocpp/consumers.py +642 -68
- ocpp/evcs.py +30 -10
- ocpp/models.py +452 -70
- ocpp/simulator.py +75 -11
- ocpp/store.py +288 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1576 -137
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +701 -123
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +39 -6
- pages/forms.py +131 -0
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +1182 -42
- pages/urls.py +4 -0
- pages/utils.py +0 -1
- pages/views.py +844 -51
- arthexis-0.1.8.dist-info/RECORD +0 -80
- arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
pages/tests.py
CHANGED
|
@@ -1,29 +1,69 @@
|
|
|
1
|
-
|
|
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
|
|
6
15
|
from django.contrib import admin
|
|
7
16
|
from django.core.exceptions import DisallowedHost
|
|
8
17
|
import socket
|
|
9
|
-
from pages.models import Application, Module, SiteBadge, Favorite
|
|
10
|
-
from core.user_data import UserDatum
|
|
18
|
+
from pages.models import Application, Module, SiteBadge, Favorite, ViewHistory
|
|
11
19
|
from pages.admin import ApplicationAdmin
|
|
20
|
+
from pages.screenshot_specs import (
|
|
21
|
+
ScreenshotSpec,
|
|
22
|
+
ScreenshotSpecRunner,
|
|
23
|
+
ScreenshotUnavailable,
|
|
24
|
+
registry,
|
|
25
|
+
)
|
|
12
26
|
from django.apps import apps as django_apps
|
|
13
|
-
from core
|
|
27
|
+
from core import mailer
|
|
28
|
+
from core.admin import ProfileAdminMixin
|
|
29
|
+
from core.models import (
|
|
30
|
+
AdminHistory,
|
|
31
|
+
InviteLead,
|
|
32
|
+
Package,
|
|
33
|
+
Reference,
|
|
34
|
+
ReleaseManager,
|
|
35
|
+
Todo,
|
|
36
|
+
)
|
|
14
37
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
|
15
38
|
import base64
|
|
16
39
|
import tempfile
|
|
17
40
|
import shutil
|
|
41
|
+
from io import StringIO
|
|
18
42
|
from django.conf import settings
|
|
19
43
|
from pathlib import Path
|
|
20
|
-
from unittest.mock import patch
|
|
21
|
-
from
|
|
44
|
+
from unittest.mock import patch, Mock
|
|
45
|
+
from types import SimpleNamespace
|
|
22
46
|
from django.core.management import call_command
|
|
23
47
|
import re
|
|
24
48
|
from django.contrib.contenttypes.models import ContentType
|
|
25
|
-
|
|
26
|
-
from
|
|
49
|
+
from datetime import date, timedelta
|
|
50
|
+
from django.core import mail
|
|
51
|
+
from django.utils import timezone
|
|
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
|
|
58
|
+
|
|
59
|
+
from nodes.models import (
|
|
60
|
+
EmailOutbox,
|
|
61
|
+
Node,
|
|
62
|
+
ContentSample,
|
|
63
|
+
NodeRole,
|
|
64
|
+
NodeFeature,
|
|
65
|
+
NodeFeatureAssignment,
|
|
66
|
+
)
|
|
27
67
|
|
|
28
68
|
|
|
29
69
|
class LoginViewTests(TestCase):
|
|
@@ -40,6 +80,10 @@ class LoginViewTests(TestCase):
|
|
|
40
80
|
resp = self.client.get(reverse("pages:index"))
|
|
41
81
|
self.assertContains(resp, 'href="/login/"')
|
|
42
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
|
+
|
|
43
87
|
def test_staff_login_redirects_admin(self):
|
|
44
88
|
resp = self.client.post(
|
|
45
89
|
reverse("pages:login"),
|
|
@@ -47,6 +91,49 @@ class LoginViewTests(TestCase):
|
|
|
47
91
|
)
|
|
48
92
|
self.assertRedirects(resp, reverse("admin:index"))
|
|
49
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
|
+
|
|
50
137
|
def test_already_logged_in_staff_redirects(self):
|
|
51
138
|
self.client.force_login(self.staff)
|
|
52
139
|
resp = self.client.get(reverse("pages:login"))
|
|
@@ -67,6 +154,189 @@ class LoginViewTests(TestCase):
|
|
|
67
154
|
self.assertRedirects(resp, "/nodes/list/")
|
|
68
155
|
|
|
69
156
|
|
|
157
|
+
|
|
158
|
+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.dummy.EmailBackend")
|
|
159
|
+
def test_login_page_hides_request_link_without_email_backend(self):
|
|
160
|
+
resp = self.client.get(reverse("pages:login"))
|
|
161
|
+
self.assertFalse(resp.context["can_request_invite"])
|
|
162
|
+
self.assertNotContains(resp, reverse("pages:request-invite"))
|
|
163
|
+
|
|
164
|
+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.dummy.EmailBackend")
|
|
165
|
+
def test_login_page_shows_request_link_when_outbox_configured(self):
|
|
166
|
+
EmailOutbox.objects.create(host="smtp.example.com")
|
|
167
|
+
resp = self.client.get(reverse("pages:login"))
|
|
168
|
+
self.assertTrue(resp.context["can_request_invite"])
|
|
169
|
+
self.assertContains(resp, reverse("pages:request-invite"))
|
|
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())
|
|
338
|
+
|
|
339
|
+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
|
|
70
340
|
class InvitationTests(TestCase):
|
|
71
341
|
def setUp(self):
|
|
72
342
|
self.client = Client()
|
|
@@ -110,14 +380,27 @@ class InvitationTests(TestCase):
|
|
|
110
380
|
self.assertIn("_auth_user_id", self.client.session)
|
|
111
381
|
|
|
112
382
|
def test_request_invite_handles_email_errors(self):
|
|
113
|
-
with patch("pages.views.
|
|
383
|
+
with patch("pages.views.mailer.send", side_effect=Exception("fail")):
|
|
114
384
|
resp = self.client.post(
|
|
115
385
|
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
116
386
|
)
|
|
117
387
|
self.assertEqual(resp.status_code, 200)
|
|
118
|
-
self.assertContains(
|
|
119
|
-
|
|
388
|
+
self.assertContains(resp, "If the email exists, an invitation has been sent.")
|
|
389
|
+
lead = InviteLead.objects.get()
|
|
390
|
+
self.assertIsNone(lead.sent_on)
|
|
391
|
+
self.assertIn("fail", lead.error)
|
|
392
|
+
self.assertIn("email service", lead.error)
|
|
393
|
+
self.assertEqual(len(mail.outbox), 0)
|
|
394
|
+
|
|
395
|
+
def test_request_invite_records_send_time(self):
|
|
396
|
+
resp = self.client.post(
|
|
397
|
+
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
120
398
|
)
|
|
399
|
+
self.assertEqual(resp.status_code, 200)
|
|
400
|
+
lead = InviteLead.objects.get()
|
|
401
|
+
self.assertIsNotNone(lead.sent_on)
|
|
402
|
+
self.assertEqual(lead.error, "")
|
|
403
|
+
self.assertEqual(len(mail.outbox), 1)
|
|
121
404
|
|
|
122
405
|
def test_request_invite_creates_lead_with_comment(self):
|
|
123
406
|
resp = self.client.post(
|
|
@@ -128,6 +411,76 @@ class InvitationTests(TestCase):
|
|
|
128
411
|
lead = InviteLead.objects.get()
|
|
129
412
|
self.assertEqual(lead.email, "new@example.com")
|
|
130
413
|
self.assertEqual(lead.comment, "Hello")
|
|
414
|
+
self.assertIsNone(lead.sent_on)
|
|
415
|
+
self.assertEqual(lead.error, "")
|
|
416
|
+
self.assertEqual(lead.mac_address, "")
|
|
417
|
+
self.assertEqual(len(mail.outbox), 0)
|
|
418
|
+
|
|
419
|
+
def test_request_invite_falls_back_to_send_mail(self):
|
|
420
|
+
node = Node.objects.create(
|
|
421
|
+
hostname="local", address="127.0.0.1", mac_address="00:11:22:33:44:55"
|
|
422
|
+
)
|
|
423
|
+
with (
|
|
424
|
+
patch("pages.views.Node.get_local", return_value=node),
|
|
425
|
+
patch.object(
|
|
426
|
+
node, "send_mail", side_effect=Exception("node fail")
|
|
427
|
+
) as node_send,
|
|
428
|
+
patch("pages.views.mailer.send", wraps=mailer.send) as fallback,
|
|
429
|
+
):
|
|
430
|
+
resp = self.client.post(
|
|
431
|
+
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
432
|
+
)
|
|
433
|
+
self.assertEqual(resp.status_code, 200)
|
|
434
|
+
lead = InviteLead.objects.get()
|
|
435
|
+
self.assertIsNotNone(lead.sent_on)
|
|
436
|
+
self.assertIn("node fail", lead.error)
|
|
437
|
+
self.assertIn("default mail backend", lead.error)
|
|
438
|
+
self.assertTrue(node_send.called)
|
|
439
|
+
self.assertTrue(fallback.called)
|
|
440
|
+
self.assertEqual(len(mail.outbox), 1)
|
|
441
|
+
|
|
442
|
+
@patch(
|
|
443
|
+
"pages.views.public_wifi.resolve_mac_address",
|
|
444
|
+
return_value="aa:bb:cc:dd:ee:ff",
|
|
445
|
+
)
|
|
446
|
+
def test_request_invite_records_mac_address(self, mock_resolve):
|
|
447
|
+
resp = self.client.post(
|
|
448
|
+
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
449
|
+
)
|
|
450
|
+
self.assertEqual(resp.status_code, 200)
|
|
451
|
+
lead = InviteLead.objects.get()
|
|
452
|
+
self.assertEqual(lead.mac_address, "aa:bb:cc:dd:ee:ff")
|
|
453
|
+
|
|
454
|
+
@patch("pages.views.public_wifi.grant_public_access")
|
|
455
|
+
@patch(
|
|
456
|
+
"pages.views.public_wifi.resolve_mac_address",
|
|
457
|
+
return_value="aa:bb:cc:dd:ee:ff",
|
|
458
|
+
)
|
|
459
|
+
def test_invitation_login_grants_public_wifi_access(self, mock_resolve, mock_grant):
|
|
460
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
461
|
+
feature = NodeFeature.objects.create(
|
|
462
|
+
slug="ap-public-wifi", display="AP Public Wi-Fi"
|
|
463
|
+
)
|
|
464
|
+
feature.roles.add(control_role)
|
|
465
|
+
node = Node.objects.create(
|
|
466
|
+
hostname="control",
|
|
467
|
+
address="127.0.0.1",
|
|
468
|
+
mac_address=Node.get_current_mac(),
|
|
469
|
+
role=control_role,
|
|
470
|
+
)
|
|
471
|
+
NodeFeatureAssignment.objects.create(node=node, feature=feature)
|
|
472
|
+
with patch("pages.views.Node.get_local", return_value=node):
|
|
473
|
+
resp = self.client.post(
|
|
474
|
+
reverse("pages:request-invite"), {"email": "invite@example.com"}
|
|
475
|
+
)
|
|
476
|
+
self.assertEqual(resp.status_code, 200)
|
|
477
|
+
link = re.search(r"http://testserver[\S]+", mail.outbox[0].body).group(0)
|
|
478
|
+
with patch("pages.views.Node.get_local", return_value=node):
|
|
479
|
+
resp = self.client.post(link)
|
|
480
|
+
self.assertEqual(resp.status_code, 302)
|
|
481
|
+
self.user.refresh_from_db()
|
|
482
|
+
self.assertTrue(self.user.is_active)
|
|
483
|
+
mock_grant.assert_called_once_with(self.user, "aa:bb:cc:dd:ee:ff")
|
|
131
484
|
|
|
132
485
|
|
|
133
486
|
class NavbarBrandTests(TestCase):
|
|
@@ -139,16 +492,12 @@ class NavbarBrandTests(TestCase):
|
|
|
139
492
|
|
|
140
493
|
def test_site_name_displayed_when_known(self):
|
|
141
494
|
resp = self.client.get(reverse("pages:index"))
|
|
142
|
-
self.assertContains(
|
|
143
|
-
resp, '<a class="navbar-brand" href="/">Terminal</a>'
|
|
144
|
-
)
|
|
495
|
+
self.assertContains(resp, '<a class="navbar-brand" href="/">Terminal</a>')
|
|
145
496
|
|
|
146
497
|
def test_default_brand_when_unknown(self):
|
|
147
498
|
Site.objects.filter(id=1).update(domain="example.com")
|
|
148
499
|
resp = self.client.get(reverse("pages:index"))
|
|
149
|
-
self.assertContains(
|
|
150
|
-
resp, '<a class="navbar-brand" href="/">Arthexis</a>'
|
|
151
|
-
)
|
|
500
|
+
self.assertContains(resp, '<a class="navbar-brand" href="/">Arthexis</a>')
|
|
152
501
|
|
|
153
502
|
@override_settings(ALLOWED_HOSTS=["127.0.0.1", "testserver"])
|
|
154
503
|
def test_brand_uses_role_name_when_site_name_blank(self):
|
|
@@ -198,7 +547,11 @@ class AdminBadgesTests(TestCase):
|
|
|
198
547
|
self.node.role = role
|
|
199
548
|
self.node.save()
|
|
200
549
|
resp = self.client.get(reverse("admin:index"))
|
|
550
|
+
role_list = reverse("admin:nodes_noderole_changelist")
|
|
551
|
+
role_change = reverse("admin:nodes_noderole_change", args=[role.pk])
|
|
201
552
|
self.assertContains(resp, "ROLE: Dev")
|
|
553
|
+
self.assertContains(resp, f'href="{role_list}"')
|
|
554
|
+
self.assertContains(resp, f'href="{role_change}"')
|
|
202
555
|
|
|
203
556
|
def test_badges_warn_when_node_missing(self):
|
|
204
557
|
from nodes.models import Node
|
|
@@ -221,6 +574,56 @@ class AdminBadgesTests(TestCase):
|
|
|
221
574
|
self.assertContains(resp, f'href="{node_change}"')
|
|
222
575
|
|
|
223
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
|
+
|
|
224
627
|
class AdminSidebarTests(TestCase):
|
|
225
628
|
def setUp(self):
|
|
226
629
|
self.client = Client()
|
|
@@ -242,6 +645,140 @@ class AdminSidebarTests(TestCase):
|
|
|
242
645
|
self.assertContains(resp, 'id="admin-collapsible-apps"')
|
|
243
646
|
|
|
244
647
|
|
|
648
|
+
class ViewHistoryLoggingTests(TestCase):
|
|
649
|
+
def setUp(self):
|
|
650
|
+
self.client = Client()
|
|
651
|
+
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
652
|
+
|
|
653
|
+
def test_successful_visit_creates_entry(self):
|
|
654
|
+
resp = self.client.get(reverse("pages:index"))
|
|
655
|
+
self.assertEqual(resp.status_code, 200)
|
|
656
|
+
entry = ViewHistory.objects.order_by("-visited_at").first()
|
|
657
|
+
self.assertIsNotNone(entry)
|
|
658
|
+
self.assertEqual(entry.path, "/")
|
|
659
|
+
self.assertEqual(entry.status_code, 200)
|
|
660
|
+
self.assertEqual(entry.error_message, "")
|
|
661
|
+
|
|
662
|
+
def test_error_visit_records_message(self):
|
|
663
|
+
resp = self.client.get("/missing-page/")
|
|
664
|
+
self.assertEqual(resp.status_code, 404)
|
|
665
|
+
entry = (
|
|
666
|
+
ViewHistory.objects.filter(path="/missing-page/")
|
|
667
|
+
.order_by("-visited_at")
|
|
668
|
+
.first()
|
|
669
|
+
)
|
|
670
|
+
self.assertIsNotNone(entry)
|
|
671
|
+
self.assertEqual(entry.status_code, 404)
|
|
672
|
+
self.assertNotEqual(entry.error_message, "")
|
|
673
|
+
|
|
674
|
+
def test_debug_toolbar_requests_not_tracked(self):
|
|
675
|
+
resp = self.client.get(reverse("pages:index"), {"djdt": "toolbar"})
|
|
676
|
+
self.assertEqual(resp.status_code, 200)
|
|
677
|
+
self.assertFalse(ViewHistory.objects.exists())
|
|
678
|
+
|
|
679
|
+
def test_authenticated_user_last_visit_ip_updated(self):
|
|
680
|
+
User = get_user_model()
|
|
681
|
+
user = User.objects.create_user(
|
|
682
|
+
username="history_user", password="pwd", email="history@example.com"
|
|
683
|
+
)
|
|
684
|
+
self.assertTrue(self.client.login(username="history_user", password="pwd"))
|
|
685
|
+
|
|
686
|
+
resp = self.client.get(
|
|
687
|
+
reverse("pages:index"),
|
|
688
|
+
HTTP_X_FORWARDED_FOR="203.0.113.5",
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
self.assertEqual(resp.status_code, 200)
|
|
692
|
+
user.refresh_from_db()
|
|
693
|
+
self.assertEqual(user.last_visit_ip_address, "203.0.113.5")
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
class ViewHistoryAdminTests(TestCase):
|
|
697
|
+
def setUp(self):
|
|
698
|
+
self.client = Client()
|
|
699
|
+
User = get_user_model()
|
|
700
|
+
self.admin = User.objects.create_superuser(
|
|
701
|
+
username="history_admin", password="pwd", email="admin@example.com"
|
|
702
|
+
)
|
|
703
|
+
self.client.force_login(self.admin)
|
|
704
|
+
Site.objects.update_or_create(
|
|
705
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
def _create_history(self, path: str, days_offset: int = 0, count: int = 1):
|
|
709
|
+
for _ in range(count):
|
|
710
|
+
entry = ViewHistory.objects.create(
|
|
711
|
+
path=path,
|
|
712
|
+
method="GET",
|
|
713
|
+
status_code=200,
|
|
714
|
+
status_text="OK",
|
|
715
|
+
error_message="",
|
|
716
|
+
view_name="pages:index",
|
|
717
|
+
)
|
|
718
|
+
if days_offset:
|
|
719
|
+
entry.visited_at = timezone.now() - timedelta(days=days_offset)
|
|
720
|
+
entry.save(update_fields=["visited_at"])
|
|
721
|
+
|
|
722
|
+
def test_change_list_includes_graph_link(self):
|
|
723
|
+
resp = self.client.get(reverse("admin:pages_viewhistory_changelist"))
|
|
724
|
+
self.assertContains(resp, reverse("admin:pages_viewhistory_traffic_graph"))
|
|
725
|
+
self.assertContains(resp, "Traffic graph")
|
|
726
|
+
|
|
727
|
+
def test_graph_view_renders_canvas(self):
|
|
728
|
+
resp = self.client.get(reverse("admin:pages_viewhistory_traffic_graph"))
|
|
729
|
+
self.assertContains(resp, "viewhistory-chart")
|
|
730
|
+
self.assertContains(resp, reverse("admin:pages_viewhistory_changelist"))
|
|
731
|
+
self.assertContains(resp, static("core/vendor/chart.umd.min.js"))
|
|
732
|
+
|
|
733
|
+
def test_graph_data_endpoint(self):
|
|
734
|
+
self._create_history("/", count=2)
|
|
735
|
+
self._create_history("/about/", days_offset=1)
|
|
736
|
+
url = reverse("admin:pages_viewhistory_traffic_data")
|
|
737
|
+
resp = self.client.get(url)
|
|
738
|
+
self.assertEqual(resp.status_code, 200)
|
|
739
|
+
data = resp.json()
|
|
740
|
+
self.assertIn("labels", data)
|
|
741
|
+
self.assertIn("datasets", data)
|
|
742
|
+
self.assertGreater(len(data["labels"]), 0)
|
|
743
|
+
totals = {
|
|
744
|
+
dataset["label"]: sum(dataset["data"]) for dataset in data["datasets"]
|
|
745
|
+
}
|
|
746
|
+
self.assertEqual(totals.get("/"), 2)
|
|
747
|
+
self.assertEqual(totals.get("/about/"), 1)
|
|
748
|
+
|
|
749
|
+
def test_admin_index_displays_widget(self):
|
|
750
|
+
resp = self.client.get(reverse("admin:index"))
|
|
751
|
+
self.assertContains(resp, "viewhistory-mini-module")
|
|
752
|
+
self.assertContains(resp, reverse("admin:pages_viewhistory_traffic_graph"))
|
|
753
|
+
self.assertContains(resp, static("core/vendor/chart.umd.min.js"))
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
class AdminModelStatusTests(TestCase):
|
|
757
|
+
def setUp(self):
|
|
758
|
+
self.client = Client()
|
|
759
|
+
User = get_user_model()
|
|
760
|
+
self.admin = User.objects.create_superuser(
|
|
761
|
+
username="status_admin", password="pwd", email="admin@example.com"
|
|
762
|
+
)
|
|
763
|
+
self.client.force_login(self.admin)
|
|
764
|
+
Site.objects.update_or_create(
|
|
765
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
766
|
+
)
|
|
767
|
+
from nodes.models import Node
|
|
768
|
+
|
|
769
|
+
Node.objects.create(hostname="testserver", address="127.0.0.1")
|
|
770
|
+
|
|
771
|
+
@patch("pages.templatetags.admin_extras.connection.introspection.table_names")
|
|
772
|
+
def test_status_dots_render(self, mock_tables):
|
|
773
|
+
from django.db import connection
|
|
774
|
+
|
|
775
|
+
tables = type(connection.introspection).table_names(connection.introspection)
|
|
776
|
+
mock_tables.return_value = [t for t in tables if t != "pages_module"]
|
|
777
|
+
resp = self.client.get(reverse("admin:index"))
|
|
778
|
+
self.assertContains(resp, 'class="model-status ok"')
|
|
779
|
+
self.assertContains(resp, 'class="model-status missing"', count=1)
|
|
780
|
+
|
|
781
|
+
|
|
245
782
|
class SiteAdminRegisterCurrentTests(TestCase):
|
|
246
783
|
def setUp(self):
|
|
247
784
|
self.client = Client()
|
|
@@ -382,6 +919,175 @@ class NavAppsTests(TestCase):
|
|
|
382
919
|
self.assertNotContains(resp, 'href="/core/"')
|
|
383
920
|
|
|
384
921
|
|
|
922
|
+
class ConstellationNavTests(TestCase):
|
|
923
|
+
def setUp(self):
|
|
924
|
+
self.client = Client()
|
|
925
|
+
role, _ = NodeRole.objects.get_or_create(name="Constellation")
|
|
926
|
+
Node.objects.update_or_create(
|
|
927
|
+
mac_address=Node.get_current_mac(),
|
|
928
|
+
defaults={
|
|
929
|
+
"hostname": "localhost",
|
|
930
|
+
"address": "127.0.0.1",
|
|
931
|
+
"role": role,
|
|
932
|
+
},
|
|
933
|
+
)
|
|
934
|
+
Site.objects.update_or_create(
|
|
935
|
+
id=1, defaults={"domain": "testserver", "name": ""}
|
|
936
|
+
)
|
|
937
|
+
fixtures = [
|
|
938
|
+
Path(
|
|
939
|
+
settings.BASE_DIR,
|
|
940
|
+
"pages",
|
|
941
|
+
"fixtures",
|
|
942
|
+
"constellation__application_ocpp.json",
|
|
943
|
+
),
|
|
944
|
+
Path(
|
|
945
|
+
settings.BASE_DIR,
|
|
946
|
+
"pages",
|
|
947
|
+
"fixtures",
|
|
948
|
+
"constellation__module_ocpp.json",
|
|
949
|
+
),
|
|
950
|
+
Path(
|
|
951
|
+
settings.BASE_DIR,
|
|
952
|
+
"pages",
|
|
953
|
+
"fixtures",
|
|
954
|
+
"constellation__landing_ocpp_dashboard.json",
|
|
955
|
+
),
|
|
956
|
+
Path(
|
|
957
|
+
settings.BASE_DIR,
|
|
958
|
+
"pages",
|
|
959
|
+
"fixtures",
|
|
960
|
+
"constellation__landing_ocpp_cp_simulator.json",
|
|
961
|
+
),
|
|
962
|
+
Path(
|
|
963
|
+
settings.BASE_DIR,
|
|
964
|
+
"pages",
|
|
965
|
+
"fixtures",
|
|
966
|
+
"constellation__landing_ocpp_rfid.json",
|
|
967
|
+
),
|
|
968
|
+
]
|
|
969
|
+
call_command("loaddata", *map(str, fixtures))
|
|
970
|
+
|
|
971
|
+
def test_rfid_pill_hidden(self):
|
|
972
|
+
resp = self.client.get(reverse("pages:index"))
|
|
973
|
+
nav_labels = [
|
|
974
|
+
module.menu_label.upper() for module in resp.context["nav_modules"]
|
|
975
|
+
]
|
|
976
|
+
self.assertNotIn("RFID", nav_labels)
|
|
977
|
+
self.assertTrue(
|
|
978
|
+
Module.objects.filter(
|
|
979
|
+
path="/ocpp/", node_role__name="Constellation"
|
|
980
|
+
).exists()
|
|
981
|
+
)
|
|
982
|
+
self.assertFalse(
|
|
983
|
+
Module.objects.filter(
|
|
984
|
+
path="/ocpp/rfid/",
|
|
985
|
+
node_role__name="Constellation",
|
|
986
|
+
is_deleted=False,
|
|
987
|
+
).exists()
|
|
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)
|
|
1089
|
+
|
|
1090
|
+
|
|
385
1091
|
class StaffNavVisibilityTests(TestCase):
|
|
386
1092
|
def setUp(self):
|
|
387
1093
|
self.client = Client()
|
|
@@ -472,16 +1178,30 @@ class LandingCreationTests(TestCase):
|
|
|
472
1178
|
self.role = role
|
|
473
1179
|
|
|
474
1180
|
def test_landings_created_on_module_creation(self):
|
|
475
|
-
module = Module.objects.create(
|
|
1181
|
+
module = Module.objects.create(
|
|
1182
|
+
node_role=self.role, application=self.app, path="/"
|
|
1183
|
+
)
|
|
476
1184
|
self.assertTrue(module.landings.filter(path="/").exists())
|
|
477
1185
|
|
|
478
1186
|
|
|
479
1187
|
class LandingFixtureTests(TestCase):
|
|
480
1188
|
def test_constellation_fixture_loads_without_duplicates(self):
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
1189
|
+
from glob import glob
|
|
1190
|
+
|
|
1191
|
+
NodeRole.objects.get_or_create(name="Constellation")
|
|
1192
|
+
fixtures = glob(
|
|
1193
|
+
str(Path(settings.BASE_DIR, "pages", "fixtures", "constellation__*.json"))
|
|
1194
|
+
)
|
|
1195
|
+
fixtures = sorted(
|
|
1196
|
+
fixtures,
|
|
1197
|
+
key=lambda path: (
|
|
1198
|
+
0 if "__application_" in path else 1 if "__module_" in path else 2
|
|
1199
|
+
),
|
|
1200
|
+
)
|
|
1201
|
+
call_command("loaddata", *fixtures)
|
|
1202
|
+
call_command("loaddata", *fixtures)
|
|
484
1203
|
module = Module.objects.get(path="/ocpp/", node_role__name="Constellation")
|
|
1204
|
+
module.create_landings()
|
|
485
1205
|
self.assertEqual(module.landings.filter(path="/ocpp/rfid/").count(), 1)
|
|
486
1206
|
|
|
487
1207
|
|
|
@@ -494,20 +1214,14 @@ class AllowedHostSubnetTests(TestCase):
|
|
|
494
1214
|
|
|
495
1215
|
@override_settings(ALLOWED_HOSTS=["10.42.0.0/16", "192.168.0.0/16"])
|
|
496
1216
|
def test_private_network_hosts_allowed(self):
|
|
497
|
-
resp = self.client.get(
|
|
498
|
-
reverse("pages:index"), HTTP_HOST="10.42.1.5"
|
|
499
|
-
)
|
|
1217
|
+
resp = self.client.get(reverse("pages:index"), HTTP_HOST="10.42.1.5")
|
|
500
1218
|
self.assertEqual(resp.status_code, 200)
|
|
501
|
-
resp = self.client.get(
|
|
502
|
-
reverse("pages:index"), HTTP_HOST="192.168.2.3"
|
|
503
|
-
)
|
|
1219
|
+
resp = self.client.get(reverse("pages:index"), HTTP_HOST="192.168.2.3")
|
|
504
1220
|
self.assertEqual(resp.status_code, 200)
|
|
505
1221
|
|
|
506
1222
|
@override_settings(ALLOWED_HOSTS=["10.42.0.0/16"])
|
|
507
1223
|
def test_host_outside_subnets_disallowed(self):
|
|
508
|
-
resp = self.client.get(
|
|
509
|
-
reverse("pages:index"), HTTP_HOST="11.0.0.1"
|
|
510
|
-
)
|
|
1224
|
+
resp = self.client.get(reverse("pages:index"), HTTP_HOST="11.0.0.1")
|
|
511
1225
|
self.assertEqual(resp.status_code, 400)
|
|
512
1226
|
|
|
513
1227
|
|
|
@@ -540,7 +1254,11 @@ class FaviconTests(TestCase):
|
|
|
540
1254
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
541
1255
|
Node.objects.update_or_create(
|
|
542
1256
|
mac_address=Node.get_current_mac(),
|
|
543
|
-
defaults={
|
|
1257
|
+
defaults={
|
|
1258
|
+
"hostname": "localhost",
|
|
1259
|
+
"address": "127.0.0.1",
|
|
1260
|
+
"role": role,
|
|
1261
|
+
},
|
|
544
1262
|
)
|
|
545
1263
|
site, _ = Site.objects.update_or_create(
|
|
546
1264
|
id=1, defaults={"domain": "testserver", "name": ""}
|
|
@@ -564,7 +1282,11 @@ class FaviconTests(TestCase):
|
|
|
564
1282
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
565
1283
|
Node.objects.update_or_create(
|
|
566
1284
|
mac_address=Node.get_current_mac(),
|
|
567
|
-
defaults={
|
|
1285
|
+
defaults={
|
|
1286
|
+
"hostname": "localhost",
|
|
1287
|
+
"address": "127.0.0.1",
|
|
1288
|
+
"role": role,
|
|
1289
|
+
},
|
|
568
1290
|
)
|
|
569
1291
|
site, _ = Site.objects.update_or_create(
|
|
570
1292
|
id=1, defaults={"domain": "testserver", "name": ""}
|
|
@@ -584,7 +1306,11 @@ class FaviconTests(TestCase):
|
|
|
584
1306
|
role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
585
1307
|
Node.objects.update_or_create(
|
|
586
1308
|
mac_address=Node.get_current_mac(),
|
|
587
|
-
defaults={
|
|
1309
|
+
defaults={
|
|
1310
|
+
"hostname": "localhost",
|
|
1311
|
+
"address": "127.0.0.1",
|
|
1312
|
+
"role": role,
|
|
1313
|
+
},
|
|
588
1314
|
)
|
|
589
1315
|
Site.objects.update_or_create(
|
|
590
1316
|
id=1, defaults={"domain": "testserver", "name": ""}
|
|
@@ -606,13 +1332,30 @@ class FavoriteTests(TestCase):
|
|
|
606
1332
|
self.user = User.objects.create_superuser(
|
|
607
1333
|
username="favadmin", password="pwd", email="fav@example.com"
|
|
608
1334
|
)
|
|
1335
|
+
ReleaseManager.objects.create(user=self.user)
|
|
609
1336
|
self.client.force_login(self.user)
|
|
610
|
-
Site.objects.update_or_create(
|
|
1337
|
+
Site.objects.update_or_create(
|
|
1338
|
+
id=1, defaults={"name": "test", "domain": "testserver"}
|
|
1339
|
+
)
|
|
1340
|
+
from nodes.models import Node, NodeRole
|
|
1341
|
+
|
|
1342
|
+
terminal_role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
1343
|
+
self.node, _ = Node.objects.update_or_create(
|
|
1344
|
+
mac_address=Node.get_current_mac(),
|
|
1345
|
+
defaults={
|
|
1346
|
+
"hostname": "localhost",
|
|
1347
|
+
"address": "127.0.0.1",
|
|
1348
|
+
"role": terminal_role,
|
|
1349
|
+
},
|
|
1350
|
+
)
|
|
1351
|
+
ContentType.objects.clear_cache()
|
|
611
1352
|
|
|
612
1353
|
def test_add_favorite(self):
|
|
613
1354
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
614
1355
|
next_url = reverse("admin:pages_application_changelist")
|
|
615
|
-
url =
|
|
1356
|
+
url = (
|
|
1357
|
+
reverse("admin:favorite_toggle", args=[ct.id]) + f"?next={quote(next_url)}"
|
|
1358
|
+
)
|
|
616
1359
|
resp = self.client.post(url, {"custom_label": "Apps", "user_data": "on"})
|
|
617
1360
|
self.assertRedirects(resp, next_url)
|
|
618
1361
|
fav = Favorite.objects.get(user=self.user, content_type=ct)
|
|
@@ -622,7 +1365,9 @@ class FavoriteTests(TestCase):
|
|
|
622
1365
|
def test_cancel_link_uses_next(self):
|
|
623
1366
|
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
624
1367
|
next_url = reverse("admin:pages_application_changelist")
|
|
625
|
-
url =
|
|
1368
|
+
url = (
|
|
1369
|
+
reverse("admin:favorite_toggle", args=[ct.id]) + f"?next={quote(next_url)}"
|
|
1370
|
+
)
|
|
626
1371
|
resp = self.client.get(url)
|
|
627
1372
|
self.assertContains(resp, f'href="{next_url}"')
|
|
628
1373
|
|
|
@@ -646,10 +1391,10 @@ class FavoriteTests(TestCase):
|
|
|
646
1391
|
|
|
647
1392
|
def test_dashboard_includes_favorites_and_user_data(self):
|
|
648
1393
|
fav_ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
649
|
-
Favorite.objects.create(
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
1394
|
+
Favorite.objects.create(
|
|
1395
|
+
user=self.user, content_type=fav_ct, custom_label="Apps"
|
|
1396
|
+
)
|
|
1397
|
+
NodeRole.objects.create(name="DataRole", is_user_data=True)
|
|
653
1398
|
resp = self.client.get(reverse("admin:index"))
|
|
654
1399
|
self.assertContains(resp, reverse("admin:pages_application_changelist"))
|
|
655
1400
|
self.assertContains(resp, reverse("admin:nodes_noderole_changelist"))
|
|
@@ -657,8 +1402,7 @@ class FavoriteTests(TestCase):
|
|
|
657
1402
|
def test_dashboard_merges_duplicate_future_actions(self):
|
|
658
1403
|
ct = ContentType.objects.get_for_model(NodeRole)
|
|
659
1404
|
Favorite.objects.create(user=self.user, content_type=ct)
|
|
660
|
-
|
|
661
|
-
UserDatum.objects.create(user=self.user, content_type=ct, object_id=role.pk)
|
|
1405
|
+
NodeRole.objects.create(name="DataRole2", is_user_data=True)
|
|
662
1406
|
AdminHistory.objects.create(
|
|
663
1407
|
user=self.user,
|
|
664
1408
|
content_type=ct,
|
|
@@ -668,3 +1412,399 @@ class FavoriteTests(TestCase):
|
|
|
668
1412
|
url = reverse("admin:nodes_noderole_changelist")
|
|
669
1413
|
self.assertGreaterEqual(resp.content.decode().count(url), 1)
|
|
670
1414
|
self.assertContains(resp, NodeRole._meta.verbose_name_plural)
|
|
1415
|
+
|
|
1416
|
+
def test_dashboard_limits_future_actions_to_top_four(self):
|
|
1417
|
+
from pages.templatetags.admin_extras import future_action_items
|
|
1418
|
+
|
|
1419
|
+
role_ct = ContentType.objects.get_for_model(NodeRole)
|
|
1420
|
+
role_url = reverse("admin:nodes_noderole_changelist")
|
|
1421
|
+
AdminHistory.objects.create(
|
|
1422
|
+
user=self.user,
|
|
1423
|
+
content_type=role_ct,
|
|
1424
|
+
url=role_url,
|
|
1425
|
+
)
|
|
1426
|
+
AdminHistory.objects.create(
|
|
1427
|
+
user=self.user,
|
|
1428
|
+
content_type=role_ct,
|
|
1429
|
+
url=f"{role_url}?page=2",
|
|
1430
|
+
)
|
|
1431
|
+
AdminHistory.objects.create(
|
|
1432
|
+
user=self.user,
|
|
1433
|
+
content_type=role_ct,
|
|
1434
|
+
url=f"{role_url}?page=3",
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
app_ct = ContentType.objects.get_for_model(Application)
|
|
1438
|
+
app_url = reverse("admin:pages_application_changelist")
|
|
1439
|
+
AdminHistory.objects.create(
|
|
1440
|
+
user=self.user,
|
|
1441
|
+
content_type=app_ct,
|
|
1442
|
+
url=app_url,
|
|
1443
|
+
)
|
|
1444
|
+
AdminHistory.objects.create(
|
|
1445
|
+
user=self.user,
|
|
1446
|
+
content_type=app_ct,
|
|
1447
|
+
url=f"{app_url}?page=2",
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
module_ct = ContentType.objects.get_for_model(Module)
|
|
1451
|
+
module_url = reverse("admin:pages_module_changelist")
|
|
1452
|
+
AdminHistory.objects.create(
|
|
1453
|
+
user=self.user,
|
|
1454
|
+
content_type=module_ct,
|
|
1455
|
+
url=module_url,
|
|
1456
|
+
)
|
|
1457
|
+
AdminHistory.objects.create(
|
|
1458
|
+
user=self.user,
|
|
1459
|
+
content_type=module_ct,
|
|
1460
|
+
url=f"{module_url}?page=2",
|
|
1461
|
+
)
|
|
1462
|
+
|
|
1463
|
+
package_ct = ContentType.objects.get_for_model(Package)
|
|
1464
|
+
package_url = reverse("admin:core_package_changelist")
|
|
1465
|
+
AdminHistory.objects.create(
|
|
1466
|
+
user=self.user,
|
|
1467
|
+
content_type=package_ct,
|
|
1468
|
+
url=package_url,
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
view_history_ct = ContentType.objects.get_for_model(ViewHistory)
|
|
1472
|
+
view_history_url = reverse("admin:pages_viewhistory_changelist")
|
|
1473
|
+
AdminHistory.objects.create(
|
|
1474
|
+
user=self.user,
|
|
1475
|
+
content_type=view_history_ct,
|
|
1476
|
+
url=view_history_url,
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1480
|
+
items = future_action_items({"request": resp.wsgi_request})["models"]
|
|
1481
|
+
labels = {item["label"] for item in items}
|
|
1482
|
+
self.assertEqual(len(items), 4)
|
|
1483
|
+
self.assertIn("Node Roles", labels)
|
|
1484
|
+
self.assertIn("Modules", labels)
|
|
1485
|
+
self.assertIn("applications", labels)
|
|
1486
|
+
self.assertIn("View Histories", labels)
|
|
1487
|
+
self.assertNotIn("Packages", labels)
|
|
1488
|
+
ContentType.objects.clear_cache()
|
|
1489
|
+
|
|
1490
|
+
def test_favorite_ct_id_recreates_missing_content_type(self):
|
|
1491
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
1492
|
+
ct.delete()
|
|
1493
|
+
from pages.templatetags.favorites import favorite_ct_id
|
|
1494
|
+
|
|
1495
|
+
new_id = favorite_ct_id("pages", "Application")
|
|
1496
|
+
self.assertIsNotNone(new_id)
|
|
1497
|
+
self.assertTrue(
|
|
1498
|
+
ContentType.objects.filter(
|
|
1499
|
+
pk=new_id, app_label="pages", model="application"
|
|
1500
|
+
).exists()
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
def test_dashboard_uses_change_label(self):
|
|
1504
|
+
ct = ContentType.objects.get_by_natural_key("pages", "application")
|
|
1505
|
+
Favorite.objects.create(user=self.user, content_type=ct)
|
|
1506
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1507
|
+
self.assertContains(resp, "Change Applications")
|
|
1508
|
+
self.assertContains(resp, 'target="_blank" rel="noopener noreferrer"')
|
|
1509
|
+
|
|
1510
|
+
def test_dashboard_links_to_focus_view(self):
|
|
1511
|
+
todo = Todo.objects.create(request="Check docs", url="/docs/")
|
|
1512
|
+
resp = self.client.get(reverse("admin:index"))
|
|
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
|
+
)
|
|
1519
|
+
|
|
1520
|
+
def test_dashboard_shows_todo_with_done_button(self):
|
|
1521
|
+
todo = Todo.objects.create(request="Do thing")
|
|
1522
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1523
|
+
done_url = reverse("todo-done", args=[todo.pk])
|
|
1524
|
+
self.assertContains(resp, todo.request)
|
|
1525
|
+
self.assertContains(resp, f'action="{done_url}"')
|
|
1526
|
+
self.assertContains(resp, "DONE")
|
|
1527
|
+
|
|
1528
|
+
def test_dashboard_shows_request_details(self):
|
|
1529
|
+
Todo.objects.create(request="Do thing", request_details="More info")
|
|
1530
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1531
|
+
self.assertContains(
|
|
1532
|
+
resp, '<div class="todo-details">More info</div>', html=True
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
def test_dashboard_excludes_todo_changelist_link(self):
|
|
1536
|
+
ct = ContentType.objects.get_for_model(Todo)
|
|
1537
|
+
Favorite.objects.create(user=self.user, content_type=ct)
|
|
1538
|
+
AdminHistory.objects.create(
|
|
1539
|
+
user=self.user,
|
|
1540
|
+
content_type=ct,
|
|
1541
|
+
url=reverse("admin:core_todo_changelist"),
|
|
1542
|
+
)
|
|
1543
|
+
Todo.objects.create(request="Task", is_user_data=True)
|
|
1544
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1545
|
+
changelist = reverse("admin:core_todo_changelist")
|
|
1546
|
+
self.assertNotContains(resp, f'href="{changelist}"')
|
|
1547
|
+
|
|
1548
|
+
def test_dashboard_hides_todos_without_release_manager(self):
|
|
1549
|
+
todo = Todo.objects.create(request="Only Release Manager")
|
|
1550
|
+
User = get_user_model()
|
|
1551
|
+
other_user = User.objects.create_superuser(
|
|
1552
|
+
username="norole", password="pwd", email="norole@example.com"
|
|
1553
|
+
)
|
|
1554
|
+
self.client.force_login(other_user)
|
|
1555
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1556
|
+
self.assertNotContains(resp, "Release manager tasks")
|
|
1557
|
+
self.assertNotContains(resp, todo.request)
|
|
1558
|
+
|
|
1559
|
+
def test_dashboard_hides_todos_for_non_terminal_node(self):
|
|
1560
|
+
todo = Todo.objects.create(request="Terminal Tasks")
|
|
1561
|
+
from nodes.models import NodeRole
|
|
1562
|
+
|
|
1563
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
1564
|
+
self.node.role = control_role
|
|
1565
|
+
self.node.save(update_fields=["role"])
|
|
1566
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1567
|
+
self.assertNotContains(resp, "Release manager tasks")
|
|
1568
|
+
self.assertNotContains(resp, todo.request)
|
|
1569
|
+
|
|
1570
|
+
def test_dashboard_shows_todos_for_delegate_release_manager(self):
|
|
1571
|
+
todo = Todo.objects.create(request="Delegate Task")
|
|
1572
|
+
User = get_user_model()
|
|
1573
|
+
delegate = User.objects.create_superuser(
|
|
1574
|
+
username="delegate",
|
|
1575
|
+
password="pwd",
|
|
1576
|
+
email="delegate@example.com",
|
|
1577
|
+
)
|
|
1578
|
+
ReleaseManager.objects.create(user=delegate)
|
|
1579
|
+
operator = User.objects.create_superuser(
|
|
1580
|
+
username="operator",
|
|
1581
|
+
password="pwd",
|
|
1582
|
+
email="operator@example.com",
|
|
1583
|
+
)
|
|
1584
|
+
operator.operate_as = delegate
|
|
1585
|
+
operator.full_clean()
|
|
1586
|
+
operator.save()
|
|
1587
|
+
self.client.force_login(operator)
|
|
1588
|
+
resp = self.client.get(reverse("admin:index"))
|
|
1589
|
+
self.assertContains(resp, "Release manager tasks")
|
|
1590
|
+
self.assertContains(resp, todo.request)
|
|
1591
|
+
|
|
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
|
+
|
|
1624
|
+
class AdminModelGraphViewTests(TestCase):
|
|
1625
|
+
def setUp(self):
|
|
1626
|
+
self.client = Client()
|
|
1627
|
+
User = get_user_model()
|
|
1628
|
+
self.user = User.objects.create_user(
|
|
1629
|
+
username="graph-staff", password="pwd", is_staff=True
|
|
1630
|
+
)
|
|
1631
|
+
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
1632
|
+
self.client.force_login(self.user)
|
|
1633
|
+
|
|
1634
|
+
def _mock_graph(self):
|
|
1635
|
+
fake_graph = Mock()
|
|
1636
|
+
fake_graph.source = "digraph {}"
|
|
1637
|
+
fake_graph.engine = "dot"
|
|
1638
|
+
|
|
1639
|
+
def pipe_side_effect(*args, **kwargs):
|
|
1640
|
+
fmt = kwargs.get("format") or (args[0] if args else None)
|
|
1641
|
+
if fmt == "svg":
|
|
1642
|
+
return '<svg xmlns="http://www.w3.org/2000/svg"></svg>'
|
|
1643
|
+
if fmt == "pdf":
|
|
1644
|
+
return b"%PDF-1.4 mock"
|
|
1645
|
+
raise AssertionError(f"Unexpected format: {fmt}")
|
|
1646
|
+
|
|
1647
|
+
fake_graph.pipe.side_effect = pipe_side_effect
|
|
1648
|
+
return fake_graph
|
|
1649
|
+
|
|
1650
|
+
def test_model_graph_renders_controls_and_download_link(self):
|
|
1651
|
+
url = reverse("admin-model-graph", args=["pages"])
|
|
1652
|
+
graph = self._mock_graph()
|
|
1653
|
+
with (
|
|
1654
|
+
patch("pages.views._build_model_graph", return_value=graph),
|
|
1655
|
+
patch("pages.views.shutil.which", return_value="/usr/bin/dot"),
|
|
1656
|
+
):
|
|
1657
|
+
response = self.client.get(url)
|
|
1658
|
+
|
|
1659
|
+
self.assertEqual(response.status_code, 200)
|
|
1660
|
+
self.assertContains(response, "data-model-graph")
|
|
1661
|
+
self.assertContains(response, 'data-graph-action="zoom-in"')
|
|
1662
|
+
self.assertContains(response, "Download PDF")
|
|
1663
|
+
self.assertIn("?format=pdf", response.context_data["download_url"])
|
|
1664
|
+
args, kwargs = graph.pipe.call_args
|
|
1665
|
+
self.assertEqual(kwargs.get("format"), "svg")
|
|
1666
|
+
self.assertEqual(kwargs.get("encoding"), "utf-8")
|
|
1667
|
+
|
|
1668
|
+
def test_model_graph_pdf_download(self):
|
|
1669
|
+
url = reverse("admin-model-graph", args=["pages"])
|
|
1670
|
+
graph = self._mock_graph()
|
|
1671
|
+
with (
|
|
1672
|
+
patch("pages.views._build_model_graph", return_value=graph),
|
|
1673
|
+
patch("pages.views.shutil.which", return_value="/usr/bin/dot"),
|
|
1674
|
+
):
|
|
1675
|
+
response = self.client.get(url, {"format": "pdf"})
|
|
1676
|
+
|
|
1677
|
+
self.assertEqual(response.status_code, 200)
|
|
1678
|
+
self.assertEqual(response["Content-Type"], "application/pdf")
|
|
1679
|
+
app_config = django_apps.get_app_config("pages")
|
|
1680
|
+
expected_slug = slugify(app_config.verbose_name) or app_config.label
|
|
1681
|
+
self.assertIn(
|
|
1682
|
+
f"{expected_slug}-model-graph.pdf", response["Content-Disposition"]
|
|
1683
|
+
)
|
|
1684
|
+
self.assertEqual(response.content, b"%PDF-1.4 mock")
|
|
1685
|
+
args, kwargs = graph.pipe.call_args
|
|
1686
|
+
self.assertEqual(kwargs.get("format"), "pdf")
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
class DatasetteTests(TestCase):
|
|
1690
|
+
def setUp(self):
|
|
1691
|
+
self.client = Client()
|
|
1692
|
+
User = get_user_model()
|
|
1693
|
+
self.user = User.objects.create_user(username="ds", password="pwd")
|
|
1694
|
+
Site.objects.update_or_create(id=1, defaults={"name": "Terminal"})
|
|
1695
|
+
|
|
1696
|
+
def test_datasette_auth_endpoint(self):
|
|
1697
|
+
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
1698
|
+
self.assertEqual(resp.status_code, 401)
|
|
1699
|
+
self.client.force_login(self.user)
|
|
1700
|
+
resp = self.client.get(reverse("pages:datasette-auth"))
|
|
1701
|
+
self.assertEqual(resp.status_code, 200)
|
|
1702
|
+
|
|
1703
|
+
def test_navbar_includes_datasette_when_enabled(self):
|
|
1704
|
+
lock_dir = Path(settings.BASE_DIR) / "locks"
|
|
1705
|
+
lock_dir.mkdir(exist_ok=True)
|
|
1706
|
+
lock_file = lock_dir / "datasette.lck"
|
|
1707
|
+
try:
|
|
1708
|
+
lock_file.touch()
|
|
1709
|
+
resp = self.client.get(reverse("pages:index"))
|
|
1710
|
+
self.assertContains(resp, 'href="/data/"')
|
|
1711
|
+
finally:
|
|
1712
|
+
lock_file.unlink(missing_ok=True)
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
class ClientReportLiveUpdateTests(TestCase):
|
|
1716
|
+
def setUp(self):
|
|
1717
|
+
self.client = Client()
|
|
1718
|
+
|
|
1719
|
+
def test_client_report_includes_interval(self):
|
|
1720
|
+
resp = self.client.get(reverse("pages:client-report"))
|
|
1721
|
+
self.assertEqual(resp.context["request"].live_update_interval, 5)
|
|
1722
|
+
self.assertContains(resp, "setInterval(() => location.reload()")
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
class ScreenshotSpecInfrastructureTests(TestCase):
|
|
1726
|
+
def test_runner_creates_outputs_and_cleans_old_samples(self):
|
|
1727
|
+
spec = ScreenshotSpec(slug="spec-test", url="/")
|
|
1728
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1729
|
+
temp_dir = Path(tmp)
|
|
1730
|
+
screenshot_path = temp_dir / "source.png"
|
|
1731
|
+
screenshot_path.write_bytes(b"fake")
|
|
1732
|
+
ContentSample.objects.create(
|
|
1733
|
+
kind=ContentSample.IMAGE,
|
|
1734
|
+
path="old.png",
|
|
1735
|
+
method="spec:old",
|
|
1736
|
+
hash="old-hash",
|
|
1737
|
+
)
|
|
1738
|
+
ContentSample.objects.filter(hash="old-hash").update(
|
|
1739
|
+
created_at=timezone.now() - timedelta(days=8)
|
|
1740
|
+
)
|
|
1741
|
+
with (
|
|
1742
|
+
patch(
|
|
1743
|
+
"pages.screenshot_specs.base.capture_screenshot",
|
|
1744
|
+
return_value=screenshot_path,
|
|
1745
|
+
) as capture_mock,
|
|
1746
|
+
patch(
|
|
1747
|
+
"pages.screenshot_specs.base.save_screenshot", return_value=None
|
|
1748
|
+
) as save_mock,
|
|
1749
|
+
):
|
|
1750
|
+
with ScreenshotSpecRunner(temp_dir) as runner:
|
|
1751
|
+
result = runner.run(spec)
|
|
1752
|
+
self.assertTrue(result.image_path.exists())
|
|
1753
|
+
self.assertTrue(result.base64_path.exists())
|
|
1754
|
+
self.assertEqual(ContentSample.objects.filter(hash="old-hash").count(), 0)
|
|
1755
|
+
capture_mock.assert_called_once()
|
|
1756
|
+
save_mock.assert_called_once_with(screenshot_path, method="spec:spec-test")
|
|
1757
|
+
|
|
1758
|
+
def test_runner_respects_manual_reason(self):
|
|
1759
|
+
spec = ScreenshotSpec(slug="manual-spec", url="/", manual_reason="hardware")
|
|
1760
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1761
|
+
with ScreenshotSpecRunner(Path(tmp)) as runner:
|
|
1762
|
+
with self.assertRaises(ScreenshotUnavailable):
|
|
1763
|
+
runner.run(spec)
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
class CaptureUIScreenshotsCommandTests(TestCase):
|
|
1767
|
+
def tearDown(self):
|
|
1768
|
+
registry.unregister("manual-cmd")
|
|
1769
|
+
registry.unregister("auto-cmd")
|
|
1770
|
+
|
|
1771
|
+
def test_manual_spec_emits_warning(self):
|
|
1772
|
+
spec = ScreenshotSpec(slug="manual-cmd", url="/", manual_reason="manual")
|
|
1773
|
+
registry.register(spec)
|
|
1774
|
+
out = StringIO()
|
|
1775
|
+
call_command("capture_ui_screenshots", "--spec", spec.slug, stdout=out)
|
|
1776
|
+
self.assertIn("Skipping manual screenshot", out.getvalue())
|
|
1777
|
+
|
|
1778
|
+
def test_command_invokes_runner(self):
|
|
1779
|
+
spec = ScreenshotSpec(slug="auto-cmd", url="/")
|
|
1780
|
+
registry.register(spec)
|
|
1781
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
1782
|
+
tmp_path = Path(tmp)
|
|
1783
|
+
image_path = tmp_path / "auto-cmd.png"
|
|
1784
|
+
base64_path = tmp_path / "auto-cmd.base64"
|
|
1785
|
+
image_path.write_bytes(b"fake")
|
|
1786
|
+
base64_path.write_text("Zg==", encoding="utf-8")
|
|
1787
|
+
runner = Mock()
|
|
1788
|
+
runner.__enter__ = Mock(return_value=runner)
|
|
1789
|
+
runner.__exit__ = Mock(return_value=None)
|
|
1790
|
+
runner.run.return_value = SimpleNamespace(
|
|
1791
|
+
image_path=image_path,
|
|
1792
|
+
base64_path=base64_path,
|
|
1793
|
+
sample=None,
|
|
1794
|
+
)
|
|
1795
|
+
with patch(
|
|
1796
|
+
"pages.management.commands.capture_ui_screenshots.ScreenshotSpecRunner",
|
|
1797
|
+
return_value=runner,
|
|
1798
|
+
) as runner_cls:
|
|
1799
|
+
out = StringIO()
|
|
1800
|
+
call_command(
|
|
1801
|
+
"capture_ui_screenshots",
|
|
1802
|
+
"--spec",
|
|
1803
|
+
spec.slug,
|
|
1804
|
+
"--output-dir",
|
|
1805
|
+
tmp_path,
|
|
1806
|
+
stdout=out,
|
|
1807
|
+
)
|
|
1808
|
+
runner_cls.assert_called_once()
|
|
1809
|
+
runner.run.assert_called_once_with(spec)
|
|
1810
|
+
self.assertIn("Captured 'auto-cmd'", out.getvalue())
|