arthexis 0.1.26__py3-none-any.whl → 0.1.28__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.

nodes/tests.py CHANGED
@@ -43,9 +43,14 @@ from django.test import (
43
43
  from django.urls import reverse
44
44
  from django.contrib.auth import get_user_model
45
45
  from django.contrib import admin
46
+ from django.contrib import messages
46
47
  from django.contrib.admin import helpers
47
48
  from django.contrib.auth.models import Permission
48
49
  from django_celery_beat.models import IntervalSchedule, PeriodicTask
50
+ from core.celery_utils import (
51
+ periodic_task_name_variants,
52
+ slugify_task_name,
53
+ )
49
54
  from django.conf import settings
50
55
  from django.utils import timezone
51
56
  from urllib.parse import urlparse
@@ -54,6 +59,7 @@ from . import dns as dns_utils
54
59
  from selenium.common.exceptions import WebDriverException
55
60
  from .classifiers import run_default_classifiers
56
61
  from .utils import capture_rpi_snapshot, capture_screenshot, save_screenshot
62
+ from .feature_checks import feature_checks
57
63
  from django.db.utils import DatabaseError
58
64
 
59
65
  from .admin import NodeAdmin
@@ -90,7 +96,7 @@ class NodeBadgeColorTests(TestCase):
90
96
  node = Node.objects.create(
91
97
  hostname="watchtower",
92
98
  address="10.1.0.1",
93
- port=8000,
99
+ port=8888,
94
100
  mac_address="00:aa:bb:cc:dd:01",
95
101
  role=self.watchtower,
96
102
  )
@@ -127,6 +133,23 @@ class NodeTests(TestCase):
127
133
  NodeRole.objects.get_or_create(name="Terminal")
128
134
  NodeRole.objects.get_or_create(name="Interface")
129
135
 
136
+ def test_terminal_role_enables_clipboard_feature_by_default(self):
137
+ role = NodeRole.objects.get(name="Terminal")
138
+ feature, _ = NodeFeature.objects.get_or_create(
139
+ slug="clipboard-poll", defaults={"display": "Clipboard Poll"}
140
+ )
141
+ feature.roles.add(role)
142
+
143
+ node = Node.objects.create(
144
+ hostname="terminal-node",
145
+ address="10.0.0.5",
146
+ port=8888,
147
+ mac_address="aa:bb:cc:dd:ee:ff",
148
+ role=role,
149
+ )
150
+
151
+ self.assertTrue(node.has_feature("clipboard-poll"))
152
+
130
153
 
131
154
  class NodeGetLocalDatabaseUnavailableTests(SimpleTestCase):
132
155
  def test_get_local_handles_database_errors(self):
@@ -317,7 +340,7 @@ class NodeGetLocalTests(TestCase):
317
340
  data={
318
341
  "hostname": "local",
319
342
  "address": "127.0.0.1",
320
- "port": 8000,
343
+ "port": 8888,
321
344
  "mac_address": "00:11:22:33:44:55",
322
345
  },
323
346
  content_type="application/json",
@@ -437,7 +460,7 @@ class NodeGetLocalTests(TestCase):
437
460
  hostname="best-ip",
438
461
  address="gateway.local",
439
462
  ipv4_address="198.51.100.5",
440
- port=8000,
463
+ port=8888,
441
464
  mac_address="00:11:22:33:44:77",
442
465
  )
443
466
  self.assertEqual(node.get_best_ip(), "198.51.100.5")
@@ -502,7 +525,7 @@ class NodeGetLocalTests(TestCase):
502
525
  data={
503
526
  "hostname": "lcd",
504
527
  "address": "127.0.0.1",
505
- "port": 8000,
528
+ "port": 8888,
506
529
  "mac_address": "00:aa:bb:cc:dd:ee",
507
530
  "features": ["clipboard-poll"],
508
531
  },
