arthexis 0.1.7__py3-none-any.whl → 0.1.9__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.9.dist-info/METADATA +168 -0
- arthexis-0.1.9.dist-info/RECORD +92 -0
- arthexis-0.1.9.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 +134 -16
- config/urls.py +71 -3
- core/admin.py +1331 -165
- core/admin_history.py +50 -0
- core/admindocs.py +151 -0
- core/apps.py +158 -3
- core/backends.py +46 -4
- core/entity.py +62 -48
- core/fields.py +6 -1
- 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 +1136 -259
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/release.py +27 -20
- core/sigil_builder.py +131 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +129 -10
- core/tasks.py +118 -19
- core/test_system_info.py +22 -0
- core/tests.py +445 -58
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +329 -167
- core/views.py +383 -57
- core/widgets.py +51 -0
- core/workgroup_urls.py +17 -0
- core/workgroup_views.py +94 -0
- nodes/actions.py +0 -2
- nodes/admin.py +159 -284
- nodes/apps.py +9 -15
- nodes/backends.py +53 -0
- nodes/lcd.py +24 -10
- nodes/models.py +375 -178
- nodes/tasks.py +1 -5
- nodes/tests.py +524 -129
- nodes/utils.py +13 -2
- nodes/views.py +66 -23
- ocpp/admin.py +150 -61
- ocpp/apps.py +4 -3
- ocpp/consumers.py +432 -69
- ocpp/evcs.py +25 -8
- ocpp/models.py +408 -68
- ocpp/simulator.py +13 -6
- ocpp/store.py +258 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1198 -135
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +654 -101
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +19 -6
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +759 -40
- pages/urls.py +3 -0
- pages/utils.py +0 -1
- pages/views.py +576 -25
- arthexis-0.1.7.dist-info/METADATA +0 -126
- arthexis-0.1.7.dist-info/RECORD +0 -77
- arthexis-0.1.7.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.7.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
- {arthexis-0.1.7.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
nodes/tests.py
CHANGED
|
@@ -6,13 +6,17 @@ import django
|
|
|
6
6
|
django.setup()
|
|
7
7
|
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
from types import SimpleNamespace
|
|
9
10
|
from unittest.mock import patch, call, MagicMock
|
|
11
|
+
from django.core import mail
|
|
12
|
+
from django.core.management import call_command
|
|
10
13
|
import socket
|
|
11
14
|
import base64
|
|
12
15
|
import json
|
|
13
16
|
import uuid
|
|
14
17
|
from tempfile import TemporaryDirectory
|
|
15
18
|
import shutil
|
|
19
|
+
import stat
|
|
16
20
|
import time
|
|
17
21
|
|
|
18
22
|
from django.test import Client, TestCase, TransactionTestCase, override_settings
|
|
@@ -31,23 +35,22 @@ from .models import (
|
|
|
31
35
|
EmailOutbox,
|
|
32
36
|
ContentSample,
|
|
33
37
|
NodeRole,
|
|
38
|
+
NodeFeature,
|
|
39
|
+
NodeFeatureAssignment,
|
|
34
40
|
NetMessage,
|
|
35
41
|
)
|
|
42
|
+
from .backends import OutboxEmailBackend
|
|
36
43
|
from .tasks import capture_node_screenshot, sample_clipboard
|
|
37
44
|
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
|
38
45
|
from cryptography.hazmat.primitives import serialization, hashes
|
|
39
46
|
from core.models import PackageRelease
|
|
40
|
-
from .models import Operation
|
|
41
|
-
from .admin import RUN_CONTEXTS
|
|
42
47
|
|
|
43
48
|
|
|
44
49
|
class NodeTests(TestCase):
|
|
45
50
|
def setUp(self):
|
|
46
51
|
self.client = Client()
|
|
47
52
|
User = get_user_model()
|
|
48
|
-
self.user = User.objects.create_user(
|
|
49
|
-
username="nodeuser", password="pwd"
|
|
50
|
-
)
|
|
53
|
+
self.user = User.objects.create_user(username="nodeuser", password="pwd")
|
|
51
54
|
self.client.force_login(self.user)
|
|
52
55
|
NodeRole.objects.get_or_create(name="Terminal")
|
|
53
56
|
|
|
@@ -55,16 +58,18 @@ class NodeTests(TestCase):
|
|
|
55
58
|
with TemporaryDirectory() as tmp:
|
|
56
59
|
base = Path(tmp)
|
|
57
60
|
with override_settings(BASE_DIR=base):
|
|
58
|
-
with
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
with (
|
|
62
|
+
patch(
|
|
63
|
+
"nodes.models.Node.get_current_mac",
|
|
64
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
65
|
+
),
|
|
66
|
+
patch("nodes.models.socket.gethostname", return_value="testhost"),
|
|
67
|
+
patch(
|
|
68
|
+
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
69
|
+
),
|
|
70
|
+
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
71
|
+
patch.object(Node, "ensure_keys"),
|
|
72
|
+
):
|
|
68
73
|
Node.register_current()
|
|
69
74
|
self.assertEqual(PackageRelease.objects.count(), 0)
|
|
70
75
|
|
|
@@ -117,7 +122,10 @@ class NodeTests(TestCase):
|
|
|
117
122
|
hostnames = {n["hostname"] for n in data["nodes"]}
|
|
118
123
|
self.assertEqual(hostnames, {"dup", "local2"})
|
|
119
124
|
|
|
120
|
-
def
|
|
125
|
+
def test_register_node_feature_toggle(self):
|
|
126
|
+
NodeFeature.objects.get_or_create(
|
|
127
|
+
slug="clipboard-poll", defaults={"display": "Clipboard Poll"}
|
|
128
|
+
)
|
|
121
129
|
url = reverse("register-node")
|
|
122
130
|
first = self.client.post(
|
|
123
131
|
url,
|
|
@@ -126,13 +134,13 @@ class NodeTests(TestCase):
|
|
|
126
134
|
"address": "127.0.0.1",
|
|
127
135
|
"port": 8000,
|
|
128
136
|
"mac_address": "00:aa:bb:cc:dd:ee",
|
|
129
|
-
"
|
|
137
|
+
"features": ["clipboard-poll"],
|
|
130
138
|
},
|
|
131
139
|
content_type="application/json",
|
|
132
140
|
)
|
|
133
141
|
self.assertEqual(first.status_code, 200)
|
|
134
142
|
node = Node.objects.get(mac_address="00:aa:bb:cc:dd:ee")
|
|
135
|
-
self.assertTrue(node.
|
|
143
|
+
self.assertTrue(node.has_feature("clipboard-poll"))
|
|
136
144
|
|
|
137
145
|
self.client.post(
|
|
138
146
|
url,
|
|
@@ -141,12 +149,44 @@ class NodeTests(TestCase):
|
|
|
141
149
|
"address": "127.0.0.1",
|
|
142
150
|
"port": 8000,
|
|
143
151
|
"mac_address": "00:aa:bb:cc:dd:ee",
|
|
144
|
-
"
|
|
152
|
+
"features": [],
|
|
145
153
|
},
|
|
146
154
|
content_type="application/json",
|
|
147
155
|
)
|
|
148
156
|
node.refresh_from_db()
|
|
149
|
-
self.assertFalse(node.
|
|
157
|
+
self.assertFalse(node.has_feature("clipboard-poll"))
|
|
158
|
+
|
|
159
|
+
def test_register_node_sets_cors_headers(self):
|
|
160
|
+
payload = {
|
|
161
|
+
"hostname": "cors",
|
|
162
|
+
"address": "127.0.0.1",
|
|
163
|
+
"port": 8000,
|
|
164
|
+
"mac_address": "10:20:30:40:50:60",
|
|
165
|
+
}
|
|
166
|
+
response = self.client.post(
|
|
167
|
+
reverse("register-node"),
|
|
168
|
+
data=json.dumps(payload),
|
|
169
|
+
content_type="application/json",
|
|
170
|
+
HTTP_ORIGIN="http://example.com",
|
|
171
|
+
)
|
|
172
|
+
self.assertEqual(response.status_code, 200)
|
|
173
|
+
self.assertEqual(response["Access-Control-Allow-Origin"], "http://example.com")
|
|
174
|
+
self.assertEqual(response["Access-Control-Allow-Credentials"], "true")
|
|
175
|
+
|
|
176
|
+
def test_register_node_accepts_text_plain_payload(self):
|
|
177
|
+
payload = {
|
|
178
|
+
"hostname": "plain",
|
|
179
|
+
"address": "127.0.0.1",
|
|
180
|
+
"port": 8001,
|
|
181
|
+
"mac_address": "aa:bb:cc:dd:ee:ff",
|
|
182
|
+
}
|
|
183
|
+
response = self.client.post(
|
|
184
|
+
reverse("register-node"),
|
|
185
|
+
data=json.dumps(payload),
|
|
186
|
+
content_type="text/plain",
|
|
187
|
+
)
|
|
188
|
+
self.assertEqual(response.status_code, 200)
|
|
189
|
+
self.assertTrue(Node.objects.filter(mac_address="aa:bb:cc:dd:ee:ff").exists())
|
|
150
190
|
|
|
151
191
|
|
|
152
192
|
class NodeRegisterCurrentTests(TestCase):
|
|
@@ -156,39 +196,71 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
156
196
|
self.user = User.objects.create_user(username="nodeuser", password="pwd")
|
|
157
197
|
self.client.force_login(self.user)
|
|
158
198
|
NodeRole.objects.get_or_create(name="Terminal")
|
|
159
|
-
|
|
199
|
+
|
|
200
|
+
def test_register_current_refreshes_lcd_feature(self):
|
|
201
|
+
NodeFeature.objects.get_or_create(
|
|
202
|
+
slug="lcd-screen", defaults={"display": "LCD Screen"}
|
|
203
|
+
)
|
|
160
204
|
with TemporaryDirectory() as tmp:
|
|
161
205
|
base = Path(tmp)
|
|
162
206
|
locks = base / "locks"
|
|
163
207
|
locks.mkdir()
|
|
164
|
-
|
|
208
|
+
lock = locks / "lcd_screen.lck"
|
|
209
|
+
lock.touch()
|
|
165
210
|
with override_settings(BASE_DIR=base):
|
|
166
|
-
with
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"nodes.models.
|
|
172
|
-
|
|
211
|
+
with (
|
|
212
|
+
patch(
|
|
213
|
+
"nodes.models.Node.get_current_mac",
|
|
214
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
215
|
+
),
|
|
216
|
+
patch("nodes.models.socket.gethostname", return_value="testhost"),
|
|
217
|
+
patch(
|
|
218
|
+
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
219
|
+
),
|
|
220
|
+
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
221
|
+
patch.object(Node, "ensure_keys"),
|
|
222
|
+
):
|
|
173
223
|
node, created = Node.register_current()
|
|
174
224
|
self.assertTrue(created)
|
|
175
|
-
self.assertTrue(node.
|
|
176
|
-
|
|
177
|
-
node.has_lcd_screen = False
|
|
178
|
-
node.save(update_fields=["has_lcd_screen"])
|
|
225
|
+
self.assertTrue(node.has_feature("lcd-screen"))
|
|
179
226
|
|
|
227
|
+
lock.unlink()
|
|
180
228
|
with override_settings(BASE_DIR=base):
|
|
181
|
-
with
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"nodes.models.
|
|
187
|
-
|
|
188
|
-
|
|
229
|
+
with (
|
|
230
|
+
patch(
|
|
231
|
+
"nodes.models.Node.get_current_mac",
|
|
232
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
233
|
+
),
|
|
234
|
+
patch("nodes.models.socket.gethostname", return_value="testhost"),
|
|
235
|
+
patch(
|
|
236
|
+
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
237
|
+
),
|
|
238
|
+
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
239
|
+
patch.object(Node, "ensure_keys"),
|
|
240
|
+
):
|
|
241
|
+
_, created2 = Node.register_current()
|
|
189
242
|
self.assertFalse(created2)
|
|
190
243
|
node.refresh_from_db()
|
|
191
|
-
self.assertFalse(node.
|
|
244
|
+
self.assertFalse(node.has_feature("lcd-screen"))
|
|
245
|
+
|
|
246
|
+
lock.touch()
|
|
247
|
+
with override_settings(BASE_DIR=base):
|
|
248
|
+
with (
|
|
249
|
+
patch(
|
|
250
|
+
"nodes.models.Node.get_current_mac",
|
|
251
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
252
|
+
),
|
|
253
|
+
patch("nodes.models.socket.gethostname", return_value="testhost"),
|
|
254
|
+
patch(
|
|
255
|
+
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
256
|
+
),
|
|
257
|
+
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
258
|
+
patch.object(Node, "ensure_keys"),
|
|
259
|
+
):
|
|
260
|
+
node, created3 = Node.register_current()
|
|
261
|
+
self.assertFalse(created3)
|
|
262
|
+
node.refresh_from_db()
|
|
263
|
+
self.assertTrue(node.has_feature("lcd-screen"))
|
|
192
264
|
|
|
193
265
|
@patch("nodes.views.capture_screenshot")
|
|
194
266
|
def test_capture_screenshot(self, mock_capture):
|
|
@@ -270,9 +342,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
270
342
|
self.assertEqual(get_resp.json()["hostname"], "public")
|
|
271
343
|
|
|
272
344
|
pre_count = NetMessage.objects.count()
|
|
273
|
-
post_resp = self.client.post(
|
|
274
|
-
url, data="hello", content_type="text/plain"
|
|
275
|
-
)
|
|
345
|
+
post_resp = self.client.post(url, data="hello", content_type="text/plain")
|
|
276
346
|
self.assertEqual(post_resp.status_code, 200)
|
|
277
347
|
self.assertEqual(NetMessage.objects.count(), pre_count + 1)
|
|
278
348
|
msg = NetMessage.objects.order_by("-created").first()
|
|
@@ -307,10 +377,14 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
307
377
|
|
|
308
378
|
def test_net_message_with_valid_signature(self):
|
|
309
379
|
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
310
|
-
public_key =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
380
|
+
public_key = (
|
|
381
|
+
key.public_key()
|
|
382
|
+
.public_bytes(
|
|
383
|
+
encoding=serialization.Encoding.PEM,
|
|
384
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
385
|
+
)
|
|
386
|
+
.decode()
|
|
387
|
+
)
|
|
314
388
|
sender = Node.objects.create(
|
|
315
389
|
hostname="sender",
|
|
316
390
|
address="10.0.0.1",
|
|
@@ -327,9 +401,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
327
401
|
"sender": str(sender.uuid),
|
|
328
402
|
}
|
|
329
403
|
payload_json = json.dumps(payload, separators=(",", ":"), sort_keys=True)
|
|
330
|
-
signature = key.sign(
|
|
331
|
-
payload_json.encode(), padding.PKCS1v15(), hashes.SHA256()
|
|
332
|
-
)
|
|
404
|
+
signature = key.sign(payload_json.encode(), padding.PKCS1v15(), hashes.SHA256())
|
|
333
405
|
resp = self.client.post(
|
|
334
406
|
reverse("net-message"),
|
|
335
407
|
data=payload_json,
|
|
@@ -340,6 +412,9 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
340
412
|
self.assertTrue(NetMessage.objects.filter(uuid=msg_id).exists())
|
|
341
413
|
|
|
342
414
|
def test_clipboard_polling_creates_task(self):
|
|
415
|
+
feature, _ = NodeFeature.objects.get_or_create(
|
|
416
|
+
slug="clipboard-poll", defaults={"display": "Clipboard Poll"}
|
|
417
|
+
)
|
|
343
418
|
node = Node.objects.create(
|
|
344
419
|
hostname="clip",
|
|
345
420
|
address="127.0.0.1",
|
|
@@ -347,15 +422,16 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
347
422
|
mac_address="00:11:22:33:44:99",
|
|
348
423
|
)
|
|
349
424
|
task_name = f"poll_clipboard_node_{node.pk}"
|
|
350
|
-
|
|
351
|
-
node
|
|
352
|
-
node.save()
|
|
425
|
+
PeriodicTask.objects.filter(name=task_name).delete()
|
|
426
|
+
NodeFeatureAssignment.objects.create(node=node, feature=feature)
|
|
353
427
|
self.assertTrue(PeriodicTask.objects.filter(name=task_name).exists())
|
|
354
|
-
node
|
|
355
|
-
node.save()
|
|
428
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=feature).delete()
|
|
356
429
|
self.assertFalse(PeriodicTask.objects.filter(name=task_name).exists())
|
|
357
430
|
|
|
358
431
|
def test_screenshot_polling_creates_task(self):
|
|
432
|
+
feature, _ = NodeFeature.objects.get_or_create(
|
|
433
|
+
slug="screenshot-poll", defaults={"display": "Screenshot Poll"}
|
|
434
|
+
)
|
|
359
435
|
node = Node.objects.create(
|
|
360
436
|
hostname="shot",
|
|
361
437
|
address="127.0.0.1",
|
|
@@ -363,14 +439,13 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
363
439
|
mac_address="00:11:22:33:44:aa",
|
|
364
440
|
)
|
|
365
441
|
task_name = f"capture_screenshot_node_{node.pk}"
|
|
366
|
-
|
|
367
|
-
node
|
|
368
|
-
node.save()
|
|
442
|
+
PeriodicTask.objects.filter(name=task_name).delete()
|
|
443
|
+
NodeFeatureAssignment.objects.create(node=node, feature=feature)
|
|
369
444
|
self.assertTrue(PeriodicTask.objects.filter(name=task_name).exists())
|
|
370
|
-
node
|
|
371
|
-
node.save()
|
|
445
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=feature).delete()
|
|
372
446
|
self.assertFalse(PeriodicTask.objects.filter(name=task_name).exists())
|
|
373
447
|
|
|
448
|
+
|
|
374
449
|
class NodeAdminTests(TestCase):
|
|
375
450
|
|
|
376
451
|
def setUp(self):
|
|
@@ -395,7 +470,7 @@ class NodeAdminTests(TestCase):
|
|
|
395
470
|
self.assertTemplateUsed(response, "admin/nodes/node/register_remote.html")
|
|
396
471
|
self.assertEqual(Node.objects.count(), 1)
|
|
397
472
|
node = Node.objects.first()
|
|
398
|
-
ver = Path(
|
|
473
|
+
ver = Path("VERSION").read_text().strip()
|
|
399
474
|
rev = "abcdef123456"
|
|
400
475
|
self.assertEqual(node.base_path, str(settings.BASE_DIR))
|
|
401
476
|
self.assertEqual(node.installed_version, ver)
|
|
@@ -408,9 +483,7 @@ class NodeAdminTests(TestCase):
|
|
|
408
483
|
self.assertTrue(priv.exists())
|
|
409
484
|
self.assertTrue(pub.exists())
|
|
410
485
|
self.assertTrue(node.public_key)
|
|
411
|
-
self.assertTrue(
|
|
412
|
-
Site.objects.filter(domain=hostname, name="host").exists()
|
|
413
|
-
)
|
|
486
|
+
self.assertTrue(Site.objects.filter(domain=hostname, name="host").exists())
|
|
414
487
|
|
|
415
488
|
def test_register_current_updates_existing_node(self):
|
|
416
489
|
hostname = socket.gethostname()
|
|
@@ -446,9 +519,7 @@ class NodeAdminTests(TestCase):
|
|
|
446
519
|
self.assertIn(node.public_key.strip(), resp.content.decode())
|
|
447
520
|
|
|
448
521
|
@patch("nodes.admin.capture_screenshot")
|
|
449
|
-
def test_capture_site_screenshot_from_admin(
|
|
450
|
-
self, mock_capture_screenshot
|
|
451
|
-
):
|
|
522
|
+
def test_capture_site_screenshot_from_admin(self, mock_capture_screenshot):
|
|
452
523
|
screenshot_dir = settings.LOG_DIR / "screenshots"
|
|
453
524
|
screenshot_dir.mkdir(parents=True, exist_ok=True)
|
|
454
525
|
file_path = screenshot_dir / "test.png"
|
|
@@ -472,9 +543,7 @@ class NodeAdminTests(TestCase):
|
|
|
472
543
|
self.assertEqual(screenshot.path, "screenshots/test.png")
|
|
473
544
|
self.assertEqual(screenshot.method, "ADMIN")
|
|
474
545
|
mock_capture_screenshot.assert_called_once_with("http://testserver/")
|
|
475
|
-
self.assertContains(
|
|
476
|
-
response, "Screenshot saved to screenshots/test.png"
|
|
477
|
-
)
|
|
546
|
+
self.assertContains(response, "Screenshot saved to screenshots/test.png")
|
|
478
547
|
|
|
479
548
|
def test_view_screenshot_in_change_admin(self):
|
|
480
549
|
screenshot_dir = settings.LOG_DIR / "screenshots"
|
|
@@ -578,7 +647,9 @@ class NetMessageReachTests(TestCase):
|
|
|
578
647
|
for name in ["Terminal", "Control", "Satellite", "Constellation"]:
|
|
579
648
|
self.roles[name], _ = NodeRole.objects.get_or_create(name=name)
|
|
580
649
|
self.nodes = {}
|
|
581
|
-
for idx, name in enumerate(
|
|
650
|
+
for idx, name in enumerate(
|
|
651
|
+
["Terminal", "Control", "Satellite", "Constellation"], start=1
|
|
652
|
+
):
|
|
582
653
|
self.nodes[name] = Node.objects.create(
|
|
583
654
|
hostname=name.lower(),
|
|
584
655
|
address=f"10.0.0.{idx}",
|
|
@@ -589,7 +660,9 @@ class NetMessageReachTests(TestCase):
|
|
|
589
660
|
|
|
590
661
|
@patch("requests.post")
|
|
591
662
|
def test_terminal_reach_limits_nodes(self, mock_post):
|
|
592
|
-
msg = NetMessage.objects.create(
|
|
663
|
+
msg = NetMessage.objects.create(
|
|
664
|
+
subject="s", body="b", reach=self.roles["Terminal"]
|
|
665
|
+
)
|
|
593
666
|
with patch.object(Node, "get_local", return_value=None):
|
|
594
667
|
msg.propagate()
|
|
595
668
|
roles = set(msg.propagated_to.values_list("role__name", flat=True))
|
|
@@ -598,7 +671,9 @@ class NetMessageReachTests(TestCase):
|
|
|
598
671
|
|
|
599
672
|
@patch("requests.post")
|
|
600
673
|
def test_control_reach_includes_control_and_terminal(self, mock_post):
|
|
601
|
-
msg = NetMessage.objects.create(
|
|
674
|
+
msg = NetMessage.objects.create(
|
|
675
|
+
subject="s", body="b", reach=self.roles["Control"]
|
|
676
|
+
)
|
|
602
677
|
with patch.object(Node, "get_local", return_value=None):
|
|
603
678
|
msg.propagate()
|
|
604
679
|
roles = set(msg.propagated_to.values_list("role__name", flat=True))
|
|
@@ -607,7 +682,9 @@ class NetMessageReachTests(TestCase):
|
|
|
607
682
|
|
|
608
683
|
@patch("requests.post")
|
|
609
684
|
def test_satellite_reach_includes_lower_roles(self, mock_post):
|
|
610
|
-
msg = NetMessage.objects.create(
|
|
685
|
+
msg = NetMessage.objects.create(
|
|
686
|
+
subject="s", body="b", reach=self.roles["Satellite"]
|
|
687
|
+
)
|
|
611
688
|
with patch.object(Node, "get_local", return_value=None):
|
|
612
689
|
msg.propagate()
|
|
613
690
|
roles = set(msg.propagated_to.values_list("role__name", flat=True))
|
|
@@ -616,7 +693,9 @@ class NetMessageReachTests(TestCase):
|
|
|
616
693
|
|
|
617
694
|
@patch("requests.post")
|
|
618
695
|
def test_constellation_reach_prioritizes_constellation(self, mock_post):
|
|
619
|
-
msg = NetMessage.objects.create(
|
|
696
|
+
msg = NetMessage.objects.create(
|
|
697
|
+
subject="s", body="b", reach=self.roles["Constellation"]
|
|
698
|
+
)
|
|
620
699
|
with patch.object(Node, "get_local", return_value=None):
|
|
621
700
|
msg.propagate()
|
|
622
701
|
roles = set(msg.propagated_to.values_list("role__name", flat=True))
|
|
@@ -650,7 +729,9 @@ class NetMessagePropagationTests(TestCase):
|
|
|
650
729
|
|
|
651
730
|
@patch("requests.post")
|
|
652
731
|
@patch("core.notifications.notify")
|
|
653
|
-
def test_propagate_forwards_to_three_and_notifies_local(
|
|
732
|
+
def test_propagate_forwards_to_three_and_notifies_local(
|
|
733
|
+
self, mock_notify, mock_post
|
|
734
|
+
):
|
|
654
735
|
msg = NetMessage.objects.create(subject="s", body="b", reach=self.role)
|
|
655
736
|
with patch.object(Node, "get_local", return_value=self.local):
|
|
656
737
|
msg.propagate(seen=[str(self.remotes[0].uuid)])
|
|
@@ -665,6 +746,7 @@ class NetMessagePropagationTests(TestCase):
|
|
|
665
746
|
self.assertEqual(msg.propagated_to.count(), 4)
|
|
666
747
|
self.assertTrue(msg.complete)
|
|
667
748
|
|
|
749
|
+
|
|
668
750
|
class NodeActionTests(TestCase):
|
|
669
751
|
def setUp(self):
|
|
670
752
|
self.client = Client()
|
|
@@ -739,7 +821,7 @@ class NodeActionTests(TestCase):
|
|
|
739
821
|
|
|
740
822
|
|
|
741
823
|
class StartupNotificationTests(TestCase):
|
|
742
|
-
def
|
|
824
|
+
def test_startup_notification_uses_hostname_and_revision(self):
|
|
743
825
|
from nodes.apps import _startup_notification
|
|
744
826
|
|
|
745
827
|
with TemporaryDirectory() as tmp:
|
|
@@ -750,20 +832,17 @@ class StartupNotificationTests(TestCase):
|
|
|
750
832
|
"nodes.apps.revision.get_revision", return_value="abcdef123456"
|
|
751
833
|
):
|
|
752
834
|
with patch("nodes.models.NetMessage.broadcast") as mock_broadcast:
|
|
753
|
-
with patch(
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
):
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
):
|
|
760
|
-
_startup_notification()
|
|
761
|
-
time.sleep(0.1)
|
|
835
|
+
with patch(
|
|
836
|
+
"nodes.apps.socket.gethostname", return_value="host"
|
|
837
|
+
):
|
|
838
|
+
with patch.dict(os.environ, {"PORT": "9000"}):
|
|
839
|
+
_startup_notification()
|
|
840
|
+
time.sleep(0.1)
|
|
762
841
|
|
|
763
842
|
mock_broadcast.assert_called_once()
|
|
764
843
|
_, kwargs = mock_broadcast.call_args
|
|
765
|
-
self.assertEqual(kwargs["subject"], "
|
|
766
|
-
self.assertTrue(kwargs["body"].startswith("
|
|
844
|
+
self.assertEqual(kwargs["subject"], "host:9000")
|
|
845
|
+
self.assertTrue(kwargs["body"].startswith("1.2.3 r"))
|
|
767
846
|
|
|
768
847
|
|
|
769
848
|
class StartupHandlerTests(TestCase):
|
|
@@ -787,11 +866,14 @@ class StartupHandlerTests(TestCase):
|
|
|
787
866
|
|
|
788
867
|
with patch("nodes.apps._startup_notification") as mock_start:
|
|
789
868
|
with patch("nodes.apps.connections") as mock_connections:
|
|
790
|
-
mock_connections.__getitem__.return_value.ensure_connection.return_value =
|
|
869
|
+
mock_connections.__getitem__.return_value.ensure_connection.return_value = (
|
|
870
|
+
None
|
|
871
|
+
)
|
|
791
872
|
_trigger_startup_notification()
|
|
792
873
|
|
|
793
874
|
mock_start.assert_called_once()
|
|
794
875
|
|
|
876
|
+
|
|
795
877
|
class NotificationManagerTests(TestCase):
|
|
796
878
|
def test_send_writes_trimmed_lines(self):
|
|
797
879
|
from core.notifications import NotificationManager
|
|
@@ -921,6 +1003,7 @@ class ContentSampleAdminTests(TestCase):
|
|
|
921
1003
|
self.assertContains(resp, "Duplicate sample not created")
|
|
922
1004
|
|
|
923
1005
|
|
|
1006
|
+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
|
|
924
1007
|
class EmailOutboxTests(TestCase):
|
|
925
1008
|
def test_node_send_mail_uses_outbox(self):
|
|
926
1009
|
node = Node.objects.create(
|
|
@@ -929,31 +1012,28 @@ class EmailOutboxTests(TestCase):
|
|
|
929
1012
|
port=8000,
|
|
930
1013
|
mac_address="00:11:22:33:aa:bb",
|
|
931
1014
|
)
|
|
932
|
-
EmailOutbox.objects.create(
|
|
1015
|
+
outbox = EmailOutbox.objects.create(
|
|
933
1016
|
node=node, host="smtp.example.com", port=25, username="u", password="p"
|
|
934
1017
|
)
|
|
935
|
-
with patch("nodes.models.
|
|
936
|
-
"nodes.models.send_mail"
|
|
937
|
-
) as sm:
|
|
938
|
-
conn = MagicMock()
|
|
939
|
-
gc.return_value = conn
|
|
1018
|
+
with patch("nodes.models.mailer.send") as ms:
|
|
940
1019
|
node.send_mail("sub", "msg", ["to@example.com"])
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
port=25,
|
|
944
|
-
username="u",
|
|
945
|
-
password="p",
|
|
946
|
-
use_tls=True,
|
|
947
|
-
use_ssl=False,
|
|
948
|
-
)
|
|
949
|
-
sm.assert_called_once_with(
|
|
950
|
-
"sub",
|
|
951
|
-
"msg",
|
|
952
|
-
settings.DEFAULT_FROM_EMAIL,
|
|
953
|
-
["to@example.com"],
|
|
954
|
-
connection=conn,
|
|
1020
|
+
ms.assert_called_once_with(
|
|
1021
|
+
"sub", "msg", ["to@example.com"], None, outbox=outbox
|
|
955
1022
|
)
|
|
956
1023
|
|
|
1024
|
+
def test_node_send_mail_queues_email(self):
|
|
1025
|
+
node = Node.objects.create(
|
|
1026
|
+
hostname="host",
|
|
1027
|
+
address="127.0.0.1",
|
|
1028
|
+
port=8000,
|
|
1029
|
+
mac_address="00:11:22:33:cc:dd",
|
|
1030
|
+
)
|
|
1031
|
+
node.send_mail("sub", "msg", ["to@example.com"])
|
|
1032
|
+
self.assertEqual(len(mail.outbox), 1)
|
|
1033
|
+
email = mail.outbox[0]
|
|
1034
|
+
self.assertEqual(email.subject, "sub")
|
|
1035
|
+
self.assertEqual(email.to, ["to@example.com"])
|
|
1036
|
+
|
|
957
1037
|
|
|
958
1038
|
class ClipboardTaskTests(TestCase):
|
|
959
1039
|
@patch("nodes.tasks.pyperclip.paste")
|
|
@@ -1066,29 +1146,344 @@ class NodeRoleAdminTests(TestCase):
|
|
|
1066
1146
|
self.assertIsNone(node1.role)
|
|
1067
1147
|
self.assertEqual(node2.role, role)
|
|
1068
1148
|
|
|
1149
|
+
def test_registered_count_displayed(self):
|
|
1150
|
+
role = NodeRole.objects.create(name="ViewRole")
|
|
1151
|
+
Node.objects.create(
|
|
1152
|
+
hostname="n1",
|
|
1153
|
+
address="127.0.0.1",
|
|
1154
|
+
port=8000,
|
|
1155
|
+
mac_address="00:11:22:33:44:77",
|
|
1156
|
+
role=role,
|
|
1157
|
+
)
|
|
1158
|
+
resp = self.client.get(reverse("admin:nodes_noderole_changelist"))
|
|
1159
|
+
self.assertContains(resp, '<td class="field-registered">1</td>', html=True)
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
class NodeFeatureFixtureTests(TestCase):
|
|
1163
|
+
def test_rfid_scanner_fixture_includes_control_role(self):
|
|
1164
|
+
for name in ("Terminal", "Satellite", "Constellation", "Control"):
|
|
1165
|
+
NodeRole.objects.get_or_create(name=name)
|
|
1166
|
+
fixture_path = (
|
|
1167
|
+
Path(__file__).resolve().parent
|
|
1168
|
+
/ "fixtures"
|
|
1169
|
+
/ "node_features__nodefeature_rfid_scanner.json"
|
|
1170
|
+
)
|
|
1171
|
+
call_command("loaddata", str(fixture_path), verbosity=0)
|
|
1172
|
+
feature = NodeFeature.objects.get(slug="rfid-scanner")
|
|
1173
|
+
role_names = set(feature.roles.values_list("name", flat=True))
|
|
1174
|
+
self.assertIn("Control", role_names)
|
|
1175
|
+
|
|
1176
|
+
def test_ap_router_fixture_limits_roles(self):
|
|
1177
|
+
for name in ("Control", "Satellite"):
|
|
1178
|
+
NodeRole.objects.get_or_create(name=name)
|
|
1179
|
+
fixture_path = (
|
|
1180
|
+
Path(__file__).resolve().parent
|
|
1181
|
+
/ "fixtures"
|
|
1182
|
+
/ "node_features__nodefeature_ap_router.json"
|
|
1183
|
+
)
|
|
1184
|
+
call_command("loaddata", str(fixture_path), verbosity=0)
|
|
1185
|
+
feature = NodeFeature.objects.get(slug="ap-router")
|
|
1186
|
+
role_names = set(feature.roles.values_list("name", flat=True))
|
|
1187
|
+
self.assertEqual(role_names, {"Satellite"})
|
|
1069
1188
|
|
|
1070
|
-
|
|
1189
|
+
|
|
1190
|
+
class NodeFeatureTests(TestCase):
|
|
1071
1191
|
def setUp(self):
|
|
1072
|
-
self.
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1192
|
+
self.role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
1193
|
+
with patch(
|
|
1194
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1195
|
+
):
|
|
1196
|
+
self.node = Node.objects.create(
|
|
1197
|
+
hostname="local",
|
|
1198
|
+
address="127.0.0.1",
|
|
1199
|
+
port=8000,
|
|
1200
|
+
mac_address="00:11:22:33:44:55",
|
|
1201
|
+
role=self.role,
|
|
1202
|
+
)
|
|
1203
|
+
|
|
1204
|
+
def test_lcd_screen_enabled(self):
|
|
1205
|
+
feature = NodeFeature.objects.create(slug="lcd-screen", display="LCD")
|
|
1206
|
+
feature.roles.add(self.role)
|
|
1207
|
+
NodeFeatureAssignment.objects.create(node=self.node, feature=feature)
|
|
1208
|
+
with patch(
|
|
1209
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1210
|
+
):
|
|
1211
|
+
self.assertTrue(feature.is_enabled)
|
|
1212
|
+
NodeFeatureAssignment.objects.filter(node=self.node, feature=feature).delete()
|
|
1213
|
+
with patch(
|
|
1214
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1215
|
+
):
|
|
1216
|
+
self.assertFalse(feature.is_enabled)
|
|
1217
|
+
|
|
1218
|
+
def test_rfid_scanner_lock(self):
|
|
1219
|
+
feature = NodeFeature.objects.create(slug="rfid-scanner", display="RFID")
|
|
1220
|
+
feature.roles.add(self.role)
|
|
1221
|
+
with TemporaryDirectory() as tmp:
|
|
1222
|
+
base = Path(tmp)
|
|
1223
|
+
locks = base / "locks"
|
|
1224
|
+
locks.mkdir()
|
|
1225
|
+
with override_settings(BASE_DIR=base):
|
|
1226
|
+
with patch(
|
|
1227
|
+
"nodes.models.Node.get_current_mac",
|
|
1228
|
+
return_value="00:11:22:33:44:55",
|
|
1229
|
+
):
|
|
1230
|
+
self.assertFalse(feature.is_enabled)
|
|
1231
|
+
(locks / "rfid.lck").touch()
|
|
1232
|
+
self.assertTrue(feature.is_enabled)
|
|
1233
|
+
|
|
1234
|
+
def test_gui_toast_detection(self):
|
|
1235
|
+
feature = NodeFeature.objects.create(slug="gui-toast", display="GUI Toast")
|
|
1236
|
+
feature.roles.add(self.role)
|
|
1237
|
+
with patch(
|
|
1238
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1239
|
+
):
|
|
1240
|
+
with patch("core.notifications.supports_gui_toast", return_value=True):
|
|
1241
|
+
self.assertTrue(feature.is_enabled)
|
|
1242
|
+
with patch(
|
|
1243
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1244
|
+
):
|
|
1245
|
+
with patch("core.notifications.supports_gui_toast", return_value=False):
|
|
1246
|
+
self.assertFalse(feature.is_enabled)
|
|
1247
|
+
|
|
1248
|
+
def test_role_membership_alone_does_not_enable_feature(self):
|
|
1249
|
+
feature = NodeFeature.objects.create(
|
|
1250
|
+
slug="custom-feature", display="Custom Feature"
|
|
1251
|
+
)
|
|
1252
|
+
feature.roles.add(self.role)
|
|
1253
|
+
with patch(
|
|
1254
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1255
|
+
):
|
|
1256
|
+
self.assertFalse(feature.is_enabled)
|
|
1257
|
+
NodeFeatureAssignment.objects.create(node=self.node, feature=feature)
|
|
1258
|
+
with patch(
|
|
1259
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1260
|
+
):
|
|
1261
|
+
self.assertTrue(feature.is_enabled)
|
|
1262
|
+
|
|
1263
|
+
@patch("nodes.models.Node._has_rpi_camera", return_value=True)
|
|
1264
|
+
def test_rpi_camera_detection(self, mock_camera):
|
|
1265
|
+
feature = NodeFeature.objects.create(
|
|
1266
|
+
slug="rpi-camera", display="Raspberry Pi Camera"
|
|
1267
|
+
)
|
|
1268
|
+
feature.roles.add(self.role)
|
|
1269
|
+
with patch(
|
|
1270
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1271
|
+
):
|
|
1272
|
+
self.node.refresh_features()
|
|
1273
|
+
self.assertTrue(
|
|
1274
|
+
NodeFeatureAssignment.objects.filter(
|
|
1275
|
+
node=self.node, feature=feature
|
|
1276
|
+
).exists()
|
|
1076
1277
|
)
|
|
1077
|
-
self.client.force_login(self.user)
|
|
1078
1278
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1279
|
+
@patch("nodes.models.Node._has_rpi_camera", side_effect=[True, False])
|
|
1280
|
+
def test_rpi_camera_removed_when_unavailable(self, mock_camera):
|
|
1281
|
+
feature = NodeFeature.objects.create(
|
|
1282
|
+
slug="rpi-camera", display="Raspberry Pi Camera"
|
|
1283
|
+
)
|
|
1284
|
+
feature.roles.add(self.role)
|
|
1285
|
+
with patch(
|
|
1286
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1287
|
+
):
|
|
1288
|
+
self.node.refresh_features()
|
|
1289
|
+
self.assertTrue(
|
|
1290
|
+
NodeFeatureAssignment.objects.filter(
|
|
1291
|
+
node=self.node, feature=feature
|
|
1292
|
+
).exists()
|
|
1293
|
+
)
|
|
1294
|
+
self.node.refresh_features()
|
|
1295
|
+
self.assertFalse(
|
|
1296
|
+
NodeFeatureAssignment.objects.filter(
|
|
1297
|
+
node=self.node, feature=feature
|
|
1298
|
+
).exists()
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
@patch("nodes.models.Node._hosts_gelectriic_ap", return_value=True)
|
|
1302
|
+
def test_ap_router_detection(self, mock_hosts):
|
|
1303
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
1304
|
+
feature = NodeFeature.objects.create(slug="ap-router", display="AP Router")
|
|
1305
|
+
feature.roles.add(control_role)
|
|
1306
|
+
mac = "00:11:22:33:44:66"
|
|
1307
|
+
with patch("nodes.models.Node.get_current_mac", return_value=mac):
|
|
1308
|
+
node = Node.objects.create(
|
|
1309
|
+
hostname="control",
|
|
1310
|
+
address="127.0.0.1",
|
|
1311
|
+
port=8000,
|
|
1312
|
+
mac_address=mac,
|
|
1313
|
+
role=control_role,
|
|
1314
|
+
)
|
|
1315
|
+
node.refresh_features()
|
|
1316
|
+
self.assertTrue(
|
|
1317
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=feature).exists()
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
@patch("nodes.models.Node._hosts_gelectriic_ap", return_value=True)
|
|
1321
|
+
def test_ap_public_wifi_detection(self, mock_hosts):
|
|
1322
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
1323
|
+
router = NodeFeature.objects.create(slug="ap-router", display="AP Router")
|
|
1324
|
+
router.roles.add(control_role)
|
|
1325
|
+
public = NodeFeature.objects.create(
|
|
1326
|
+
slug="ap-public-wifi", display="AP Public Wi-Fi"
|
|
1327
|
+
)
|
|
1328
|
+
public.roles.add(control_role)
|
|
1329
|
+
mac = "00:11:22:33:44:88"
|
|
1330
|
+
with TemporaryDirectory() as tmp, override_settings(BASE_DIR=Path(tmp)):
|
|
1331
|
+
locks = Path(tmp) / "locks"
|
|
1332
|
+
locks.mkdir(parents=True, exist_ok=True)
|
|
1333
|
+
(locks / "public_wifi_mode.lck").touch()
|
|
1334
|
+
with patch("nodes.models.Node.get_current_mac", return_value=mac):
|
|
1335
|
+
node = Node.objects.create(
|
|
1336
|
+
hostname="control",
|
|
1337
|
+
address="127.0.0.1",
|
|
1338
|
+
port=8000,
|
|
1339
|
+
mac_address=mac,
|
|
1340
|
+
role=control_role,
|
|
1341
|
+
base_path=str(Path(tmp)),
|
|
1342
|
+
)
|
|
1343
|
+
node.refresh_features()
|
|
1344
|
+
self.assertTrue(
|
|
1345
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=public).exists()
|
|
1346
|
+
)
|
|
1347
|
+
self.assertFalse(
|
|
1348
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=router).exists()
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
@patch("nodes.models.Node._hosts_gelectriic_ap", side_effect=[True, False])
|
|
1352
|
+
def test_ap_router_removed_when_not_hosting(self, mock_hosts):
|
|
1353
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
1354
|
+
feature = NodeFeature.objects.create(slug="ap-router", display="AP Router")
|
|
1355
|
+
feature.roles.add(control_role)
|
|
1356
|
+
mac = "00:11:22:33:44:77"
|
|
1357
|
+
with patch("nodes.models.Node.get_current_mac", return_value=mac):
|
|
1358
|
+
node = Node.objects.create(
|
|
1359
|
+
hostname="control",
|
|
1360
|
+
address="127.0.0.1",
|
|
1361
|
+
port=8000,
|
|
1362
|
+
mac_address=mac,
|
|
1363
|
+
role=control_role,
|
|
1364
|
+
)
|
|
1365
|
+
self.assertTrue(
|
|
1366
|
+
NodeFeatureAssignment.objects.filter(
|
|
1367
|
+
node=node, feature=feature
|
|
1368
|
+
).exists()
|
|
1369
|
+
)
|
|
1370
|
+
node.refresh_features()
|
|
1371
|
+
self.assertFalse(
|
|
1372
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=feature).exists()
|
|
1373
|
+
)
|
|
1374
|
+
|
|
1375
|
+
@patch("nodes.models.Node._uses_postgres", return_value=True)
|
|
1376
|
+
def test_postgres_detection(self, mock_postgres):
|
|
1377
|
+
feature = NodeFeature.objects.create(
|
|
1378
|
+
slug="postgres-db", display="PostgreSQL Database"
|
|
1379
|
+
)
|
|
1380
|
+
feature.roles.add(self.role)
|
|
1381
|
+
with patch(
|
|
1382
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1383
|
+
):
|
|
1384
|
+
self.node.refresh_features()
|
|
1385
|
+
self.assertTrue(
|
|
1386
|
+
NodeFeatureAssignment.objects.filter(
|
|
1387
|
+
node=self.node, feature=feature
|
|
1388
|
+
).exists()
|
|
1389
|
+
)
|
|
1081
1390
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1391
|
+
@patch("nodes.models.Node._uses_postgres", side_effect=[True, False])
|
|
1392
|
+
def test_postgres_removed_when_not_in_use(self, mock_postgres):
|
|
1393
|
+
feature = NodeFeature.objects.create(
|
|
1394
|
+
slug="postgres-db", display="PostgreSQL Database"
|
|
1395
|
+
)
|
|
1396
|
+
feature.roles.add(self.role)
|
|
1397
|
+
with patch(
|
|
1398
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1399
|
+
):
|
|
1400
|
+
self.node.refresh_features()
|
|
1401
|
+
self.assertTrue(
|
|
1402
|
+
NodeFeatureAssignment.objects.filter(
|
|
1403
|
+
node=self.node, feature=feature
|
|
1404
|
+
).exists()
|
|
1405
|
+
)
|
|
1406
|
+
with patch(
|
|
1407
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1408
|
+
):
|
|
1409
|
+
self.node.refresh_features()
|
|
1410
|
+
self.assertFalse(
|
|
1411
|
+
NodeFeatureAssignment.objects.filter(
|
|
1412
|
+
node=self.node, feature=feature
|
|
1413
|
+
).exists()
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
|
|
1417
|
+
class NodeRpiCameraDetectionTests(TestCase):
|
|
1418
|
+
@patch("nodes.models.subprocess.run")
|
|
1419
|
+
@patch("nodes.models.shutil.which")
|
|
1420
|
+
@patch("nodes.models.os.access")
|
|
1421
|
+
@patch("nodes.models.os.stat")
|
|
1422
|
+
@patch("nodes.models.Path.exists")
|
|
1423
|
+
def test_has_rpi_camera_true(
|
|
1424
|
+
self,
|
|
1425
|
+
mock_exists,
|
|
1426
|
+
mock_stat,
|
|
1427
|
+
mock_access,
|
|
1428
|
+
mock_which,
|
|
1429
|
+
mock_run,
|
|
1430
|
+
):
|
|
1431
|
+
mock_exists.return_value = True
|
|
1432
|
+
mock_stat.return_value = SimpleNamespace(st_mode=stat.S_IFCHR)
|
|
1433
|
+
mock_access.return_value = True
|
|
1434
|
+
mock_which.side_effect = lambda name: f"/usr/bin/{name}"
|
|
1435
|
+
mock_run.return_value = SimpleNamespace(returncode=0)
|
|
1436
|
+
|
|
1437
|
+
self.assertTrue(Node._has_rpi_camera())
|
|
1438
|
+
self.assertEqual(mock_which.call_count, len(Node.RPI_CAMERA_BINARIES))
|
|
1439
|
+
self.assertEqual(mock_run.call_count, len(Node.RPI_CAMERA_BINARIES))
|
|
1440
|
+
|
|
1441
|
+
@patch("nodes.models.subprocess.run")
|
|
1442
|
+
@patch("nodes.models.shutil.which")
|
|
1443
|
+
@patch("nodes.models.os.access")
|
|
1444
|
+
@patch("nodes.models.os.stat")
|
|
1445
|
+
@patch("nodes.models.Path.exists")
|
|
1446
|
+
def test_has_rpi_camera_missing_device(
|
|
1447
|
+
self,
|
|
1448
|
+
mock_exists,
|
|
1449
|
+
mock_stat,
|
|
1450
|
+
mock_access,
|
|
1451
|
+
mock_which,
|
|
1452
|
+
mock_run,
|
|
1453
|
+
):
|
|
1454
|
+
mock_exists.return_value = False
|
|
1455
|
+
|
|
1456
|
+
self.assertFalse(Node._has_rpi_camera())
|
|
1457
|
+
mock_stat.assert_not_called()
|
|
1458
|
+
mock_access.assert_not_called()
|
|
1459
|
+
mock_which.assert_not_called()
|
|
1460
|
+
mock_run.assert_not_called()
|
|
1461
|
+
|
|
1462
|
+
@patch("nodes.models.subprocess.run")
|
|
1463
|
+
@patch("nodes.models.shutil.which")
|
|
1464
|
+
@patch("nodes.models.os.access")
|
|
1465
|
+
@patch("nodes.models.os.stat")
|
|
1466
|
+
@patch("nodes.models.Path.exists")
|
|
1467
|
+
def test_has_rpi_camera_missing_tool(
|
|
1468
|
+
self,
|
|
1469
|
+
mock_exists,
|
|
1470
|
+
mock_stat,
|
|
1471
|
+
mock_access,
|
|
1472
|
+
mock_which,
|
|
1473
|
+
mock_run,
|
|
1474
|
+
):
|
|
1475
|
+
mock_exists.return_value = True
|
|
1476
|
+
mock_stat.return_value = SimpleNamespace(st_mode=stat.S_IFCHR)
|
|
1477
|
+
mock_access.return_value = True
|
|
1478
|
+
mock_run.return_value = SimpleNamespace(returncode=0)
|
|
1087
1479
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
self.assertContains(resp, 'value="Continue"')
|
|
1480
|
+
def tool_lookup(name):
|
|
1481
|
+
if name == Node.RPI_CAMERA_BINARIES[-1]:
|
|
1482
|
+
return None
|
|
1483
|
+
return f"/usr/bin/{name}"
|
|
1093
1484
|
|
|
1485
|
+
mock_which.side_effect = tool_lookup
|
|
1094
1486
|
|
|
1487
|
+
self.assertFalse(Node._has_rpi_camera())
|
|
1488
|
+
missing_index = Node.RPI_CAMERA_BINARIES.index(Node.RPI_CAMERA_BINARIES[-1])
|
|
1489
|
+
self.assertEqual(mock_run.call_count, missing_index)
|