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
nodes/tests.py
CHANGED
|
@@ -6,14 +6,20 @@ 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.mail import EmailMessage
|
|
13
|
+
from django.core.management import call_command
|
|
10
14
|
import socket
|
|
11
15
|
import base64
|
|
12
16
|
import json
|
|
13
17
|
import uuid
|
|
14
18
|
from tempfile import TemporaryDirectory
|
|
15
19
|
import shutil
|
|
20
|
+
import stat
|
|
16
21
|
import time
|
|
22
|
+
from datetime import timedelta
|
|
17
23
|
|
|
18
24
|
from django.test import Client, TestCase, TransactionTestCase, override_settings
|
|
19
25
|
from django.urls import reverse
|
|
@@ -22,6 +28,7 @@ from django.contrib import admin
|
|
|
22
28
|
from django.contrib.sites.models import Site
|
|
23
29
|
from django_celery_beat.models import PeriodicTask
|
|
24
30
|
from django.conf import settings
|
|
31
|
+
from django.utils import timezone
|
|
25
32
|
from .actions import NodeAction
|
|
26
33
|
from selenium.common.exceptions import WebDriverException
|
|
27
34
|
from .utils import capture_screenshot
|
|
@@ -31,23 +38,22 @@ from .models import (
|
|
|
31
38
|
EmailOutbox,
|
|
32
39
|
ContentSample,
|
|
33
40
|
NodeRole,
|
|
41
|
+
NodeFeature,
|
|
42
|
+
NodeFeatureAssignment,
|
|
34
43
|
NetMessage,
|
|
35
44
|
)
|
|
45
|
+
from .backends import OutboxEmailBackend
|
|
36
46
|
from .tasks import capture_node_screenshot, sample_clipboard
|
|
37
47
|
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
|
38
48
|
from cryptography.hazmat.primitives import serialization, hashes
|
|
39
|
-
from core.models import PackageRelease
|
|
40
|
-
from .models import Operation
|
|
41
|
-
from .admin import RUN_CONTEXTS
|
|
49
|
+
from core.models import PackageRelease, SecurityGroup
|
|
42
50
|
|
|
43
51
|
|
|
44
52
|
class NodeTests(TestCase):
|
|
45
53
|
def setUp(self):
|
|
46
54
|
self.client = Client()
|
|
47
55
|
User = get_user_model()
|
|
48
|
-
self.user = User.objects.create_user(
|
|
49
|
-
username="nodeuser", password="pwd"
|
|
50
|
-
)
|
|
56
|
+
self.user = User.objects.create_user(username="nodeuser", password="pwd")
|
|
51
57
|
self.client.force_login(self.user)
|
|
52
58
|
NodeRole.objects.get_or_create(name="Terminal")
|
|
53
59
|
|
|
@@ -55,16 +61,18 @@ class NodeTests(TestCase):
|
|
|
55
61
|
with TemporaryDirectory() as tmp:
|
|
56
62
|
base = Path(tmp)
|
|
57
63
|
with override_settings(BASE_DIR=base):
|
|
58
|
-
with
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
with (
|
|
65
|
+
patch(
|
|
66
|
+
"nodes.models.Node.get_current_mac",
|
|
67
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
68
|
+
),
|
|
69
|
+
patch("nodes.models.socket.gethostname", return_value="testhost"),
|
|
70
|
+
patch(
|
|
71
|
+
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
72
|
+
),
|
|
73
|
+
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
74
|
+
patch.object(Node, "ensure_keys"),
|
|
75
|
+
):
|
|
68
76
|
Node.register_current()
|
|
69
77
|
self.assertEqual(PackageRelease.objects.count(), 0)
|
|
70
78
|
|
|
@@ -117,7 +125,10 @@ class NodeTests(TestCase):
|
|
|
117
125
|
hostnames = {n["hostname"] for n in data["nodes"]}
|
|
118
126
|
self.assertEqual(hostnames, {"dup", "local2"})
|
|
119
127
|
|
|
120
|
-
def
|
|
128
|
+
def test_register_node_feature_toggle(self):
|
|
129
|
+
NodeFeature.objects.get_or_create(
|
|
130
|
+
slug="clipboard-poll", defaults={"display": "Clipboard Poll"}
|
|
131
|
+
)
|
|
121
132
|
url = reverse("register-node")
|
|
122
133
|
first = self.client.post(
|
|
123
134
|
url,
|
|
@@ -126,13 +137,13 @@ class NodeTests(TestCase):
|
|
|
126
137
|
"address": "127.0.0.1",
|
|
127
138
|
"port": 8000,
|
|
128
139
|
"mac_address": "00:aa:bb:cc:dd:ee",
|
|
129
|
-
"
|
|
140
|
+
"features": ["clipboard-poll"],
|
|
130
141
|
},
|
|
131
142
|
content_type="application/json",
|
|
132
143
|
)
|
|
133
144
|
self.assertEqual(first.status_code, 200)
|
|
134
145
|
node = Node.objects.get(mac_address="00:aa:bb:cc:dd:ee")
|
|
135
|
-
self.assertTrue(node.
|
|
146
|
+
self.assertTrue(node.has_feature("clipboard-poll"))
|
|
136
147
|
|
|
137
148
|
self.client.post(
|
|
138
149
|
url,
|
|
@@ -141,12 +152,224 @@ class NodeTests(TestCase):
|
|
|
141
152
|
"address": "127.0.0.1",
|
|
142
153
|
"port": 8000,
|
|
143
154
|
"mac_address": "00:aa:bb:cc:dd:ee",
|
|
144
|
-
"
|
|
155
|
+
"features": [],
|
|
145
156
|
},
|
|
146
157
|
content_type="application/json",
|
|
147
158
|
)
|
|
148
159
|
node.refresh_from_db()
|
|
149
|
-
self.assertFalse(node.
|
|
160
|
+
self.assertFalse(node.has_feature("clipboard-poll"))
|
|
161
|
+
|
|
162
|
+
def test_register_node_records_version_details(self):
|
|
163
|
+
url = reverse("register-node")
|
|
164
|
+
payload = {
|
|
165
|
+
"hostname": "versioned",
|
|
166
|
+
"address": "127.0.0.5",
|
|
167
|
+
"port": 8100,
|
|
168
|
+
"mac_address": "aa:bb:cc:dd:ee:10",
|
|
169
|
+
"installed_version": "2.0.1",
|
|
170
|
+
"installed_revision": "rev-abcdef",
|
|
171
|
+
}
|
|
172
|
+
response = self.client.post(
|
|
173
|
+
url, data=json.dumps(payload), content_type="application/json"
|
|
174
|
+
)
|
|
175
|
+
self.assertEqual(response.status_code, 200)
|
|
176
|
+
node = Node.objects.get(mac_address="aa:bb:cc:dd:ee:10")
|
|
177
|
+
self.assertEqual(node.installed_version, "2.0.1")
|
|
178
|
+
self.assertEqual(node.installed_revision, "rev-abcdef")
|
|
179
|
+
|
|
180
|
+
update_payload = {
|
|
181
|
+
**payload,
|
|
182
|
+
"installed_version": "2.1.0",
|
|
183
|
+
"installed_revision": "rev-fedcba",
|
|
184
|
+
}
|
|
185
|
+
second = self.client.post(
|
|
186
|
+
url, data=json.dumps(update_payload), content_type="application/json"
|
|
187
|
+
)
|
|
188
|
+
self.assertEqual(second.status_code, 200)
|
|
189
|
+
node.refresh_from_db()
|
|
190
|
+
self.assertEqual(node.installed_version, "2.1.0")
|
|
191
|
+
self.assertEqual(node.installed_revision, "rev-fedcba")
|
|
192
|
+
|
|
193
|
+
def test_register_node_update_triggers_notification(self):
|
|
194
|
+
node = Node.objects.create(
|
|
195
|
+
hostname="friend",
|
|
196
|
+
address="10.1.1.5",
|
|
197
|
+
port=8123,
|
|
198
|
+
mac_address="aa:bb:cc:dd:ee:01",
|
|
199
|
+
installed_version="1.0.0",
|
|
200
|
+
installed_revision="rev-old",
|
|
201
|
+
)
|
|
202
|
+
url = reverse("register-node")
|
|
203
|
+
payload = {
|
|
204
|
+
"hostname": "friend",
|
|
205
|
+
"address": "10.1.1.5",
|
|
206
|
+
"port": 8123,
|
|
207
|
+
"mac_address": "aa:bb:cc:dd:ee:01",
|
|
208
|
+
"installed_version": "2.0.0",
|
|
209
|
+
"installed_revision": "abcdef123456",
|
|
210
|
+
}
|
|
211
|
+
with patch("nodes.models.notify_async") as mock_notify:
|
|
212
|
+
response = self.client.post(
|
|
213
|
+
url, data=json.dumps(payload), content_type="application/json"
|
|
214
|
+
)
|
|
215
|
+
self.assertEqual(response.status_code, 200)
|
|
216
|
+
node.refresh_from_db()
|
|
217
|
+
self.assertEqual(node.installed_version, "2.0.0")
|
|
218
|
+
self.assertEqual(node.installed_revision, "abcdef123456")
|
|
219
|
+
mock_notify.assert_called_once()
|
|
220
|
+
subject, body = mock_notify.call_args[0]
|
|
221
|
+
self.assertEqual(subject, "UP friend")
|
|
222
|
+
self.assertEqual(body, "v2.0.0 r123456")
|
|
223
|
+
|
|
224
|
+
def test_register_node_update_without_version_change_still_notifies(self):
|
|
225
|
+
node = Node.objects.create(
|
|
226
|
+
hostname="friend",
|
|
227
|
+
address="10.1.1.5",
|
|
228
|
+
port=8123,
|
|
229
|
+
mac_address="aa:bb:cc:dd:ee:02",
|
|
230
|
+
installed_version="2.0.0",
|
|
231
|
+
installed_revision="abcdef123456",
|
|
232
|
+
)
|
|
233
|
+
url = reverse("register-node")
|
|
234
|
+
payload = {
|
|
235
|
+
"hostname": "friend",
|
|
236
|
+
"address": "10.1.1.5",
|
|
237
|
+
"port": 8123,
|
|
238
|
+
"mac_address": "aa:bb:cc:dd:ee:02",
|
|
239
|
+
"installed_version": "2.0.0",
|
|
240
|
+
"installed_revision": "abcdef123456",
|
|
241
|
+
}
|
|
242
|
+
with patch("nodes.models.notify_async") as mock_notify:
|
|
243
|
+
response = self.client.post(
|
|
244
|
+
url, data=json.dumps(payload), content_type="application/json"
|
|
245
|
+
)
|
|
246
|
+
self.assertEqual(response.status_code, 200)
|
|
247
|
+
node.refresh_from_db()
|
|
248
|
+
mock_notify.assert_called_once()
|
|
249
|
+
subject, body = mock_notify.call_args[0]
|
|
250
|
+
self.assertEqual(subject, "UP friend")
|
|
251
|
+
self.assertEqual(body, "v2.0.0 r123456")
|
|
252
|
+
|
|
253
|
+
def test_register_node_creation_triggers_notification(self):
|
|
254
|
+
url = reverse("register-node")
|
|
255
|
+
payload = {
|
|
256
|
+
"hostname": "newbie",
|
|
257
|
+
"address": "10.1.1.6",
|
|
258
|
+
"port": 8124,
|
|
259
|
+
"mac_address": "aa:bb:cc:dd:ee:03",
|
|
260
|
+
"installed_version": "3.0.0",
|
|
261
|
+
"installed_revision": "rev-1234567890",
|
|
262
|
+
}
|
|
263
|
+
with patch("nodes.models.notify_async") as mock_notify:
|
|
264
|
+
response = self.client.post(
|
|
265
|
+
url, data=json.dumps(payload), content_type="application/json"
|
|
266
|
+
)
|
|
267
|
+
self.assertEqual(response.status_code, 200)
|
|
268
|
+
self.assertTrue(Node.objects.filter(mac_address="aa:bb:cc:dd:ee:03").exists())
|
|
269
|
+
mock_notify.assert_called_once()
|
|
270
|
+
subject, body = mock_notify.call_args[0]
|
|
271
|
+
self.assertEqual(subject, "UP newbie")
|
|
272
|
+
self.assertEqual(body, "v3.0.0 r567890")
|
|
273
|
+
|
|
274
|
+
def test_register_node_sets_cors_headers(self):
|
|
275
|
+
payload = {
|
|
276
|
+
"hostname": "cors",
|
|
277
|
+
"address": "127.0.0.1",
|
|
278
|
+
"port": 8000,
|
|
279
|
+
"mac_address": "10:20:30:40:50:60",
|
|
280
|
+
}
|
|
281
|
+
response = self.client.post(
|
|
282
|
+
reverse("register-node"),
|
|
283
|
+
data=json.dumps(payload),
|
|
284
|
+
content_type="application/json",
|
|
285
|
+
HTTP_ORIGIN="http://example.com",
|
|
286
|
+
)
|
|
287
|
+
self.assertEqual(response.status_code, 200)
|
|
288
|
+
self.assertEqual(response["Access-Control-Allow-Origin"], "http://example.com")
|
|
289
|
+
self.assertEqual(response["Access-Control-Allow-Credentials"], "true")
|
|
290
|
+
|
|
291
|
+
def test_register_node_requires_auth_without_signature(self):
|
|
292
|
+
self.client.logout()
|
|
293
|
+
payload = {
|
|
294
|
+
"hostname": "visitor",
|
|
295
|
+
"address": "127.0.0.1",
|
|
296
|
+
"port": 8000,
|
|
297
|
+
"mac_address": "aa:bb:cc:dd:ee:00",
|
|
298
|
+
}
|
|
299
|
+
response = self.client.post(
|
|
300
|
+
reverse("register-node"),
|
|
301
|
+
data=json.dumps(payload),
|
|
302
|
+
content_type="application/json",
|
|
303
|
+
HTTP_ORIGIN="http://example.com",
|
|
304
|
+
)
|
|
305
|
+
self.assertEqual(response.status_code, 401)
|
|
306
|
+
data = response.json()
|
|
307
|
+
self.assertEqual(data["detail"], "authentication required")
|
|
308
|
+
self.assertEqual(response["Access-Control-Allow-Origin"], "http://example.com")
|
|
309
|
+
|
|
310
|
+
def test_register_node_allows_preflight_without_authentication(self):
|
|
311
|
+
self.client.logout()
|
|
312
|
+
response = self.client.options(
|
|
313
|
+
reverse("register-node"), HTTP_ORIGIN="https://example.com"
|
|
314
|
+
)
|
|
315
|
+
self.assertEqual(response.status_code, 200)
|
|
316
|
+
self.assertEqual(response["Access-Control-Allow-Origin"], "https://example.com")
|
|
317
|
+
self.assertEqual(response["Access-Control-Allow-Credentials"], "true")
|
|
318
|
+
|
|
319
|
+
def test_register_node_accepts_signed_payload_without_login(self):
|
|
320
|
+
self.client.logout()
|
|
321
|
+
NodeFeature.objects.get_or_create(
|
|
322
|
+
slug="clipboard-poll", defaults={"display": "Clipboard Poll"}
|
|
323
|
+
)
|
|
324
|
+
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
325
|
+
public_bytes = private_key.public_key().public_bytes(
|
|
326
|
+
encoding=serialization.Encoding.PEM,
|
|
327
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
328
|
+
).decode()
|
|
329
|
+
token = "visitor-token"
|
|
330
|
+
signature = base64.b64encode(
|
|
331
|
+
private_key.sign(
|
|
332
|
+
token.encode(),
|
|
333
|
+
padding.PKCS1v15(),
|
|
334
|
+
hashes.SHA256(),
|
|
335
|
+
)
|
|
336
|
+
).decode()
|
|
337
|
+
payload = {
|
|
338
|
+
"hostname": "visitor",
|
|
339
|
+
"address": "127.0.0.1",
|
|
340
|
+
"port": 8000,
|
|
341
|
+
"mac_address": "aa:bb:cc:dd:ee:11",
|
|
342
|
+
"public_key": public_bytes,
|
|
343
|
+
"token": token,
|
|
344
|
+
"signature": signature,
|
|
345
|
+
"features": ["clipboard-poll"],
|
|
346
|
+
}
|
|
347
|
+
response = self.client.post(
|
|
348
|
+
reverse("register-node"),
|
|
349
|
+
data=json.dumps(payload),
|
|
350
|
+
content_type="application/json",
|
|
351
|
+
HTTP_ORIGIN="http://example.com",
|
|
352
|
+
)
|
|
353
|
+
self.assertEqual(response.status_code, 200)
|
|
354
|
+
self.assertEqual(response["Access-Control-Allow-Origin"], "http://example.com")
|
|
355
|
+
node = Node.objects.get(mac_address="aa:bb:cc:dd:ee:11")
|
|
356
|
+
self.assertEqual(node.public_key, public_bytes)
|
|
357
|
+
self.assertTrue(node.has_feature("clipboard-poll"))
|
|
358
|
+
|
|
359
|
+
def test_register_node_accepts_text_plain_payload(self):
|
|
360
|
+
payload = {
|
|
361
|
+
"hostname": "plain",
|
|
362
|
+
"address": "127.0.0.1",
|
|
363
|
+
"port": 8001,
|
|
364
|
+
"mac_address": "aa:bb:cc:dd:ee:ff",
|
|
365
|
+
}
|
|
366
|
+
response = self.client.post(
|
|
367
|
+
reverse("register-node"),
|
|
368
|
+
data=json.dumps(payload),
|
|
369
|
+
content_type="text/plain",
|
|
370
|
+
)
|
|
371
|
+
self.assertEqual(response.status_code, 200)
|
|
372
|
+
self.assertTrue(Node.objects.filter(mac_address="aa:bb:cc:dd:ee:ff").exists())
|
|
150
373
|
|
|
151
374
|
|
|
152
375
|
class NodeRegisterCurrentTests(TestCase):
|
|
@@ -156,39 +379,179 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
156
379
|
self.user = User.objects.create_user(username="nodeuser", password="pwd")
|
|
157
380
|
self.client.force_login(self.user)
|
|
158
381
|
NodeRole.objects.get_or_create(name="Terminal")
|
|
159
|
-
|
|
382
|
+
|
|
383
|
+
def test_register_current_notifies_peers_on_start(self):
|
|
384
|
+
with TemporaryDirectory() as tmp:
|
|
385
|
+
base = Path(tmp)
|
|
386
|
+
with override_settings(BASE_DIR=base):
|
|
387
|
+
with (
|
|
388
|
+
patch(
|
|
389
|
+
"nodes.models.Node.get_current_mac",
|
|
390
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
391
|
+
),
|
|
392
|
+
patch("nodes.models.socket.gethostname", return_value="testhost"),
|
|
393
|
+
patch(
|
|
394
|
+
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
395
|
+
),
|
|
396
|
+
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
397
|
+
patch.object(Node, "ensure_keys"),
|
|
398
|
+
patch.object(Node, "notify_peers_of_update") as mock_notify,
|
|
399
|
+
):
|
|
400
|
+
Node.register_current()
|
|
401
|
+
mock_notify.assert_called_once()
|
|
402
|
+
|
|
403
|
+
def test_register_current_refreshes_lcd_feature(self):
|
|
404
|
+
NodeFeature.objects.get_or_create(
|
|
405
|
+
slug="lcd-screen", defaults={"display": "LCD Screen"}
|
|
406
|
+
)
|
|
160
407
|
with TemporaryDirectory() as tmp:
|
|
161
408
|
base = Path(tmp)
|
|
162
409
|
locks = base / "locks"
|
|
163
410
|
locks.mkdir()
|
|
164
|
-
|
|
411
|
+
lock = locks / "lcd_screen.lck"
|
|
412
|
+
lock.touch()
|
|
165
413
|
with override_settings(BASE_DIR=base):
|
|
166
|
-
with
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"nodes.models.
|
|
172
|
-
|
|
414
|
+
with (
|
|
415
|
+
patch(
|
|
416
|
+
"nodes.models.Node.get_current_mac",
|
|
417
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
418
|
+
),
|
|
419
|
+
patch("nodes.models.socket.gethostname", return_value="testhost"),
|
|
420
|
+
patch(
|
|
421
|
+
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
422
|
+
),
|
|
423
|
+
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
424
|
+
patch.object(Node, "ensure_keys"),
|
|
425
|
+
):
|
|
173
426
|
node, created = Node.register_current()
|
|
174
427
|
self.assertTrue(created)
|
|
175
|
-
self.assertTrue(node.
|
|
176
|
-
|
|
177
|
-
node.has_lcd_screen = False
|
|
178
|
-
node.save(update_fields=["has_lcd_screen"])
|
|
428
|
+
self.assertTrue(node.has_feature("lcd-screen"))
|
|
179
429
|
|
|
430
|
+
lock.unlink()
|
|
180
431
|
with override_settings(BASE_DIR=base):
|
|
181
|
-
with
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
"nodes.models.
|
|
187
|
-
|
|
188
|
-
|
|
432
|
+
with (
|
|
433
|
+
patch(
|
|
434
|
+
"nodes.models.Node.get_current_mac",
|
|
435
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
436
|
+
),
|
|
437
|
+
patch("nodes.models.socket.gethostname", return_value="testhost"),
|
|
438
|
+
patch(
|
|
439
|
+
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
440
|
+
),
|
|
441
|
+
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
442
|
+
patch.object(Node, "ensure_keys"),
|
|
443
|
+
):
|
|
444
|
+
_, created2 = Node.register_current()
|
|
189
445
|
self.assertFalse(created2)
|
|
190
446
|
node.refresh_from_db()
|
|
191
|
-
self.assertFalse(node.
|
|
447
|
+
self.assertFalse(node.has_feature("lcd-screen"))
|
|
448
|
+
|
|
449
|
+
lock.touch()
|
|
450
|
+
with override_settings(BASE_DIR=base):
|
|
451
|
+
with (
|
|
452
|
+
patch(
|
|
453
|
+
"nodes.models.Node.get_current_mac",
|
|
454
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
455
|
+
),
|
|
456
|
+
patch("nodes.models.socket.gethostname", return_value="testhost"),
|
|
457
|
+
patch(
|
|
458
|
+
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
459
|
+
),
|
|
460
|
+
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
461
|
+
patch.object(Node, "ensure_keys"),
|
|
462
|
+
):
|
|
463
|
+
node, created3 = Node.register_current()
|
|
464
|
+
self.assertFalse(created3)
|
|
465
|
+
node.refresh_from_db()
|
|
466
|
+
self.assertTrue(node.has_feature("lcd-screen"))
|
|
467
|
+
|
|
468
|
+
def test_register_current_notifies_peers_on_version_upgrade(self):
|
|
469
|
+
remote = Node.objects.create(
|
|
470
|
+
hostname="remote",
|
|
471
|
+
address="10.0.0.2",
|
|
472
|
+
port=9100,
|
|
473
|
+
mac_address="aa:bb:cc:dd:ee:ff",
|
|
474
|
+
)
|
|
475
|
+
with TemporaryDirectory() as tmp:
|
|
476
|
+
base = Path(tmp)
|
|
477
|
+
(base / "VERSION").write_text("2.0.0")
|
|
478
|
+
with override_settings(BASE_DIR=base):
|
|
479
|
+
with (
|
|
480
|
+
patch(
|
|
481
|
+
"nodes.models.Node.get_current_mac",
|
|
482
|
+
return_value="00:ff:ee:dd:cc:bb",
|
|
483
|
+
),
|
|
484
|
+
patch("nodes.models.socket.gethostname", return_value="localnode"),
|
|
485
|
+
patch(
|
|
486
|
+
"nodes.models.socket.gethostbyname",
|
|
487
|
+
return_value="192.168.1.5",
|
|
488
|
+
),
|
|
489
|
+
patch("nodes.models.revision.get_revision", return_value="newrev"),
|
|
490
|
+
patch("requests.post") as mock_post,
|
|
491
|
+
):
|
|
492
|
+
Node.objects.create(
|
|
493
|
+
hostname="localnode",
|
|
494
|
+
address="192.168.1.5",
|
|
495
|
+
port=8000,
|
|
496
|
+
mac_address="00:ff:ee:dd:cc:bb",
|
|
497
|
+
installed_version="1.9.0",
|
|
498
|
+
installed_revision="oldrev",
|
|
499
|
+
)
|
|
500
|
+
mock_post.return_value = SimpleNamespace(
|
|
501
|
+
ok=True, status_code=200, text=""
|
|
502
|
+
)
|
|
503
|
+
node, created = Node.register_current()
|
|
504
|
+
self.assertFalse(created)
|
|
505
|
+
self.assertGreaterEqual(mock_post.call_count, 1)
|
|
506
|
+
args, kwargs = mock_post.call_args
|
|
507
|
+
self.assertIn(str(remote.port), args[0])
|
|
508
|
+
payload = json.loads(kwargs["data"])
|
|
509
|
+
self.assertEqual(payload["hostname"], "localnode")
|
|
510
|
+
self.assertEqual(payload["installed_version"], "2.0.0")
|
|
511
|
+
self.assertEqual(payload["installed_revision"], "newrev")
|
|
512
|
+
|
|
513
|
+
def test_register_current_notifies_peers_without_version_change(self):
|
|
514
|
+
Node.objects.create(
|
|
515
|
+
hostname="remote",
|
|
516
|
+
address="10.0.0.3",
|
|
517
|
+
port=9200,
|
|
518
|
+
mac_address="aa:bb:cc:dd:ee:11",
|
|
519
|
+
)
|
|
520
|
+
with TemporaryDirectory() as tmp:
|
|
521
|
+
base = Path(tmp)
|
|
522
|
+
(base / "VERSION").write_text("1.0.0")
|
|
523
|
+
with override_settings(BASE_DIR=base):
|
|
524
|
+
with (
|
|
525
|
+
patch(
|
|
526
|
+
"nodes.models.Node.get_current_mac",
|
|
527
|
+
return_value="00:ff:ee:dd:cc:cc",
|
|
528
|
+
),
|
|
529
|
+
patch("nodes.models.socket.gethostname", return_value="samever"),
|
|
530
|
+
patch(
|
|
531
|
+
"nodes.models.socket.gethostbyname",
|
|
532
|
+
return_value="192.168.1.6",
|
|
533
|
+
),
|
|
534
|
+
patch("nodes.models.revision.get_revision", return_value="rev1"),
|
|
535
|
+
patch("requests.post") as mock_post,
|
|
536
|
+
):
|
|
537
|
+
Node.objects.create(
|
|
538
|
+
hostname="samever",
|
|
539
|
+
address="192.168.1.6",
|
|
540
|
+
port=8000,
|
|
541
|
+
mac_address="00:ff:ee:dd:cc:cc",
|
|
542
|
+
installed_version="1.0.0",
|
|
543
|
+
installed_revision="rev1",
|
|
544
|
+
)
|
|
545
|
+
mock_post.return_value = SimpleNamespace(
|
|
546
|
+
ok=True, status_code=200, text=""
|
|
547
|
+
)
|
|
548
|
+
Node.register_current()
|
|
549
|
+
self.assertEqual(mock_post.call_count, 1)
|
|
550
|
+
args, kwargs = mock_post.call_args
|
|
551
|
+
self.assertIn("/nodes/register/", args[0])
|
|
552
|
+
payload = json.loads(kwargs["data"])
|
|
553
|
+
self.assertEqual(payload["installed_version"], "1.0.0")
|
|
554
|
+
self.assertEqual(payload.get("installed_revision"), "rev1")
|
|
192
555
|
|
|
193
556
|
@patch("nodes.views.capture_screenshot")
|
|
194
557
|
def test_capture_screenshot(self, mock_capture):
|
|
@@ -270,9 +633,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
270
633
|
self.assertEqual(get_resp.json()["hostname"], "public")
|
|
271
634
|
|
|
272
635
|
pre_count = NetMessage.objects.count()
|
|
273
|
-
post_resp = self.client.post(
|
|
274
|
-
url, data="hello", content_type="text/plain"
|
|
275
|
-
)
|
|
636
|
+
post_resp = self.client.post(url, data="hello", content_type="text/plain")
|
|
276
637
|
self.assertEqual(post_resp.status_code, 200)
|
|
277
638
|
self.assertEqual(NetMessage.objects.count(), pre_count + 1)
|
|
278
639
|
msg = NetMessage.objects.order_by("-created").first()
|
|
@@ -307,10 +668,14 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
307
668
|
|
|
308
669
|
def test_net_message_with_valid_signature(self):
|
|
309
670
|
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
310
|
-
public_key =
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
671
|
+
public_key = (
|
|
672
|
+
key.public_key()
|
|
673
|
+
.public_bytes(
|
|
674
|
+
encoding=serialization.Encoding.PEM,
|
|
675
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
676
|
+
)
|
|
677
|
+
.decode()
|
|
678
|
+
)
|
|
314
679
|
sender = Node.objects.create(
|
|
315
680
|
hostname="sender",
|
|
316
681
|
address="10.0.0.1",
|
|
@@ -325,11 +690,10 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
325
690
|
"body": "world",
|
|
326
691
|
"seen": [],
|
|
327
692
|
"sender": str(sender.uuid),
|
|
693
|
+
"origin": str(sender.uuid),
|
|
328
694
|
}
|
|
329
695
|
payload_json = json.dumps(payload, separators=(",", ":"), sort_keys=True)
|
|
330
|
-
signature = key.sign(
|
|
331
|
-
payload_json.encode(), padding.PKCS1v15(), hashes.SHA256()
|
|
332
|
-
)
|
|
696
|
+
signature = key.sign(payload_json.encode(), padding.PKCS1v15(), hashes.SHA256())
|
|
333
697
|
resp = self.client.post(
|
|
334
698
|
reverse("net-message"),
|
|
335
699
|
data=payload_json,
|
|
@@ -338,8 +702,13 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
338
702
|
)
|
|
339
703
|
self.assertEqual(resp.status_code, 200)
|
|
340
704
|
self.assertTrue(NetMessage.objects.filter(uuid=msg_id).exists())
|
|
705
|
+
message = NetMessage.objects.get(uuid=msg_id)
|
|
706
|
+
self.assertEqual(message.node_origin, sender)
|
|
341
707
|
|
|
342
708
|
def test_clipboard_polling_creates_task(self):
|
|
709
|
+
feature, _ = NodeFeature.objects.get_or_create(
|
|
710
|
+
slug="clipboard-poll", defaults={"display": "Clipboard Poll"}
|
|
711
|
+
)
|
|
343
712
|
node = Node.objects.create(
|
|
344
713
|
hostname="clip",
|
|
345
714
|
address="127.0.0.1",
|
|
@@ -347,15 +716,16 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
347
716
|
mac_address="00:11:22:33:44:99",
|
|
348
717
|
)
|
|
349
718
|
task_name = f"poll_clipboard_node_{node.pk}"
|
|
350
|
-
|
|
351
|
-
node
|
|
352
|
-
node.save()
|
|
719
|
+
PeriodicTask.objects.filter(name=task_name).delete()
|
|
720
|
+
NodeFeatureAssignment.objects.create(node=node, feature=feature)
|
|
353
721
|
self.assertTrue(PeriodicTask.objects.filter(name=task_name).exists())
|
|
354
|
-
node
|
|
355
|
-
node.save()
|
|
722
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=feature).delete()
|
|
356
723
|
self.assertFalse(PeriodicTask.objects.filter(name=task_name).exists())
|
|
357
724
|
|
|
358
725
|
def test_screenshot_polling_creates_task(self):
|
|
726
|
+
feature, _ = NodeFeature.objects.get_or_create(
|
|
727
|
+
slug="screenshot-poll", defaults={"display": "Screenshot Poll"}
|
|
728
|
+
)
|
|
359
729
|
node = Node.objects.create(
|
|
360
730
|
hostname="shot",
|
|
361
731
|
address="127.0.0.1",
|
|
@@ -363,14 +733,22 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
363
733
|
mac_address="00:11:22:33:44:aa",
|
|
364
734
|
)
|
|
365
735
|
task_name = f"capture_screenshot_node_{node.pk}"
|
|
366
|
-
|
|
367
|
-
node
|
|
368
|
-
node.save()
|
|
736
|
+
PeriodicTask.objects.filter(name=task_name).delete()
|
|
737
|
+
NodeFeatureAssignment.objects.create(node=node, feature=feature)
|
|
369
738
|
self.assertTrue(PeriodicTask.objects.filter(name=task_name).exists())
|
|
370
|
-
node
|
|
371
|
-
node.save()
|
|
739
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=feature).delete()
|
|
372
740
|
self.assertFalse(PeriodicTask.objects.filter(name=task_name).exists())
|
|
373
741
|
|
|
742
|
+
|
|
743
|
+
class CheckRegistrationReadyCommandTests(TestCase):
|
|
744
|
+
def test_command_completes_successfully(self):
|
|
745
|
+
NodeRole.objects.get_or_create(name="Terminal")
|
|
746
|
+
with TemporaryDirectory() as tmp:
|
|
747
|
+
base = Path(tmp)
|
|
748
|
+
with override_settings(BASE_DIR=base):
|
|
749
|
+
call_command("check_registration_ready")
|
|
750
|
+
|
|
751
|
+
|
|
374
752
|
class NodeAdminTests(TestCase):
|
|
375
753
|
|
|
376
754
|
def setUp(self):
|
|
@@ -395,7 +773,7 @@ class NodeAdminTests(TestCase):
|
|
|
395
773
|
self.assertTemplateUsed(response, "admin/nodes/node/register_remote.html")
|
|
396
774
|
self.assertEqual(Node.objects.count(), 1)
|
|
397
775
|
node = Node.objects.first()
|
|
398
|
-
ver = Path(
|
|
776
|
+
ver = Path("VERSION").read_text().strip()
|
|
399
777
|
rev = "abcdef123456"
|
|
400
778
|
self.assertEqual(node.base_path, str(settings.BASE_DIR))
|
|
401
779
|
self.assertEqual(node.installed_version, ver)
|
|
@@ -408,9 +786,7 @@ class NodeAdminTests(TestCase):
|
|
|
408
786
|
self.assertTrue(priv.exists())
|
|
409
787
|
self.assertTrue(pub.exists())
|
|
410
788
|
self.assertTrue(node.public_key)
|
|
411
|
-
self.assertTrue(
|
|
412
|
-
Site.objects.filter(domain=hostname, name="host").exists()
|
|
413
|
-
)
|
|
789
|
+
self.assertTrue(Site.objects.filter(domain=hostname, name="host").exists())
|
|
414
790
|
|
|
415
791
|
def test_register_current_updates_existing_node(self):
|
|
416
792
|
hostname = socket.gethostname()
|
|
@@ -446,9 +822,7 @@ class NodeAdminTests(TestCase):
|
|
|
446
822
|
self.assertIn(node.public_key.strip(), resp.content.decode())
|
|
447
823
|
|
|
448
824
|
@patch("nodes.admin.capture_screenshot")
|
|
449
|
-
def test_capture_site_screenshot_from_admin(
|
|
450
|
-
self, mock_capture_screenshot
|
|
451
|
-
):
|
|
825
|
+
def test_capture_site_screenshot_from_admin(self, mock_capture_screenshot):
|
|
452
826
|
screenshot_dir = settings.LOG_DIR / "screenshots"
|
|
453
827
|
screenshot_dir.mkdir(parents=True, exist_ok=True)
|
|
454
828
|
file_path = screenshot_dir / "test.png"
|
|
@@ -472,9 +846,7 @@ class NodeAdminTests(TestCase):
|
|
|
472
846
|
self.assertEqual(screenshot.path, "screenshots/test.png")
|
|
473
847
|
self.assertEqual(screenshot.method, "ADMIN")
|
|
474
848
|
mock_capture_screenshot.assert_called_once_with("http://testserver/")
|
|
475
|
-
self.assertContains(
|
|
476
|
-
response, "Screenshot saved to screenshots/test.png"
|
|
477
|
-
)
|
|
849
|
+
self.assertContains(response, "Screenshot saved to screenshots/test.png")
|
|
478
850
|
|
|
479
851
|
def test_view_screenshot_in_change_admin(self):
|
|
480
852
|
screenshot_dir = settings.LOG_DIR / "screenshots"
|
|
@@ -578,7 +950,9 @@ class NetMessageReachTests(TestCase):
|
|
|
578
950
|
for name in ["Terminal", "Control", "Satellite", "Constellation"]:
|
|
579
951
|
self.roles[name], _ = NodeRole.objects.get_or_create(name=name)
|
|
580
952
|
self.nodes = {}
|
|
581
|
-
for idx, name in enumerate(
|
|
953
|
+
for idx, name in enumerate(
|
|
954
|
+
["Terminal", "Control", "Satellite", "Constellation"], start=1
|
|
955
|
+
):
|
|
582
956
|
self.nodes[name] = Node.objects.create(
|
|
583
957
|
hostname=name.lower(),
|
|
584
958
|
address=f"10.0.0.{idx}",
|
|
@@ -589,7 +963,9 @@ class NetMessageReachTests(TestCase):
|
|
|
589
963
|
|
|
590
964
|
@patch("requests.post")
|
|
591
965
|
def test_terminal_reach_limits_nodes(self, mock_post):
|
|
592
|
-
msg = NetMessage.objects.create(
|
|
966
|
+
msg = NetMessage.objects.create(
|
|
967
|
+
subject="s", body="b", reach=self.roles["Terminal"]
|
|
968
|
+
)
|
|
593
969
|
with patch.object(Node, "get_local", return_value=None):
|
|
594
970
|
msg.propagate()
|
|
595
971
|
roles = set(msg.propagated_to.values_list("role__name", flat=True))
|
|
@@ -598,7 +974,9 @@ class NetMessageReachTests(TestCase):
|
|
|
598
974
|
|
|
599
975
|
@patch("requests.post")
|
|
600
976
|
def test_control_reach_includes_control_and_terminal(self, mock_post):
|
|
601
|
-
msg = NetMessage.objects.create(
|
|
977
|
+
msg = NetMessage.objects.create(
|
|
978
|
+
subject="s", body="b", reach=self.roles["Control"]
|
|
979
|
+
)
|
|
602
980
|
with patch.object(Node, "get_local", return_value=None):
|
|
603
981
|
msg.propagate()
|
|
604
982
|
roles = set(msg.propagated_to.values_list("role__name", flat=True))
|
|
@@ -607,7 +985,9 @@ class NetMessageReachTests(TestCase):
|
|
|
607
985
|
|
|
608
986
|
@patch("requests.post")
|
|
609
987
|
def test_satellite_reach_includes_lower_roles(self, mock_post):
|
|
610
|
-
msg = NetMessage.objects.create(
|
|
988
|
+
msg = NetMessage.objects.create(
|
|
989
|
+
subject="s", body="b", reach=self.roles["Satellite"]
|
|
990
|
+
)
|
|
611
991
|
with patch.object(Node, "get_local", return_value=None):
|
|
612
992
|
msg.propagate()
|
|
613
993
|
roles = set(msg.propagated_to.values_list("role__name", flat=True))
|
|
@@ -616,7 +996,9 @@ class NetMessageReachTests(TestCase):
|
|
|
616
996
|
|
|
617
997
|
@patch("requests.post")
|
|
618
998
|
def test_constellation_reach_prioritizes_constellation(self, mock_post):
|
|
619
|
-
msg = NetMessage.objects.create(
|
|
999
|
+
msg = NetMessage.objects.create(
|
|
1000
|
+
subject="s", body="b", reach=self.roles["Constellation"]
|
|
1001
|
+
)
|
|
620
1002
|
with patch.object(Node, "get_local", return_value=None):
|
|
621
1003
|
msg.propagate()
|
|
622
1004
|
roles = set(msg.propagated_to.values_list("role__name", flat=True))
|
|
@@ -648,14 +1030,24 @@ class NetMessagePropagationTests(TestCase):
|
|
|
648
1030
|
)
|
|
649
1031
|
)
|
|
650
1032
|
|
|
1033
|
+
def test_broadcast_sets_node_origin(self):
|
|
1034
|
+
with patch.object(Node, "get_local", return_value=self.local):
|
|
1035
|
+
msg = NetMessage.broadcast(subject="subject", body="body")
|
|
1036
|
+
self.assertEqual(msg.node_origin, self.local)
|
|
1037
|
+
|
|
651
1038
|
@patch("requests.post")
|
|
652
1039
|
@patch("core.notifications.notify")
|
|
653
|
-
def test_propagate_forwards_to_three_and_notifies_local(
|
|
1040
|
+
def test_propagate_forwards_to_three_and_notifies_local(
|
|
1041
|
+
self, mock_notify, mock_post
|
|
1042
|
+
):
|
|
654
1043
|
msg = NetMessage.objects.create(subject="s", body="b", reach=self.role)
|
|
655
1044
|
with patch.object(Node, "get_local", return_value=self.local):
|
|
656
1045
|
msg.propagate(seen=[str(self.remotes[0].uuid)])
|
|
657
1046
|
mock_notify.assert_called_once_with("s", "b")
|
|
658
1047
|
self.assertEqual(mock_post.call_count, 3)
|
|
1048
|
+
for call_args in mock_post.call_args_list:
|
|
1049
|
+
payload = json.loads(call_args.kwargs["data"])
|
|
1050
|
+
self.assertEqual(payload.get("origin"), str(self.local.uuid))
|
|
659
1051
|
targets = {
|
|
660
1052
|
call.args[0].split("//")[1].split("/")[0]
|
|
661
1053
|
for call in mock_post.call_args_list
|
|
@@ -665,6 +1057,42 @@ class NetMessagePropagationTests(TestCase):
|
|
|
665
1057
|
self.assertEqual(msg.propagated_to.count(), 4)
|
|
666
1058
|
self.assertTrue(msg.complete)
|
|
667
1059
|
|
|
1060
|
+
@patch("requests.post")
|
|
1061
|
+
@patch("core.notifications.notify", return_value=True)
|
|
1062
|
+
def test_propagate_prunes_old_local_messages(self, mock_notify, mock_post):
|
|
1063
|
+
old_local = NetMessage.objects.create(
|
|
1064
|
+
subject="old local",
|
|
1065
|
+
body="body",
|
|
1066
|
+
reach=self.role,
|
|
1067
|
+
node_origin=self.local,
|
|
1068
|
+
)
|
|
1069
|
+
NetMessage.objects.filter(pk=old_local.pk).update(
|
|
1070
|
+
created=timezone.now() - timedelta(days=8)
|
|
1071
|
+
)
|
|
1072
|
+
old_remote = NetMessage.objects.create(
|
|
1073
|
+
subject="old remote",
|
|
1074
|
+
body="body",
|
|
1075
|
+
reach=self.role,
|
|
1076
|
+
node_origin=self.remotes[0],
|
|
1077
|
+
)
|
|
1078
|
+
NetMessage.objects.filter(pk=old_remote.pk).update(
|
|
1079
|
+
created=timezone.now() - timedelta(days=8)
|
|
1080
|
+
)
|
|
1081
|
+
msg = NetMessage.objects.create(
|
|
1082
|
+
subject="fresh",
|
|
1083
|
+
body="body",
|
|
1084
|
+
reach=self.role,
|
|
1085
|
+
node_origin=self.local,
|
|
1086
|
+
)
|
|
1087
|
+
with patch.object(Node, "get_local", return_value=self.local):
|
|
1088
|
+
msg.propagate()
|
|
1089
|
+
|
|
1090
|
+
mock_notify.assert_called_once_with("fresh", "body")
|
|
1091
|
+
self.assertFalse(NetMessage.objects.filter(pk=old_local.pk).exists())
|
|
1092
|
+
self.assertTrue(NetMessage.objects.filter(pk=old_remote.pk).exists())
|
|
1093
|
+
self.assertTrue(NetMessage.objects.filter(pk=msg.pk).exists())
|
|
1094
|
+
|
|
1095
|
+
|
|
668
1096
|
class NodeActionTests(TestCase):
|
|
669
1097
|
def setUp(self):
|
|
670
1098
|
self.client = Client()
|
|
@@ -739,7 +1167,7 @@ class NodeActionTests(TestCase):
|
|
|
739
1167
|
|
|
740
1168
|
|
|
741
1169
|
class StartupNotificationTests(TestCase):
|
|
742
|
-
def
|
|
1170
|
+
def test_startup_notification_uses_hostname_and_revision(self):
|
|
743
1171
|
from nodes.apps import _startup_notification
|
|
744
1172
|
|
|
745
1173
|
with TemporaryDirectory() as tmp:
|
|
@@ -750,20 +1178,17 @@ class StartupNotificationTests(TestCase):
|
|
|
750
1178
|
"nodes.apps.revision.get_revision", return_value="abcdef123456"
|
|
751
1179
|
):
|
|
752
1180
|
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)
|
|
1181
|
+
with patch(
|
|
1182
|
+
"nodes.apps.socket.gethostname", return_value="host"
|
|
1183
|
+
):
|
|
1184
|
+
with patch.dict(os.environ, {"PORT": "9000"}):
|
|
1185
|
+
_startup_notification()
|
|
1186
|
+
time.sleep(0.1)
|
|
762
1187
|
|
|
763
1188
|
mock_broadcast.assert_called_once()
|
|
764
1189
|
_, kwargs = mock_broadcast.call_args
|
|
765
|
-
self.assertEqual(kwargs["subject"], "
|
|
766
|
-
self.assertTrue(kwargs["body"].startswith("
|
|
1190
|
+
self.assertEqual(kwargs["subject"], "host:9000")
|
|
1191
|
+
self.assertTrue(kwargs["body"].startswith("1.2.3 r"))
|
|
767
1192
|
|
|
768
1193
|
|
|
769
1194
|
class StartupHandlerTests(TestCase):
|
|
@@ -787,11 +1212,14 @@ class StartupHandlerTests(TestCase):
|
|
|
787
1212
|
|
|
788
1213
|
with patch("nodes.apps._startup_notification") as mock_start:
|
|
789
1214
|
with patch("nodes.apps.connections") as mock_connections:
|
|
790
|
-
mock_connections.__getitem__.return_value.ensure_connection.return_value =
|
|
1215
|
+
mock_connections.__getitem__.return_value.ensure_connection.return_value = (
|
|
1216
|
+
None
|
|
1217
|
+
)
|
|
791
1218
|
_trigger_startup_notification()
|
|
792
1219
|
|
|
793
1220
|
mock_start.assert_called_once()
|
|
794
1221
|
|
|
1222
|
+
|
|
795
1223
|
class NotificationManagerTests(TestCase):
|
|
796
1224
|
def test_send_writes_trimmed_lines(self):
|
|
797
1225
|
from core.notifications import NotificationManager
|
|
@@ -921,6 +1349,7 @@ class ContentSampleAdminTests(TestCase):
|
|
|
921
1349
|
self.assertContains(resp, "Duplicate sample not created")
|
|
922
1350
|
|
|
923
1351
|
|
|
1352
|
+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend")
|
|
924
1353
|
class EmailOutboxTests(TestCase):
|
|
925
1354
|
def test_node_send_mail_uses_outbox(self):
|
|
926
1355
|
node = Node.objects.create(
|
|
@@ -929,31 +1358,51 @@ class EmailOutboxTests(TestCase):
|
|
|
929
1358
|
port=8000,
|
|
930
1359
|
mac_address="00:11:22:33:aa:bb",
|
|
931
1360
|
)
|
|
932
|
-
EmailOutbox.objects.create(
|
|
1361
|
+
outbox = EmailOutbox.objects.create(
|
|
933
1362
|
node=node, host="smtp.example.com", port=25, username="u", password="p"
|
|
934
1363
|
)
|
|
935
|
-
with patch("nodes.models.
|
|
936
|
-
"nodes.models.send_mail"
|
|
937
|
-
) as sm:
|
|
938
|
-
conn = MagicMock()
|
|
939
|
-
gc.return_value = conn
|
|
1364
|
+
with patch("nodes.models.mailer.send") as ms:
|
|
940
1365
|
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,
|
|
1366
|
+
ms.assert_called_once_with(
|
|
1367
|
+
"sub", "msg", ["to@example.com"], None, outbox=outbox
|
|
955
1368
|
)
|
|
956
1369
|
|
|
1370
|
+
def test_node_send_mail_queues_email(self):
|
|
1371
|
+
node = Node.objects.create(
|
|
1372
|
+
hostname="host",
|
|
1373
|
+
address="127.0.0.1",
|
|
1374
|
+
port=8000,
|
|
1375
|
+
mac_address="00:11:22:33:cc:dd",
|
|
1376
|
+
)
|
|
1377
|
+
node.send_mail("sub", "msg", ["to@example.com"])
|
|
1378
|
+
self.assertEqual(len(mail.outbox), 1)
|
|
1379
|
+
email = mail.outbox[0]
|
|
1380
|
+
self.assertEqual(email.subject, "sub")
|
|
1381
|
+
self.assertEqual(email.to, ["to@example.com"])
|
|
1382
|
+
|
|
1383
|
+
def test_string_representation_prefers_from_email(self):
|
|
1384
|
+
outbox = EmailOutbox.objects.create(
|
|
1385
|
+
host="smtp.example.com",
|
|
1386
|
+
port=587,
|
|
1387
|
+
username="mailer",
|
|
1388
|
+
password="secret",
|
|
1389
|
+
from_email="noreply@example.com",
|
|
1390
|
+
)
|
|
1391
|
+
|
|
1392
|
+
self.assertEqual(str(outbox), "noreply@example.com")
|
|
1393
|
+
|
|
1394
|
+
def test_string_representation_prefers_username_over_owner(self):
|
|
1395
|
+
group = SecurityGroup.objects.create(name="Operators")
|
|
1396
|
+
outbox = EmailOutbox.objects.create(
|
|
1397
|
+
group=group,
|
|
1398
|
+
host="smtp.example.com",
|
|
1399
|
+
port=587,
|
|
1400
|
+
username="mailer",
|
|
1401
|
+
password="secret",
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
self.assertEqual(str(outbox), "mailer@smtp.example.com")
|
|
1405
|
+
|
|
957
1406
|
|
|
958
1407
|
class ClipboardTaskTests(TestCase):
|
|
959
1408
|
@patch("nodes.tasks.pyperclip.paste")
|
|
@@ -1066,29 +1515,344 @@ class NodeRoleAdminTests(TestCase):
|
|
|
1066
1515
|
self.assertIsNone(node1.role)
|
|
1067
1516
|
self.assertEqual(node2.role, role)
|
|
1068
1517
|
|
|
1518
|
+
def test_registered_count_displayed(self):
|
|
1519
|
+
role = NodeRole.objects.create(name="ViewRole")
|
|
1520
|
+
Node.objects.create(
|
|
1521
|
+
hostname="n1",
|
|
1522
|
+
address="127.0.0.1",
|
|
1523
|
+
port=8000,
|
|
1524
|
+
mac_address="00:11:22:33:44:77",
|
|
1525
|
+
role=role,
|
|
1526
|
+
)
|
|
1527
|
+
resp = self.client.get(reverse("admin:nodes_noderole_changelist"))
|
|
1528
|
+
self.assertContains(resp, '<td class="field-registered">1</td>', html=True)
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
class NodeFeatureFixtureTests(TestCase):
|
|
1532
|
+
def test_rfid_scanner_fixture_includes_control_role(self):
|
|
1533
|
+
for name in ("Terminal", "Satellite", "Constellation", "Control"):
|
|
1534
|
+
NodeRole.objects.get_or_create(name=name)
|
|
1535
|
+
fixture_path = (
|
|
1536
|
+
Path(__file__).resolve().parent
|
|
1537
|
+
/ "fixtures"
|
|
1538
|
+
/ "node_features__nodefeature_rfid_scanner.json"
|
|
1539
|
+
)
|
|
1540
|
+
call_command("loaddata", str(fixture_path), verbosity=0)
|
|
1541
|
+
feature = NodeFeature.objects.get(slug="rfid-scanner")
|
|
1542
|
+
role_names = set(feature.roles.values_list("name", flat=True))
|
|
1543
|
+
self.assertIn("Control", role_names)
|
|
1544
|
+
|
|
1545
|
+
def test_ap_router_fixture_limits_roles(self):
|
|
1546
|
+
for name in ("Control", "Satellite"):
|
|
1547
|
+
NodeRole.objects.get_or_create(name=name)
|
|
1548
|
+
fixture_path = (
|
|
1549
|
+
Path(__file__).resolve().parent
|
|
1550
|
+
/ "fixtures"
|
|
1551
|
+
/ "node_features__nodefeature_ap_router.json"
|
|
1552
|
+
)
|
|
1553
|
+
call_command("loaddata", str(fixture_path), verbosity=0)
|
|
1554
|
+
feature = NodeFeature.objects.get(slug="ap-router")
|
|
1555
|
+
role_names = set(feature.roles.values_list("name", flat=True))
|
|
1556
|
+
self.assertEqual(role_names, {"Satellite"})
|
|
1069
1557
|
|
|
1070
|
-
|
|
1558
|
+
|
|
1559
|
+
class NodeFeatureTests(TestCase):
|
|
1071
1560
|
def setUp(self):
|
|
1072
|
-
self.
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1561
|
+
self.role, _ = NodeRole.objects.get_or_create(name="Terminal")
|
|
1562
|
+
with patch(
|
|
1563
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1564
|
+
):
|
|
1565
|
+
self.node = Node.objects.create(
|
|
1566
|
+
hostname="local",
|
|
1567
|
+
address="127.0.0.1",
|
|
1568
|
+
port=8000,
|
|
1569
|
+
mac_address="00:11:22:33:44:55",
|
|
1570
|
+
role=self.role,
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
def test_lcd_screen_enabled(self):
|
|
1574
|
+
feature = NodeFeature.objects.create(slug="lcd-screen", display="LCD")
|
|
1575
|
+
feature.roles.add(self.role)
|
|
1576
|
+
NodeFeatureAssignment.objects.create(node=self.node, feature=feature)
|
|
1577
|
+
with patch(
|
|
1578
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1579
|
+
):
|
|
1580
|
+
self.assertTrue(feature.is_enabled)
|
|
1581
|
+
NodeFeatureAssignment.objects.filter(node=self.node, feature=feature).delete()
|
|
1582
|
+
with patch(
|
|
1583
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1584
|
+
):
|
|
1585
|
+
self.assertFalse(feature.is_enabled)
|
|
1586
|
+
|
|
1587
|
+
def test_rfid_scanner_lock(self):
|
|
1588
|
+
feature = NodeFeature.objects.create(slug="rfid-scanner", display="RFID")
|
|
1589
|
+
feature.roles.add(self.role)
|
|
1590
|
+
with TemporaryDirectory() as tmp:
|
|
1591
|
+
base = Path(tmp)
|
|
1592
|
+
locks = base / "locks"
|
|
1593
|
+
locks.mkdir()
|
|
1594
|
+
with override_settings(BASE_DIR=base):
|
|
1595
|
+
with patch(
|
|
1596
|
+
"nodes.models.Node.get_current_mac",
|
|
1597
|
+
return_value="00:11:22:33:44:55",
|
|
1598
|
+
):
|
|
1599
|
+
self.assertFalse(feature.is_enabled)
|
|
1600
|
+
(locks / "rfid.lck").touch()
|
|
1601
|
+
self.assertTrue(feature.is_enabled)
|
|
1602
|
+
|
|
1603
|
+
def test_gui_toast_detection(self):
|
|
1604
|
+
feature = NodeFeature.objects.create(slug="gui-toast", display="GUI Toast")
|
|
1605
|
+
feature.roles.add(self.role)
|
|
1606
|
+
with patch(
|
|
1607
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1608
|
+
):
|
|
1609
|
+
with patch("core.notifications.supports_gui_toast", return_value=True):
|
|
1610
|
+
self.assertTrue(feature.is_enabled)
|
|
1611
|
+
with patch(
|
|
1612
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1613
|
+
):
|
|
1614
|
+
with patch("core.notifications.supports_gui_toast", return_value=False):
|
|
1615
|
+
self.assertFalse(feature.is_enabled)
|
|
1616
|
+
|
|
1617
|
+
def test_role_membership_alone_does_not_enable_feature(self):
|
|
1618
|
+
feature = NodeFeature.objects.create(
|
|
1619
|
+
slug="custom-feature", display="Custom Feature"
|
|
1620
|
+
)
|
|
1621
|
+
feature.roles.add(self.role)
|
|
1622
|
+
with patch(
|
|
1623
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1624
|
+
):
|
|
1625
|
+
self.assertFalse(feature.is_enabled)
|
|
1626
|
+
NodeFeatureAssignment.objects.create(node=self.node, feature=feature)
|
|
1627
|
+
with patch(
|
|
1628
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1629
|
+
):
|
|
1630
|
+
self.assertTrue(feature.is_enabled)
|
|
1631
|
+
|
|
1632
|
+
@patch("nodes.models.Node._has_rpi_camera", return_value=True)
|
|
1633
|
+
def test_rpi_camera_detection(self, mock_camera):
|
|
1634
|
+
feature = NodeFeature.objects.create(
|
|
1635
|
+
slug="rpi-camera", display="Raspberry Pi Camera"
|
|
1636
|
+
)
|
|
1637
|
+
feature.roles.add(self.role)
|
|
1638
|
+
with patch(
|
|
1639
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1640
|
+
):
|
|
1641
|
+
self.node.refresh_features()
|
|
1642
|
+
self.assertTrue(
|
|
1643
|
+
NodeFeatureAssignment.objects.filter(
|
|
1644
|
+
node=self.node, feature=feature
|
|
1645
|
+
).exists()
|
|
1076
1646
|
)
|
|
1077
|
-
self.client.force_login(self.user)
|
|
1078
1647
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1648
|
+
@patch("nodes.models.Node._has_rpi_camera", side_effect=[True, False])
|
|
1649
|
+
def test_rpi_camera_removed_when_unavailable(self, mock_camera):
|
|
1650
|
+
feature = NodeFeature.objects.create(
|
|
1651
|
+
slug="rpi-camera", display="Raspberry Pi Camera"
|
|
1652
|
+
)
|
|
1653
|
+
feature.roles.add(self.role)
|
|
1654
|
+
with patch(
|
|
1655
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1656
|
+
):
|
|
1657
|
+
self.node.refresh_features()
|
|
1658
|
+
self.assertTrue(
|
|
1659
|
+
NodeFeatureAssignment.objects.filter(
|
|
1660
|
+
node=self.node, feature=feature
|
|
1661
|
+
).exists()
|
|
1662
|
+
)
|
|
1663
|
+
self.node.refresh_features()
|
|
1664
|
+
self.assertFalse(
|
|
1665
|
+
NodeFeatureAssignment.objects.filter(
|
|
1666
|
+
node=self.node, feature=feature
|
|
1667
|
+
).exists()
|
|
1668
|
+
)
|
|
1669
|
+
|
|
1670
|
+
@patch("nodes.models.Node._hosts_gelectriic_ap", return_value=True)
|
|
1671
|
+
def test_ap_router_detection(self, mock_hosts):
|
|
1672
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
1673
|
+
feature = NodeFeature.objects.create(slug="ap-router", display="AP Router")
|
|
1674
|
+
feature.roles.add(control_role)
|
|
1675
|
+
mac = "00:11:22:33:44:66"
|
|
1676
|
+
with patch("nodes.models.Node.get_current_mac", return_value=mac):
|
|
1677
|
+
node = Node.objects.create(
|
|
1678
|
+
hostname="control",
|
|
1679
|
+
address="127.0.0.1",
|
|
1680
|
+
port=8000,
|
|
1681
|
+
mac_address=mac,
|
|
1682
|
+
role=control_role,
|
|
1683
|
+
)
|
|
1684
|
+
node.refresh_features()
|
|
1685
|
+
self.assertTrue(
|
|
1686
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=feature).exists()
|
|
1687
|
+
)
|
|
1081
1688
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1689
|
+
@patch("nodes.models.Node._hosts_gelectriic_ap", return_value=True)
|
|
1690
|
+
def test_ap_public_wifi_detection(self, mock_hosts):
|
|
1691
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
1692
|
+
router = NodeFeature.objects.create(slug="ap-router", display="AP Router")
|
|
1693
|
+
router.roles.add(control_role)
|
|
1694
|
+
public = NodeFeature.objects.create(
|
|
1695
|
+
slug="ap-public-wifi", display="AP Public Wi-Fi"
|
|
1696
|
+
)
|
|
1697
|
+
public.roles.add(control_role)
|
|
1698
|
+
mac = "00:11:22:33:44:88"
|
|
1699
|
+
with TemporaryDirectory() as tmp, override_settings(BASE_DIR=Path(tmp)):
|
|
1700
|
+
locks = Path(tmp) / "locks"
|
|
1701
|
+
locks.mkdir(parents=True, exist_ok=True)
|
|
1702
|
+
(locks / "public_wifi_mode.lck").touch()
|
|
1703
|
+
with patch("nodes.models.Node.get_current_mac", return_value=mac):
|
|
1704
|
+
node = Node.objects.create(
|
|
1705
|
+
hostname="control",
|
|
1706
|
+
address="127.0.0.1",
|
|
1707
|
+
port=8000,
|
|
1708
|
+
mac_address=mac,
|
|
1709
|
+
role=control_role,
|
|
1710
|
+
base_path=str(Path(tmp)),
|
|
1711
|
+
)
|
|
1712
|
+
node.refresh_features()
|
|
1713
|
+
self.assertTrue(
|
|
1714
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=public).exists()
|
|
1715
|
+
)
|
|
1716
|
+
self.assertFalse(
|
|
1717
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=router).exists()
|
|
1718
|
+
)
|
|
1719
|
+
|
|
1720
|
+
@patch("nodes.models.Node._hosts_gelectriic_ap", side_effect=[True, False])
|
|
1721
|
+
def test_ap_router_removed_when_not_hosting(self, mock_hosts):
|
|
1722
|
+
control_role, _ = NodeRole.objects.get_or_create(name="Control")
|
|
1723
|
+
feature = NodeFeature.objects.create(slug="ap-router", display="AP Router")
|
|
1724
|
+
feature.roles.add(control_role)
|
|
1725
|
+
mac = "00:11:22:33:44:77"
|
|
1726
|
+
with patch("nodes.models.Node.get_current_mac", return_value=mac):
|
|
1727
|
+
node = Node.objects.create(
|
|
1728
|
+
hostname="control",
|
|
1729
|
+
address="127.0.0.1",
|
|
1730
|
+
port=8000,
|
|
1731
|
+
mac_address=mac,
|
|
1732
|
+
role=control_role,
|
|
1733
|
+
)
|
|
1734
|
+
self.assertTrue(
|
|
1735
|
+
NodeFeatureAssignment.objects.filter(
|
|
1736
|
+
node=node, feature=feature
|
|
1737
|
+
).exists()
|
|
1738
|
+
)
|
|
1739
|
+
node.refresh_features()
|
|
1740
|
+
self.assertFalse(
|
|
1741
|
+
NodeFeatureAssignment.objects.filter(node=node, feature=feature).exists()
|
|
1742
|
+
)
|
|
1743
|
+
|
|
1744
|
+
@patch("nodes.models.Node._uses_postgres", return_value=True)
|
|
1745
|
+
def test_postgres_detection(self, mock_postgres):
|
|
1746
|
+
feature = NodeFeature.objects.create(
|
|
1747
|
+
slug="postgres-db", display="PostgreSQL Database"
|
|
1748
|
+
)
|
|
1749
|
+
feature.roles.add(self.role)
|
|
1750
|
+
with patch(
|
|
1751
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1752
|
+
):
|
|
1753
|
+
self.node.refresh_features()
|
|
1754
|
+
self.assertTrue(
|
|
1755
|
+
NodeFeatureAssignment.objects.filter(
|
|
1756
|
+
node=self.node, feature=feature
|
|
1757
|
+
).exists()
|
|
1758
|
+
)
|
|
1759
|
+
|
|
1760
|
+
@patch("nodes.models.Node._uses_postgres", side_effect=[True, False])
|
|
1761
|
+
def test_postgres_removed_when_not_in_use(self, mock_postgres):
|
|
1762
|
+
feature = NodeFeature.objects.create(
|
|
1763
|
+
slug="postgres-db", display="PostgreSQL Database"
|
|
1764
|
+
)
|
|
1765
|
+
feature.roles.add(self.role)
|
|
1766
|
+
with patch(
|
|
1767
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1768
|
+
):
|
|
1769
|
+
self.node.refresh_features()
|
|
1770
|
+
self.assertTrue(
|
|
1771
|
+
NodeFeatureAssignment.objects.filter(
|
|
1772
|
+
node=self.node, feature=feature
|
|
1773
|
+
).exists()
|
|
1774
|
+
)
|
|
1775
|
+
with patch(
|
|
1776
|
+
"nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"
|
|
1777
|
+
):
|
|
1778
|
+
self.node.refresh_features()
|
|
1779
|
+
self.assertFalse(
|
|
1780
|
+
NodeFeatureAssignment.objects.filter(
|
|
1781
|
+
node=self.node, feature=feature
|
|
1782
|
+
).exists()
|
|
1783
|
+
)
|
|
1784
|
+
|
|
1785
|
+
|
|
1786
|
+
class NodeRpiCameraDetectionTests(TestCase):
|
|
1787
|
+
@patch("nodes.models.subprocess.run")
|
|
1788
|
+
@patch("nodes.models.shutil.which")
|
|
1789
|
+
@patch("nodes.models.os.access")
|
|
1790
|
+
@patch("nodes.models.os.stat")
|
|
1791
|
+
@patch("nodes.models.Path.exists")
|
|
1792
|
+
def test_has_rpi_camera_true(
|
|
1793
|
+
self,
|
|
1794
|
+
mock_exists,
|
|
1795
|
+
mock_stat,
|
|
1796
|
+
mock_access,
|
|
1797
|
+
mock_which,
|
|
1798
|
+
mock_run,
|
|
1799
|
+
):
|
|
1800
|
+
mock_exists.return_value = True
|
|
1801
|
+
mock_stat.return_value = SimpleNamespace(st_mode=stat.S_IFCHR)
|
|
1802
|
+
mock_access.return_value = True
|
|
1803
|
+
mock_which.side_effect = lambda name: f"/usr/bin/{name}"
|
|
1804
|
+
mock_run.return_value = SimpleNamespace(returncode=0)
|
|
1805
|
+
|
|
1806
|
+
self.assertTrue(Node._has_rpi_camera())
|
|
1807
|
+
self.assertEqual(mock_which.call_count, len(Node.RPI_CAMERA_BINARIES))
|
|
1808
|
+
self.assertEqual(mock_run.call_count, len(Node.RPI_CAMERA_BINARIES))
|
|
1809
|
+
|
|
1810
|
+
@patch("nodes.models.subprocess.run")
|
|
1811
|
+
@patch("nodes.models.shutil.which")
|
|
1812
|
+
@patch("nodes.models.os.access")
|
|
1813
|
+
@patch("nodes.models.os.stat")
|
|
1814
|
+
@patch("nodes.models.Path.exists")
|
|
1815
|
+
def test_has_rpi_camera_missing_device(
|
|
1816
|
+
self,
|
|
1817
|
+
mock_exists,
|
|
1818
|
+
mock_stat,
|
|
1819
|
+
mock_access,
|
|
1820
|
+
mock_which,
|
|
1821
|
+
mock_run,
|
|
1822
|
+
):
|
|
1823
|
+
mock_exists.return_value = False
|
|
1824
|
+
|
|
1825
|
+
self.assertFalse(Node._has_rpi_camera())
|
|
1826
|
+
mock_stat.assert_not_called()
|
|
1827
|
+
mock_access.assert_not_called()
|
|
1828
|
+
mock_which.assert_not_called()
|
|
1829
|
+
mock_run.assert_not_called()
|
|
1830
|
+
|
|
1831
|
+
@patch("nodes.models.subprocess.run")
|
|
1832
|
+
@patch("nodes.models.shutil.which")
|
|
1833
|
+
@patch("nodes.models.os.access")
|
|
1834
|
+
@patch("nodes.models.os.stat")
|
|
1835
|
+
@patch("nodes.models.Path.exists")
|
|
1836
|
+
def test_has_rpi_camera_missing_tool(
|
|
1837
|
+
self,
|
|
1838
|
+
mock_exists,
|
|
1839
|
+
mock_stat,
|
|
1840
|
+
mock_access,
|
|
1841
|
+
mock_which,
|
|
1842
|
+
mock_run,
|
|
1843
|
+
):
|
|
1844
|
+
mock_exists.return_value = True
|
|
1845
|
+
mock_stat.return_value = SimpleNamespace(st_mode=stat.S_IFCHR)
|
|
1846
|
+
mock_access.return_value = True
|
|
1847
|
+
mock_run.return_value = SimpleNamespace(returncode=0)
|
|
1087
1848
|
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
self.assertContains(resp, 'value="Continue"')
|
|
1849
|
+
def tool_lookup(name):
|
|
1850
|
+
if name == Node.RPI_CAMERA_BINARIES[-1]:
|
|
1851
|
+
return None
|
|
1852
|
+
return f"/usr/bin/{name}"
|
|
1093
1853
|
|
|
1854
|
+
mock_which.side_effect = tool_lookup
|
|
1094
1855
|
|
|
1856
|
+
self.assertFalse(Node._has_rpi_camera())
|
|
1857
|
+
missing_index = Node.RPI_CAMERA_BINARIES.index(Node.RPI_CAMERA_BINARIES[-1])
|
|
1858
|
+
self.assertEqual(mock_run.call_count, missing_index)
|