@@ -517,7 +540,7 @@ class NodeGetLocalTests(TestCase):
517
540
  data={
518
541
  "hostname": "lcd",
519
542
  "address": "127.0.0.1",
520
- "port": 8000,
543
+ "port": 8888,
521
544
  "mac_address": "00:aa:bb:cc:dd:ee",
522
545
  "features": [],
523
546
  },
@@ -684,7 +707,7 @@ class NodeGetLocalTests(TestCase):
684
707
  payload = {
685
708
  "hostname": "cors",
686
709
  "address": "127.0.0.1",
687
- "port": 8000,
710
+ "port": 8888,
688
711
  "mac_address": "10:20:30:40:50:60",
689
712
  }
690
713
  response = self.client.post(
@@ -702,7 +725,7 @@ class NodeGetLocalTests(TestCase):
702
725
  payload = {
703
726
  "hostname": "visitor",
704
727
  "address": "127.0.0.1",
705
- "port": 8000,
728
+ "port": 8888,
706
729
  "mac_address": "aa:bb:cc:dd:ee:00",
707
730
  }
708
731
  response = self.client.post(
@@ -746,7 +769,7 @@ class NodeGetLocalTests(TestCase):
746
769
  payload = {
747
770
  "hostname": "visitor",
748
771
  "address": "127.0.0.1",
749
- "port": 8000,
772
+ "port": 8888,
750
773
  "mac_address": "aa:bb:cc:dd:ee:11",
751
774
  "public_key": public_bytes,
752
775
  "token": token,
@@ -832,7 +855,7 @@ class NodeEnsureKeysTests(TestCase):
832
855
  self.node = Node.objects.create(
833
856
  hostname="ensure-host",
834
857
  address="127.0.0.1",
835
- port=8000,
858
+ port=8888,
836
859
  mac_address="00:11:22:33:44:55",
837
860
  )
838
861
 
@@ -888,7 +911,7 @@ class NodeInfoViewTests(TestCase):
888
911
  address="10.0.0.10",
889
912
  ipv4_address="10.0.0.10",
890
913
  ipv6_address="2001:db8::10",
891
- port=8000,
914
+ port=8888,
892
915
  mac_address=self.mac,
893
916
  public_endpoint="local",
894
917
  current_relation=Node.Relation.SELF,
@@ -952,7 +975,7 @@ class RegisterVisitorNodeMessageTests(TestCase):
952
975
  self.visitor = Node.objects.create(
953
976
  hostname="visitor-node",
954
977
  address="10.0.0.100",
955
- port=8000,
978
+ port=8888,
956
979
  mac_address="00:10:20:30:40:50",
957
980
  role=self.role,
958
981
  )
@@ -1134,7 +1157,7 @@ class NodeRegisterCurrentTests(TestCase):
1134
1157
  Node.objects.create(
1135
1158
  hostname="localnode",
1136
1159
  address="192.168.1.5",
1137
- port=8000,
1160
+ port=8888,
1138
1161
  mac_address="00:ff:ee:dd:cc:bb",
1139
1162
  installed_version="1.9.0",
1140
1163
  installed_revision="oldrev",
@@ -1192,7 +1215,7 @@ class NodeRegisterCurrentTests(TestCase):
1192
1215
  Node.objects.create(
1193
1216
  hostname="samever",
1194
1217
  address="192.168.1.6",
1195
- port=8000,
1218
+ port=8888,
1196
1219
  mac_address="00:ff:ee:dd:cc:cc",
1197
1220
  installed_version="1.0.0",
1198
1221
  installed_revision="rev1",
@@ -1372,7 +1395,7 @@ class NodeRegisterCurrentTests(TestCase):
1372
1395
  sender = Node.objects.create(
1373
1396
  hostname="sender",
1374
1397
  address="10.0.0.1",
1375
- port=8000,
1398
+ port=8888,
1376
1399
  mac_address="00:11:22:33:44:cc",
1377
1400
  public_key=public_key,
1378
1401
  )
@@ -1614,8 +1637,11 @@ class NodeRegisterCurrentTests(TestCase):
1614
1637
  port=9000,
1615
1638
  mac_address="00:11:22:33:44:99",
1616
1639
  )
1617
- task_name = f"poll_clipboard_node_{node.pk}"
1618
- PeriodicTask.objects.filter(name=task_name).delete()
1640
+ raw_name = f"poll_clipboard_node_{node.pk}"
1641
+ task_name = slugify_task_name(raw_name)
1642
+ PeriodicTask.objects.filter(
1643
+ name__in=periodic_task_name_variants(raw_name)
1644
+ ).delete()
1619
1645
  NodeFeatureAssignment.objects.create(node=node, feature=feature)
1620
1646
  self.assertTrue(PeriodicTask.objects.filter(name=task_name).exists())
1621
1647
  NodeFeatureAssignment.objects.filter(node=node, feature=feature).delete()
@@ -1632,8 +1658,11 @@ class NodeRegisterCurrentTests(TestCase):
1632
1658
  port=9100,
1633
1659
  mac_address="00:11:22:33:44:aa",
1634
1660
  )
1635
- task_name = f"capture_screenshot_node_{node.pk}"
1636
- PeriodicTask.objects.filter(name=task_name).delete()
1661
+ raw_name = f"capture_screenshot_node_{node.pk}"
1662
+ task_name = slugify_task_name(raw_name)
1663
+ PeriodicTask.objects.filter(
1664
+ name__in=periodic_task_name_variants(raw_name)
1665
+ ).delete()
1637
1666
  NodeFeatureAssignment.objects.create(node=node, feature=feature)
1638
1667
  self.assertTrue(PeriodicTask.objects.filter(name=task_name).exists())
1639
1668
  NodeFeatureAssignment.objects.filter(node=node, feature=feature).delete()
@@ -1652,14 +1681,18 @@ class NodeRegisterCurrentTests(TestCase):
1652
1681
  "base_path": settings.BASE_DIR,
1653
1682
  },
1654
1683
  )
1655
- PeriodicTask.objects.filter(name="pages_purge_landing_leads").delete()
1684
+ raw_name = "pages_purge_landing_leads"
1685
+ task_name = slugify_task_name(raw_name)
1686
+ PeriodicTask.objects.filter(
1687
+ name__in=periodic_task_name_variants(raw_name)
1688
+ ).delete()
1656
1689
  NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
1657
1690
  self.assertTrue(
1658
- PeriodicTask.objects.filter(name="pages_purge_landing_leads").exists()
1691
+ PeriodicTask.objects.filter(name=task_name).exists()
1659
1692
  )
1660
1693
  NodeFeatureAssignment.objects.filter(node=node, feature=feature).delete()
1661
1694
  self.assertFalse(
1662
- PeriodicTask.objects.filter(name="pages_purge_landing_leads").exists()
1695
+ PeriodicTask.objects.filter(name=task_name).exists()
1663
1696
  )
1664
1697
 
1665
1698
  def test_ocpp_session_report_task_syncs_with_feature(self):
@@ -1675,8 +1708,11 @@ class NodeRegisterCurrentTests(TestCase):
1675
1708
  "base_path": settings.BASE_DIR,
1676
1709
  },
1677
1710
  )
1678
- task_name = "ocpp_send_daily_session_report"
1679
- PeriodicTask.objects.filter(name=task_name).delete()
1711
+ raw_name = "ocpp_send_daily_session_report"
1712
+ task_name = slugify_task_name(raw_name)
1713
+ PeriodicTask.objects.filter(
1714
+ name__in=periodic_task_name_variants(raw_name)
1715
+ ).delete()
1680
1716
 
1681
1717
  with patch("nodes.models.mailer.can_send_email", return_value=True):
1682
1718
  NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
@@ -1699,8 +1735,11 @@ class NodeRegisterCurrentTests(TestCase):
1699
1735
  "base_path": settings.BASE_DIR,
1700
1736
  },
1701
1737
  )
1702
- task_name = "ocpp_send_daily_session_report"
1703
- PeriodicTask.objects.filter(name=task_name).delete()
1738
+ raw_name = "ocpp_send_daily_session_report"
1739
+ task_name = slugify_task_name(raw_name)
1740
+ PeriodicTask.objects.filter(
1741
+ name__in=periodic_task_name_variants(raw_name)
1742
+ ).delete()
1704
1743
 
1705
1744
  with patch("nodes.models.mailer.can_send_email", return_value=False):
1706
1745
  NodeFeatureAssignment.objects.get_or_create(node=node, feature=feature)
@@ -1736,7 +1775,7 @@ class NodeAdminTests(TestCase):
1736
1775
  return Node.objects.create(
1737
1776
  hostname="localnode",
1738
1777
  address="127.0.0.1",
1739
- port=8000,
1778
+ port=8888,
1740
1779
  mac_address=Node.get_current_mac(),
1741
1780
  )
1742
1781
 
@@ -1811,7 +1850,7 @@ class NodeAdminTests(TestCase):
1811
1850
  Node.objects.create(
1812
1851
  hostname=hostname,
1813
1852
  address="127.0.0.1",
1814
- port=8000,
1853
+ port=8888,
1815
1854
  mac_address=None,
1816
1855
  )
1817
1856
 
@@ -1942,7 +1981,7 @@ class NodeAdminTests(TestCase):
1942
1981
  local_node = Node.objects.create(
1943
1982
  hostname="local",
1944
1983
  address="127.0.0.1",
1945
- port=8000,
1984
+ port=8888,
1946
1985
  mac_address="00:11:22:33:44:aa",
1947
1986
  public_key="LOCAL-PUB",
1948
1987
  )
@@ -2189,6 +2228,34 @@ class NodeAdminTests(TestCase):
2189
2228
  response, "Completed 0 of 1 feature check(s) successfully.", html=False
2190
2229
  )
2191
2230
 
2231
+ @pytest.mark.feature("audio-capture")
2232
+ @patch("nodes.models.Node._has_audio_capture_device", return_value=False)
2233
+ def test_check_features_for_eligibility_audio_capture_requires_device(
2234
+ self, mock_device
2235
+ ):
2236
+ self._create_local_node()
2237
+ feature, _ = NodeFeature.objects.get_or_create(
2238
+ slug="audio-capture", defaults={"display": "Audio Capture"}
2239
+ )
2240
+ changelist_url = reverse("admin:nodes_nodefeature_changelist")
2241
+ response = self.client.post(
2242
+ changelist_url,
2243
+ {
2244
+ "action": "check_features_for_eligibility",
2245
+ "_selected_action": [str(feature.pk)],
2246
+ },
2247
+ follow=True,
2248
+ )
2249
+ self.assertEqual(response.status_code, 200)
2250
+ self.assertContains(
2251
+ response,
2252
+ "No audio recording device detected on localnode for Audio Capture. This feature can be enabled manually.",
2253
+ html=False,
2254
+ )
2255
+ self.assertContains(
2256
+ response, "Completed 0 of 1 feature check(s) successfully.", html=False
2257
+ )
2258
+
2192
2259
  @pytest.mark.feature("screenshot-poll")
2193
2260
  def test_enable_selected_features_enables_manual_feature(self):
2194
2261
  node = self._create_local_node()
@@ -2756,7 +2823,7 @@ class NodeProxyGatewayTests(TestCase):
2756
2823
  self.node = Node.objects.create(
2757
2824
  hostname="requester",
2758
2825
  address="127.0.0.1",
2759
- port=8000,
2826
+ port=8888,
2760
2827
  mac_address="aa:bb:cc:dd:ee:aa",
2761
2828
  public_key=public_key,
2762
2829
  )
@@ -2981,7 +3048,7 @@ class RFIDExportViewTests(TestCase):
2981
3048
  self.local_node = Node.objects.create(
2982
3049
  hostname="local",
2983
3050
  address="127.0.0.1",
2984
- port=8000,
3051
+ port=8888,
2985
3052
  mac_address=Node.get_current_mac(),
2986
3053
  )
2987
3054
  self.remote_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
@@ -3107,7 +3174,7 @@ class NetMessageReachTests(TestCase):
3107
3174
  self.nodes[name] = Node.objects.create(
3108
3175
  hostname=name.lower(),
3109
3176
  address=f"10.0.0.{idx}",
3110
- port=8000 + idx,
3177
+ port=8888 + idx,
3111
3178
  mac_address=f"00:11:22:33:44:{idx:02x}",
3112
3179
  role=self.roles[name],
3113
3180
  )
@@ -3318,7 +3385,7 @@ class NetMessagePropagationTests(TestCase):
3318
3385
  Node.objects.create(
3319
3386
  hostname=f"n{idx}",
3320
3387
  address=f"10.0.0.{idx}",
3321
- port=8000 + idx,
3388
+ port=8888 + idx,
3322
3389
  mac_address=f"00:11:22:33:44:{idx:02x}",
3323
3390
  role=self.role,
3324
3391
  public_endpoint=f"n{idx}",
@@ -3365,7 +3432,7 @@ class NetMessagePropagationTests(TestCase):
3365
3432
  Node.objects.create(
3366
3433
  hostname=f"n{idx}",
3367
3434
  address=f"10.0.0.{idx}",
3368
- port=8000 + idx,
3435
+ port=8888 + idx,
3369
3436
  mac_address=f"00:11:22:33:44:{idx:02x}",
3370
3437
  role=self.role,
3371
3438
  public_endpoint=f"n{idx}",
@@ -3454,7 +3521,7 @@ class NetMessageQueueTests(TestCase):
3454
3521
  local = Node.objects.create(
3455
3522
  hostname="local",
3456
3523
  address="10.0.0.1",
3457
- port=8000,
3524
+ port=8888,
3458
3525
  mac_address="00:11:22:33:44:10",
3459
3526
  role=self.role,
3460
3527
  public_endpoint="local",
@@ -3753,7 +3820,7 @@ class NetworkChargerActionSecurityTests(TestCase):
3753
3820
  self.local_node = Node.objects.create(
3754
3821
  hostname="local-node",
3755
3822
  address="127.0.0.1",
3756
- port=8000,
3823
+ port=8888,
3757
3824
  mac_address="00:aa:bb:cc:dd:10",
3758
3825
  public_endpoint="local-endpoint",
3759
3826
  )
@@ -4026,7 +4093,7 @@ class ContentSampleAdminTests(TestCase):
4026
4093
  Node.objects.create(
4027
4094
  hostname="host",
4028
4095
  address="127.0.0.1",
4029
- port=8000,
4096
+ port=8888,
4030
4097
  mac_address=Node.get_current_mac(),
4031
4098
  )
4032
4099
  url = reverse("admin:nodes_contentsample_from_clipboard")
@@ -4052,7 +4119,7 @@ class EmailOutboxTests(TestCase):
4052
4119
  node = Node.objects.create(
4053
4120
  hostname="outboxhost",
4054
4121
  address="127.0.0.1",
4055
- port=8000,
4122
+ port=8888,
4056
4123
  mac_address="00:11:22:33:aa:bb",
4057
4124
  )
4058
4125
  outbox = EmailOutbox.objects.create(
@@ -4068,7 +4135,7 @@ class EmailOutboxTests(TestCase):
4068
4135
  node = Node.objects.create(
4069
4136
  hostname="host",
4070
4137
  address="127.0.0.1",
4071
- port=8000,
4138
+ port=8888,
4072
4139
  mac_address="00:11:22:33:cc:dd",
4073
4140
  )
4074
4141
  node.send_mail("sub", "msg", ["to@example.com"])
@@ -4181,7 +4248,7 @@ class ClipboardTaskTests(TestCase):
4181
4248
  Node.objects.create(
4182
4249
  hostname="host",
4183
4250
  address="127.0.0.1",
4184
- port=8000,
4251
+ port=8888,
4185
4252
  mac_address=Node.get_current_mac(),
4186
4253
  )
4187
4254
  sample_clipboard()
@@ -4205,7 +4272,7 @@ class ClipboardTaskTests(TestCase):
4205
4272
  node = Node.objects.create(
4206
4273
  hostname="host",
4207
4274
  address="127.0.0.1",
4208
- port=8000,
4275
+ port=8888,
4209
4276
  mac_address=Node.get_current_mac(),
4210
4277
  )
4211
4278
  screenshot_dir = settings.LOG_DIR / "screenshots"
@@ -4228,7 +4295,7 @@ class ClipboardTaskTests(TestCase):
4228
4295
  Node.objects.create(
4229
4296
  hostname="host",
4230
4297
  address="127.0.0.1",
4231
- port=8000,
4298
+ port=8888,
4232
4299
  mac_address=Node.get_current_mac(),
4233
4300
  )
4234
4301
  mock_capture.side_effect = RuntimeError("boom")
@@ -4290,14 +4357,14 @@ class NodeRoleAdminTests(TestCase):
4290
4357
  node1 = Node.objects.create(
4291
4358
  hostname="n1",
4292
4359
  address="127.0.0.1",
4293
- port=8000,
4360
+ port=8888,
4294
4361
  mac_address="00:11:22:33:44:55",
4295
4362
  role=role,
4296
4363
  )
4297
4364
  node2 = Node.objects.create(
4298
4365
  hostname="n2",
4299
4366
  address="127.0.0.2",
4300
- port=8000,
4367
+ port=8888,
4301
4368
  mac_address="00:11:22:33:44:66",
4302
4369
  )
4303
4370
  url = reverse("admin:nodes_noderole_change", args=[role.pk])
@@ -4316,7 +4383,7 @@ class NodeRoleAdminTests(TestCase):
4316
4383
  Node.objects.create(
4317
4384
  hostname="n1",
4318
4385
  address="127.0.0.1",
4319
- port=8000,
4386
+ port=8888,
4320
4387
  mac_address="00:11:22:33:44:77",
4321
4388
  role=role,
4322
4389
  )
@@ -4379,7 +4446,7 @@ class NodeFeatureTests(TestCase):
4379
4446
  self.node = Node.objects.create(
4380
4447
  hostname="local",
4381
4448
  address="127.0.0.1",
4382
- port=8000,
4449
+ port=8888,
4383
4450
  mac_address="00:11:22:33:44:55",
4384
4451
  role=self.role,
4385
4452
  )
@@ -4552,7 +4619,7 @@ class NodeFeatureTests(TestCase):
4552
4619
  node = Node.objects.create(
4553
4620
  hostname="control",
4554
4621
  address="127.0.0.1",
4555
- port=8000,
4622
+ port=8888,
4556
4623
  mac_address=mac,
4557
4624
  role=control_role,
4558
4625
  )
@@ -4576,7 +4643,7 @@ class NodeFeatureTests(TestCase):
4576
4643
  node = Node.objects.create(
4577
4644
  hostname="control",
4578
4645
  address="127.0.0.1",
4579
- port=8000,
4646
+ port=8888,
4580
4647
  mac_address=mac,
4581
4648
  role=control_role,
4582
4649
  base_path=str(Path(tmp)),
@@ -4597,7 +4664,7 @@ class NodeFeatureTests(TestCase):
4597
4664
  node = Node.objects.create(
4598
4665
  hostname="control",
4599
4666
  address="127.0.0.1",
4600
- port=8000,
4667
+ port=8888,
4601
4668
  mac_address=mac,
4602
4669
  role=control_role,
4603
4670
  )
@@ -4612,6 +4679,72 @@ class NodeFeatureTests(TestCase):
4612
4679
  )
4613
4680
 
4614
4681
 
4682
+ class AudioCaptureDetectionTests(TestCase):
4683
+ def test_has_audio_capture_device_true(self):
4684
+ with TemporaryDirectory() as tmp:
4685
+ pcm_path = Path(tmp) / "pcm"
4686
+ pcm_path.write_text(
4687
+ "00-00: USB Audio : USB Audio : playback 1 : capture 1\n",
4688
+ encoding="utf-8",
4689
+ )
4690
+ with patch.object(Node, "AUDIO_CAPTURE_PCM_PATH", pcm_path):
4691
+ self.assertTrue(Node._has_audio_capture_device())
4692
+
4693
+ def test_has_audio_capture_device_false_without_capture(self):
4694
+ with TemporaryDirectory() as tmp:
4695
+ pcm_path = Path(tmp) / "pcm"
4696
+ pcm_path.write_text(
4697
+ "00-00: USB Audio : USB Audio : playback 1\n",
4698
+ encoding="utf-8",
4699
+ )
4700
+ with patch.object(Node, "AUDIO_CAPTURE_PCM_PATH", pcm_path):
4701
+ self.assertFalse(Node._has_audio_capture_device())
4702
+
4703
+ def test_has_audio_capture_device_false_when_file_missing(self):
4704
+ with TemporaryDirectory() as tmp:
4705
+ pcm_path = Path(tmp) / "pcm"
4706
+ with patch.object(Node, "AUDIO_CAPTURE_PCM_PATH", pcm_path):
4707
+ self.assertFalse(Node._has_audio_capture_device())
4708
+
4709
+
4710
+ class AudioCaptureFeatureCheckTests(TestCase):
4711
+ def setUp(self):
4712
+ self.node = Node.objects.create(
4713
+ hostname="localnode",
4714
+ address="127.0.0.1",
4715
+ port=8888,
4716
+ mac_address=Node.get_current_mac(),
4717
+ )
4718
+ self.feature, _ = NodeFeature.objects.get_or_create(
4719
+ slug="audio-capture", defaults={"display": "Audio Capture"}
4720
+ )
4721
+
4722
+ @pytest.mark.feature("audio-capture")
4723
+ @patch("nodes.models.Node._has_audio_capture_device", return_value=False)
4724
+ def test_feature_check_warns_without_device(self, mock_device):
4725
+ result = feature_checks.run(self.feature, node=self.node)
4726
+ self.assertFalse(result.success)
4727
+ self.assertIn("No audio recording device detected", result.message)
4728
+ self.assertEqual(result.level, messages.WARNING)
4729
+
4730
+ @pytest.mark.feature("audio-capture")
4731
+ @patch("nodes.models.Node._has_audio_capture_device", return_value=True)
4732
+ def test_feature_check_warns_when_feature_disabled(self, mock_device):
4733
+ result = feature_checks.run(self.feature, node=self.node)
4734
+ self.assertFalse(result.success)
4735
+ self.assertIn("is not enabled", result.message)
4736
+ self.assertEqual(result.level, messages.WARNING)
4737
+
4738
+ @pytest.mark.feature("audio-capture")
4739
+ @patch("nodes.models.Node._has_audio_capture_device", return_value=True)
4740
+ def test_feature_check_passes_when_enabled(self, mock_device):
4741
+ NodeFeatureAssignment.objects.get_or_create(node=self.node, feature=self.feature)
4742
+ result = feature_checks.run(self.feature, node=self.node)
4743
+ self.assertTrue(result.success)
4744
+ self.assertIn("recording device is available", result.message)
4745
+ self.assertEqual(result.level, messages.SUCCESS)
4746
+
4747
+
4615
4748
  class CeleryReportAdminViewTests(TestCase):
4616
4749
  def setUp(self):
4617
4750
  User = get_user_model()