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

Files changed (112) hide show
  1. arthexis-0.1.26.dist-info/METADATA +272 -0
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +29 -29
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -25
  9. config/context_processors.py +67 -68
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +71 -25
  14. config/offline.py +49 -49
  15. config/settings.py +676 -492
  16. config/settings_helpers.py +109 -0
  17. config/urls.py +228 -159
  18. config/wsgi.py +17 -17
  19. core/admin.py +4052 -2066
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +192 -151
  22. core/apps.py +350 -223
  23. core/auto_upgrade.py +72 -0
  24. core/backends.py +311 -124
  25. core/changelog.py +403 -0
  26. core/entity.py +149 -133
  27. core/environment.py +60 -43
  28. core/fields.py +168 -75
  29. core/form_fields.py +75 -0
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +183 -172
  32. core/github_repos.py +72 -0
  33. core/lcd_screen.py +78 -78
  34. core/liveupdate.py +25 -25
  35. core/log_paths.py +114 -100
  36. core/mailer.py +89 -83
  37. core/middleware.py +91 -91
  38. core/models.py +5041 -2195
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +107 -0
  42. core/release.py +940 -346
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -131
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +250 -284
  47. core/system.py +1425 -230
  48. core/tasks.py +538 -199
  49. core/temp_passwords.py +181 -0
  50. core/test_system_info.py +202 -43
  51. core/tests.py +2673 -1069
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +681 -495
  55. core/views.py +2484 -789
  56. core/widgets.py +213 -51
  57. nodes/admin.py +2236 -445
  58. nodes/apps.py +98 -70
  59. nodes/backends.py +160 -53
  60. nodes/dns.py +203 -0
  61. nodes/feature_checks.py +133 -0
  62. nodes/lcd.py +165 -165
  63. nodes/models.py +2375 -870
  64. nodes/reports.py +411 -0
  65. nodes/rfid_sync.py +210 -0
  66. nodes/signals.py +18 -0
  67. nodes/tasks.py +141 -46
  68. nodes/tests.py +5045 -1489
  69. nodes/urls.py +29 -13
  70. nodes/utils.py +172 -73
  71. nodes/views.py +1768 -304
  72. ocpp/admin.py +1775 -481
  73. ocpp/apps.py +25 -25
  74. ocpp/consumers.py +1843 -630
  75. ocpp/evcs.py +844 -928
  76. ocpp/evcs_discovery.py +158 -0
  77. ocpp/models.py +1417 -640
  78. ocpp/network.py +398 -0
  79. ocpp/reference_utils.py +42 -0
  80. ocpp/routing.py +11 -9
  81. ocpp/simulator.py +745 -368
  82. ocpp/status_display.py +26 -0
  83. ocpp/store.py +603 -403
  84. ocpp/tasks.py +479 -31
  85. ocpp/test_export_import.py +131 -130
  86. ocpp/test_rfid.py +1072 -540
  87. ocpp/tests.py +5494 -2296
  88. ocpp/transactions_io.py +197 -165
  89. ocpp/urls.py +50 -50
  90. ocpp/views.py +2024 -912
  91. pages/admin.py +1123 -396
  92. pages/apps.py +45 -10
  93. pages/checks.py +40 -40
  94. pages/context_processors.py +151 -85
  95. pages/defaults.py +13 -0
  96. pages/forms.py +221 -0
  97. pages/middleware.py +213 -153
  98. pages/models.py +720 -252
  99. pages/module_defaults.py +156 -0
  100. pages/site_config.py +137 -0
  101. pages/tasks.py +74 -0
  102. pages/tests.py +4009 -1389
  103. pages/urls.py +38 -20
  104. pages/utils.py +93 -12
  105. pages/views.py +1736 -762
  106. arthexis-0.1.9.dist-info/METADATA +0 -168
  107. arthexis-0.1.9.dist-info/RECORD +0 -92
  108. core/workgroup_urls.py +0 -17
  109. core/workgroup_views.py +0 -94
  110. nodes/actions.py +0 -70
  111. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  112. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
ocpp/test_rfid.py CHANGED
@@ -1,540 +1,1072 @@
1
- import io
2
- import os
3
- import sys
4
- import types
5
- from pathlib import Path
6
- from unittest.mock import patch, MagicMock, call
7
-
8
- import pytest
9
-
10
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
11
-
12
- import django
13
-
14
- django.setup()
15
-
16
- from django.test import SimpleTestCase, TestCase
17
- from django.urls import reverse
18
- from django.contrib.auth import get_user_model
19
- from django.contrib.sites.models import Site
20
-
21
- from pages.models import Application, Module
22
- from nodes.models import Node, NodeRole
23
-
24
- from core.models import RFID
25
- from ocpp.rfid.reader import read_rfid, enable_deep_read
26
- from ocpp.rfid.detect import detect_scanner, main as detect_main
27
- from ocpp.rfid import background_reader
28
- from ocpp.rfid.constants import (
29
- DEFAULT_IRQ_PIN,
30
- DEFAULT_RST_PIN,
31
- GPIO_PIN_MODE_BCM,
32
- MODULE_WIRING,
33
- SPI_BUS,
34
- SPI_DEVICE,
35
- )
36
-
37
-
38
- pytestmark = [pytest.mark.feature("rfid-scanner")]
39
-
40
-
41
- class BackgroundReaderConfigurationTests(SimpleTestCase):
42
- def setUp(self):
43
- background_reader._auto_detect_logged = False
44
-
45
- def tearDown(self):
46
- background_reader._auto_detect_logged = False
47
-
48
- def test_is_configured_auto_detects_without_lock(self):
49
- fake_lock = Path("/tmp/rfid-auto-detect.lock")
50
- with (
51
- patch("ocpp.rfid.background_reader._lock_path", return_value=fake_lock),
52
- patch("ocpp.rfid.background_reader._has_spi_device", return_value=True),
53
- patch(
54
- "ocpp.rfid.background_reader._dependencies_available",
55
- return_value=True,
56
- ),
57
- ):
58
- self.assertTrue(background_reader.is_configured())
59
-
60
- def test_is_configured_requires_dependencies(self):
61
- fake_lock = Path("/tmp/rfid-auto-detect.lock")
62
- with (
63
- patch("ocpp.rfid.background_reader._lock_path", return_value=fake_lock),
64
- patch("ocpp.rfid.background_reader._has_spi_device", return_value=True),
65
- patch(
66
- "ocpp.rfid.background_reader._dependencies_available",
67
- return_value=False,
68
- ),
69
- ):
70
- self.assertFalse(background_reader.is_configured())
71
-
72
-
73
- class ScanNextViewTests(TestCase):
74
- @patch("config.middleware.Node.get_local", return_value=None)
75
- @patch("config.middleware.get_site")
76
- @patch(
77
- "ocpp.rfid.views.scan_sources",
78
- return_value={
79
- "rfid": "ABCD1234",
80
- "label_id": 1,
81
- "created": False,
82
- "kind": RFID.CLASSIC,
83
- },
84
- )
85
- def test_scan_next_success(self, mock_scan, mock_site, mock_node):
86
- resp = self.client.get(reverse("rfid-scan-next"))
87
- self.assertEqual(resp.status_code, 200)
88
- self.assertEqual(
89
- resp.json(),
90
- {
91
- "rfid": "ABCD1234",
92
- "label_id": 1,
93
- "created": False,
94
- "kind": RFID.CLASSIC,
95
- },
96
- )
97
-
98
- @patch("config.middleware.Node.get_local", return_value=None)
99
- @patch("config.middleware.get_site")
100
- @patch("ocpp.rfid.views.scan_sources", return_value={"error": "boom"})
101
- def test_scan_next_error(self, mock_scan, mock_site, mock_node):
102
- resp = self.client.get(reverse("rfid-scan-next"))
103
- self.assertEqual(resp.status_code, 500)
104
- self.assertEqual(resp.json(), {"error": "boom"})
105
-
106
-
107
- class ReaderNotificationTests(TestCase):
108
- def _mock_reader(self):
109
- class MockReader:
110
- MI_OK = 1
111
- PICC_REQIDL = 0
112
-
113
- def MFRC522_Request(self, _):
114
- return (self.MI_OK, None)
115
-
116
- def MFRC522_Anticoll(self):
117
- return (self.MI_OK, [0xAB, 0xCD, 0x12, 0x34, 0x56])
118
-
119
- def MFRC522_SelectTag(self, _uid):
120
- self.select_called = True
121
- return self.MI_OK
122
-
123
- def MFRC522_StopCrypto1(self):
124
- self.stop_called = True
125
-
126
- return MockReader()
127
-
128
- @patch("ocpp.rfid.reader.notify_async")
129
- @patch("core.models.RFID.objects.get_or_create")
130
- def test_notify_on_allowed_tag(self, mock_get, mock_notify):
131
- reference = MagicMock(value="https://example.com")
132
- tag = MagicMock(
133
- label_id=1,
134
- pk=1,
135
- allowed=True,
136
- color="B",
137
- released=False,
138
- reference=reference,
139
- )
140
- mock_get.return_value = (tag, False)
141
-
142
- reader = self._mock_reader()
143
- result = read_rfid(mfrc=reader, cleanup=False)
144
- self.assertEqual(result["label_id"], 1)
145
- self.assertEqual(result["kind"], RFID.CLASSIC)
146
- self.assertEqual(result["reference"], "https://example.com")
147
- self.assertEqual(mock_notify.call_count, 1)
148
- mock_notify.assert_has_calls([call("RFID 1 OK", f"{result['rfid']} B")])
149
- self.assertTrue(getattr(reader, "select_called", False))
150
- self.assertTrue(getattr(reader, "stop_called", False))
151
-
152
- @patch("ocpp.rfid.reader.notify_async")
153
- @patch("core.models.RFID.objects.get_or_create")
154
- def test_notify_on_disallowed_tag(self, mock_get, mock_notify):
155
- tag = MagicMock(
156
- label_id=2,
157
- pk=2,
158
- allowed=False,
159
- color="B",
160
- released=False,
161
- reference=None,
162
- )
163
- mock_get.return_value = (tag, False)
164
-
165
- reader = self._mock_reader()
166
- result = read_rfid(mfrc=reader, cleanup=False)
167
- self.assertEqual(result["kind"], RFID.CLASSIC)
168
- self.assertEqual(mock_notify.call_count, 1)
169
- mock_notify.assert_has_calls([call("RFID 2 BAD", f"{result['rfid']} B")])
170
- self.assertTrue(getattr(reader, "select_called", False))
171
- self.assertTrue(getattr(reader, "stop_called", False))
172
-
173
-
174
- class CardTypeDetectionTests(TestCase):
175
- def _mock_ntag_reader(self):
176
- class MockReader:
177
- MI_OK = 1
178
- PICC_REQIDL = 0
179
-
180
- def MFRC522_Request(self, _):
181
- return (self.MI_OK, None)
182
-
183
- def MFRC522_Anticoll(self):
184
- return (
185
- self.MI_OK,
186
- [0x04, 0xD3, 0x2A, 0x1B, 0x5F, 0x23, 0x19],
187
- )
188
-
189
- def MFRC522_SelectTag(self, _uid):
190
- self.select_called = True
191
- return self.MI_OK
192
-
193
- def MFRC522_StopCrypto1(self):
194
- self.stop_called = True
195
-
196
- return MockReader()
197
-
198
- @patch("ocpp.rfid.reader.notify_async")
199
- @patch("core.models.RFID.objects.get_or_create")
200
- def test_detects_ntag215(self, mock_get, _mock_notify):
201
- tag = MagicMock(
202
- pk=1,
203
- label_id=1,
204
- allowed=True,
205
- color="B",
206
- released=False,
207
- reference=None,
208
- kind=RFID.NTAG215,
209
- )
210
- mock_get.return_value = (tag, True)
211
- reader = self._mock_ntag_reader()
212
- result = read_rfid(mfrc=reader, cleanup=False)
213
- self.assertEqual(result["kind"], RFID.NTAG215)
214
- self.assertTrue(getattr(reader, "select_called", False))
215
- self.assertTrue(getattr(reader, "stop_called", False))
216
-
217
-
218
- class RFIDLastSeenTests(TestCase):
219
- def _mock_reader(self):
220
- class MockReader:
221
- MI_OK = 1
222
- PICC_REQIDL = 0
223
-
224
- def MFRC522_Request(self, _):
225
- return (self.MI_OK, None)
226
-
227
- def MFRC522_Anticoll(self):
228
- return (self.MI_OK, [0xAB, 0xCD, 0x12, 0x34])
229
-
230
- def MFRC522_SelectTag(self, _uid):
231
- self.select_called = True
232
- return self.MI_OK
233
-
234
- def MFRC522_StopCrypto1(self):
235
- self.stop_called = True
236
-
237
- return MockReader()
238
-
239
- @patch("ocpp.rfid.reader.notify_async")
240
- def test_last_seen_updated_on_read(self, _mock_notify):
241
- tag = RFID.objects.create(rfid="ABCD1234")
242
- reader = self._mock_reader()
243
- result = read_rfid(mfrc=reader, cleanup=False)
244
- tag.refresh_from_db()
245
- self.assertIsNotNone(tag.last_seen_on)
246
- self.assertEqual(result["kind"], RFID.CLASSIC)
247
- self.assertTrue(getattr(reader, "select_called", False))
248
- self.assertTrue(getattr(reader, "stop_called", False))
249
-
250
-
251
- class RFIDDetectionScriptTests(SimpleTestCase):
252
- @patch("ocpp.rfid.detect._ensure_django")
253
- @patch(
254
- "ocpp.rfid.irq_wiring_check.check_irq_pin",
255
- return_value={"irq_pin": DEFAULT_IRQ_PIN},
256
- )
257
- def test_detect_scanner_success(self, mock_check, _mock_setup):
258
- result = detect_scanner()
259
- self.assertEqual(
260
- result,
261
- {
262
- "detected": True,
263
- "irq_pin": DEFAULT_IRQ_PIN,
264
- },
265
- )
266
- mock_check.assert_called_once()
267
-
268
- @patch("ocpp.rfid.detect._ensure_django")
269
- @patch(
270
- "ocpp.rfid.irq_wiring_check.check_irq_pin",
271
- return_value={"error": "no scanner detected"},
272
- )
273
- def test_detect_scanner_failure(self, mock_check, _mock_setup):
274
- result = detect_scanner()
275
- self.assertFalse(result["detected"])
276
- self.assertEqual(result["reason"], "no scanner detected")
277
- mock_check.assert_called_once()
278
-
279
- @patch(
280
- "ocpp.rfid.detect.detect_scanner",
281
- return_value={"detected": True, "irq_pin": DEFAULT_IRQ_PIN},
282
- )
283
- def test_detect_main_success_output(self, mock_detect):
284
- buffer = io.StringIO()
285
- with patch("sys.stdout", new=buffer):
286
- exit_code = detect_main([])
287
- self.assertEqual(exit_code, 0)
288
- self.assertIn("IRQ pin", buffer.getvalue())
289
- mock_detect.assert_called_once()
290
-
291
- @patch(
292
- "ocpp.rfid.detect.detect_scanner",
293
- return_value={"detected": False, "reason": "missing hardware"},
294
- )
295
- def test_detect_main_failure_output(self, mock_detect):
296
- buffer = io.StringIO()
297
- with patch("sys.stdout", new=buffer):
298
- exit_code = detect_main([])
299
- self.assertEqual(exit_code, 1)
300
- self.assertIn("missing hardware", buffer.getvalue())
301
- mock_detect.assert_called_once()
302
-
303
-
304
- class RestartViewTests(SimpleTestCase):
305
- @patch("config.middleware.Node.get_local", return_value=None)
306
- @patch("config.middleware.get_site")
307
- @patch("ocpp.rfid.views.restart_sources", return_value={"status": "restarted"})
308
- def test_restart_endpoint(self, mock_restart, mock_site, mock_node):
309
- resp = self.client.post(reverse("rfid-scan-restart"))
310
- self.assertEqual(resp.status_code, 200)
311
- self.assertEqual(resp.json(), {"status": "restarted"})
312
- mock_restart.assert_called_once()
313
-
314
-
315
- class ScanTestViewTests(SimpleTestCase):
316
- @patch("config.middleware.Node.get_local", return_value=None)
317
- @patch("config.middleware.get_site")
318
- @patch("ocpp.rfid.views.test_sources", return_value={"irq_pin": 7})
319
- def test_scan_test_success(self, mock_test, mock_site, mock_node):
320
- resp = self.client.get(reverse("rfid-scan-test"))
321
- self.assertEqual(resp.status_code, 200)
322
- self.assertEqual(resp.json(), {"irq_pin": 7})
323
-
324
- @patch("config.middleware.Node.get_local", return_value=None)
325
- @patch("config.middleware.get_site")
326
- @patch(
327
- "ocpp.rfid.views.test_sources",
328
- return_value={"error": "no scanner detected"},
329
- )
330
- def test_scan_test_error(self, mock_test, mock_site, mock_node):
331
- resp = self.client.get(reverse("rfid-scan-test"))
332
- self.assertEqual(resp.status_code, 500)
333
- self.assertEqual(resp.json(), {"error": "no scanner detected"})
334
-
335
-
336
- class RFIDLandingTests(TestCase):
337
- def test_scanner_view_registered_as_landing(self):
338
- role, _ = NodeRole.objects.get_or_create(name="Terminal")
339
- Node.objects.update_or_create(
340
- mac_address=Node.get_current_mac(),
341
- defaults={"hostname": "localhost", "address": "127.0.0.1", "role": role},
342
- )
343
- Site.objects.update_or_create(
344
- id=1, defaults={"domain": "testserver", "name": ""}
345
- )
346
- app = Application.objects.create(name="Ocpp")
347
- module = Module.objects.create(node_role=role, application=app, path="/ocpp/")
348
- module.create_landings()
349
- self.assertTrue(module.landings.filter(path="/ocpp/rfid/").exists())
350
-
351
-
352
- class ScannerTemplateTests(TestCase):
353
- def setUp(self):
354
- self.url = reverse("rfid-reader")
355
-
356
- def test_configure_link_for_staff(self):
357
- User = get_user_model()
358
- staff = User.objects.create_user("staff", password="pwd", is_staff=True)
359
- self.client.force_login(staff)
360
- resp = self.client.get(self.url)
361
- self.assertContains(resp, 'id="rfid-configure"')
362
-
363
- def test_no_link_for_anonymous(self):
364
- resp = self.client.get(self.url)
365
- self.assertNotContains(resp, 'id="rfid-configure"')
366
-
367
- def test_advanced_fields_for_staff(self):
368
- User = get_user_model()
369
- staff = User.objects.create_user("staff2", password="pwd", is_staff=True)
370
- self.client.force_login(staff)
371
- resp = self.client.get(self.url)
372
- self.assertContains(resp, 'id="rfid-kind"')
373
- self.assertContains(resp, 'id="rfid-rfid"')
374
- self.assertContains(resp, 'id="rfid-released"')
375
- self.assertContains(resp, 'id="rfid-reference"')
376
-
377
- def test_basic_fields_for_public(self):
378
- resp = self.client.get(self.url)
379
- self.assertContains(resp, 'id="rfid-kind"')
380
- self.assertNotContains(resp, 'id="rfid-rfid"')
381
- self.assertNotContains(resp, 'id="rfid-released"')
382
- self.assertNotContains(resp, 'id="rfid-reference"')
383
-
384
- def test_deep_read_button_for_staff(self):
385
- User = get_user_model()
386
- staff = User.objects.create_user("staff3", password="pwd", is_staff=True)
387
- self.client.force_login(staff)
388
- resp = self.client.get(self.url)
389
- self.assertContains(resp, 'id="rfid-deep-read"')
390
-
391
- def test_no_deep_read_button_for_public(self):
392
- resp = self.client.get(self.url)
393
- self.assertNotContains(resp, 'id="rfid-deep-read"')
394
-
395
-
396
- class ReaderPollingTests(SimpleTestCase):
397
- def _mock_reader_no_tag(self):
398
- class MockReader:
399
- MI_OK = 1
400
- PICC_REQIDL = 0
401
-
402
- def MFRC522_Request(self, _):
403
- return (0, None)
404
-
405
- return MockReader()
406
-
407
- @patch("ocpp.rfid.reader.time.sleep")
408
- def test_poll_interval_used(self, mock_sleep):
409
- read_rfid(
410
- mfrc=self._mock_reader_no_tag(),
411
- cleanup=False,
412
- timeout=0.002,
413
- poll_interval=0.001,
414
- )
415
- mock_sleep.assert_called_with(0.001)
416
-
417
- @patch("ocpp.rfid.reader.time.sleep")
418
- def test_use_irq_skips_sleep(self, mock_sleep):
419
- read_rfid(
420
- mfrc=self._mock_reader_no_tag(),
421
- cleanup=False,
422
- timeout=0.002,
423
- use_irq=True,
424
- )
425
- mock_sleep.assert_not_called()
426
-
427
-
428
- class DeepReadViewTests(TestCase):
429
- @patch("config.middleware.Node.get_local", return_value=None)
430
- @patch("config.middleware.get_site")
431
- @patch(
432
- "ocpp.rfid.views.enable_deep_read_mode",
433
- return_value={"status": "deep", "timeout": 60},
434
- )
435
- def test_enable_deep_read(self, mock_enable, mock_site, mock_node):
436
- User = get_user_model()
437
- staff = User.objects.create_user("staff4", password="pwd", is_staff=True)
438
- self.client.force_login(staff)
439
- resp = self.client.post(reverse("rfid-scan-deep"))
440
- self.assertEqual(resp.status_code, 200)
441
- self.assertEqual(resp.json(), {"status": "deep", "timeout": 60})
442
- mock_enable.assert_called_once()
443
-
444
- def test_forbidden_for_anonymous(self):
445
- resp = self.client.post(reverse("rfid-scan-deep"))
446
- self.assertNotEqual(resp.status_code, 200)
447
-
448
-
449
- class DeepReadAuthTests(TestCase):
450
- class MockReader:
451
- MI_OK = 1
452
- MI_ERR = 2
453
- PICC_REQIDL = 0
454
- PICC_AUTHENT1A = 0x60
455
- PICC_AUTHENT1B = 0x61
456
-
457
- def __init__(self):
458
- self.auth_calls = []
459
-
460
- def MFRC522_Request(self, _):
461
- return (self.MI_OK, None)
462
-
463
- def MFRC522_Anticoll(self):
464
- return (self.MI_OK, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE])
465
-
466
- def MFRC522_Auth(self, mode, block, key, uid):
467
- self.auth_calls.append(mode)
468
- return self.MI_ERR if mode == self.PICC_AUTHENT1A else self.MI_OK
469
-
470
- def MFRC522_Read(self, block):
471
- return (self.MI_OK, [0] * 16)
472
-
473
- @patch("core.notifications.notify_async")
474
- @patch("core.models.RFID.objects.get_or_create")
475
- def test_auth_tries_key_a_then_b(self, mock_get, mock_notify):
476
- tag = MagicMock(
477
- label_id=1,
478
- pk=1,
479
- allowed=True,
480
- color="B",
481
- released=False,
482
- reference=None,
483
- )
484
- mock_get.return_value = (tag, False)
485
- reader = self.MockReader()
486
- enable_deep_read(60)
487
- read_rfid(mfrc=reader, cleanup=False)
488
- self.assertGreaterEqual(len(reader.auth_calls), 2)
489
- self.assertEqual(reader.auth_calls[0], reader.PICC_AUTHENT1A)
490
- self.assertEqual(reader.auth_calls[1], reader.PICC_AUTHENT1B)
491
-
492
-
493
- class RFIDWiringConfigTests(SimpleTestCase):
494
- def test_module_wiring_map(self):
495
- expected = [
496
- ("SDA", "CE0"),
497
- ("SCK", "SCLK"),
498
- ("MOSI", "MOSI"),
499
- ("MISO", "MISO"),
500
- ("IRQ", "IO4"),
501
- ("GND", "GND"),
502
- ("RST", "IO25"),
503
- ("3v3", "3v3"),
504
- ]
505
- self.assertEqual(list(MODULE_WIRING.items()), expected)
506
- self.assertEqual(DEFAULT_IRQ_PIN, 4)
507
- self.assertEqual(DEFAULT_RST_PIN, 25)
508
-
509
- def test_background_reader_uses_default_irq_pin(self):
510
- self.assertEqual(background_reader.IRQ_PIN, DEFAULT_IRQ_PIN)
511
-
512
- def test_reader_instantiation_uses_configured_pins(self):
513
- class DummyReader:
514
- init_args = None
515
- init_kwargs = None
516
-
517
- def __init__(self, *args, **kwargs):
518
- DummyReader.init_args = args
519
- DummyReader.init_kwargs = kwargs
520
- self.MI_OK = 1
521
- self.PICC_REQIDL = 0
522
-
523
- fake_mfrc = types.ModuleType("mfrc522")
524
- fake_mfrc.MFRC522 = DummyReader
525
- fake_gpio = types.ModuleType("RPi.GPIO")
526
- fake_rpi = types.ModuleType("RPi")
527
- fake_rpi.GPIO = fake_gpio
528
-
529
- with patch.dict(
530
- "sys.modules",
531
- {"mfrc522": fake_mfrc, "RPi": fake_rpi, "RPi.GPIO": fake_gpio},
532
- ):
533
- result = read_rfid(timeout=0, cleanup=False)
534
-
535
- self.assertEqual(result, {"rfid": None, "label_id": None})
536
- self.assertIsNotNone(DummyReader.init_kwargs)
537
- self.assertEqual(DummyReader.init_kwargs["bus"], SPI_BUS)
538
- self.assertEqual(DummyReader.init_kwargs["device"], SPI_DEVICE)
539
- self.assertEqual(DummyReader.init_kwargs["pin_mode"], GPIO_PIN_MODE_BCM)
540
- self.assertEqual(DummyReader.init_kwargs["pin_rst"], DEFAULT_RST_PIN)
1
+ import io
2
+ import json
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import types
7
+ from datetime import datetime, timezone as dt_timezone
8
+ from pathlib import Path
9
+ from unittest.mock import patch, MagicMock, call
10
+
11
+ import pytest
12
+
13
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
14
+
15
+ import django
16
+
17
+ django.setup()
18
+
19
+ from django.test import SimpleTestCase, TestCase
20
+ from django.urls import reverse
21
+ from django.contrib.auth import get_user_model
22
+ from django.contrib.sites.models import Site
23
+ from django.utils import timezone
24
+
25
+ from pages.models import Application, Module
26
+ from nodes.models import Node, NodeRole
27
+
28
+ from core.models import RFID
29
+ from ocpp.rfid.reader import read_rfid, enable_deep_read, validate_rfid_value
30
+ from ocpp.rfid.detect import detect_scanner, main as detect_main
31
+ from ocpp.rfid import background_reader, camera
32
+ from ocpp.rfid.constants import (
33
+ DEFAULT_IRQ_PIN,
34
+ DEFAULT_RST_PIN,
35
+ GPIO_PIN_MODE_BCM,
36
+ MODULE_WIRING,
37
+ SPI_BUS,
38
+ SPI_DEVICE,
39
+ )
40
+
41
+
42
+ pytestmark = [pytest.mark.feature("rfid-scanner")]
43
+
44
+
45
+ class BackgroundReaderConfigurationTests(SimpleTestCase):
46
+ def setUp(self):
47
+ background_reader._auto_detect_logged = False
48
+
49
+ def tearDown(self):
50
+ background_reader._auto_detect_logged = False
51
+
52
+ def test_is_configured_auto_detects_without_lock(self):
53
+ fake_lock = Path("/tmp/rfid-auto-detect.lock")
54
+ with (
55
+ patch("ocpp.rfid.background_reader._lock_path", return_value=fake_lock),
56
+ patch("ocpp.rfid.background_reader._has_spi_device", return_value=True),
57
+ patch(
58
+ "ocpp.rfid.background_reader._dependencies_available",
59
+ return_value=True,
60
+ ),
61
+ ):
62
+ self.assertTrue(background_reader.is_configured())
63
+
64
+ def test_is_configured_requires_dependencies(self):
65
+ fake_lock = Path("/tmp/rfid-auto-detect.lock")
66
+ with (
67
+ patch("ocpp.rfid.background_reader._lock_path", return_value=fake_lock),
68
+ patch("ocpp.rfid.background_reader._has_spi_device", return_value=True),
69
+ patch(
70
+ "ocpp.rfid.background_reader._dependencies_available",
71
+ return_value=False,
72
+ ),
73
+ ):
74
+ self.assertFalse(background_reader.is_configured())
75
+
76
+
77
+ class ScanNextViewTests(TestCase):
78
+ def setUp(self):
79
+ User = get_user_model()
80
+ self.user = User.objects.create_user("rfid-user", password="pwd")
81
+ self.client.force_login(self.user)
82
+
83
+ @patch("config.middleware.Node.get_local", return_value=None)
84
+ @patch("config.middleware.get_site")
85
+ @patch(
86
+ "ocpp.rfid.views.scan_sources",
87
+ return_value={
88
+ "rfid": "ABCD1234",
89
+ "label_id": 1,
90
+ "created": False,
91
+ "kind": RFID.CLASSIC,
92
+ },
93
+ )
94
+ def test_scan_next_success(self, mock_scan, mock_site, mock_node):
95
+ resp = self.client.get(reverse("rfid-scan-next"))
96
+ self.assertEqual(resp.status_code, 200)
97
+ self.assertEqual(
98
+ resp.json(),
99
+ {
100
+ "rfid": "ABCD1234",
101
+ "label_id": 1,
102
+ "created": False,
103
+ "kind": RFID.CLASSIC,
104
+ },
105
+ )
106
+
107
+ @patch("config.middleware.Node.get_local", return_value=None)
108
+ @patch("config.middleware.get_site")
109
+ @patch("ocpp.rfid.views.scan_sources", return_value={"error": "boom"})
110
+ def test_scan_next_error(self, mock_scan, mock_site, mock_node):
111
+ resp = self.client.get(reverse("rfid-scan-next"))
112
+ self.assertEqual(resp.status_code, 500)
113
+ self.assertEqual(resp.json(), {"error": "boom"})
114
+
115
+ @patch("config.middleware.Node.get_local", return_value=None)
116
+ @patch("config.middleware.get_site")
117
+ @patch(
118
+ "ocpp.rfid.views.validate_rfid_value",
119
+ return_value={"rfid": "ABCD1234", "label_id": 1, "created": False},
120
+ )
121
+ def test_scan_next_post_validates(self, mock_validate, mock_site, mock_node):
122
+ User = get_user_model()
123
+ user = User.objects.create_user("scanner", password="pwd")
124
+ self.client.force_login(user)
125
+ resp = self.client.post(
126
+ reverse("rfid-scan-next"),
127
+ data=json.dumps({"rfid": "ABCD1234"}),
128
+ content_type="application/json",
129
+ )
130
+ self.assertEqual(resp.status_code, 200)
131
+ self.assertEqual(
132
+ resp.json(), {"rfid": "ABCD1234", "label_id": 1, "created": False}
133
+ )
134
+ mock_validate.assert_called_once_with("ABCD1234", kind=None, endianness=None)
135
+
136
+ @patch("config.middleware.Node.get_local", return_value=None)
137
+ @patch("config.middleware.get_site")
138
+ @patch("ocpp.rfid.views.validate_rfid_value")
139
+ def test_scan_next_post_requires_authentication(
140
+ self, mock_validate, mock_site, mock_node
141
+ ):
142
+ self.client.logout()
143
+ resp = self.client.post(
144
+ reverse("rfid-scan-next"),
145
+ data=json.dumps({"rfid": "ABCD1234"}),
146
+ content_type="application/json",
147
+ )
148
+ self.assertEqual(resp.status_code, 401)
149
+ self.assertEqual(resp.json(), {"error": "Authentication required"})
150
+ mock_validate.assert_not_called()
151
+
152
+ @patch("config.middleware.Node.get_local", return_value=None)
153
+ @patch("config.middleware.get_site")
154
+ def test_scan_next_post_invalid_json(self, mock_site, mock_node):
155
+ User = get_user_model()
156
+ user = User.objects.create_user("invalid-json", password="pwd")
157
+ self.client.force_login(user)
158
+ resp = self.client.post(
159
+ reverse("rfid-scan-next"),
160
+ data="{",
161
+ content_type="application/json",
162
+ )
163
+ self.assertEqual(resp.status_code, 400)
164
+ self.assertEqual(resp.json(), {"error": "Invalid JSON payload"})
165
+
166
+ def test_scan_next_requires_authentication(self):
167
+ self.client.logout()
168
+ resp = self.client.get(reverse("rfid-scan-next"))
169
+ self.assertEqual(resp.status_code, 302)
170
+ self.assertIn(reverse("pages:login"), resp.url)
171
+
172
+
173
+ class ReaderNotificationTests(TestCase):
174
+ def setUp(self):
175
+ super().setUp()
176
+ self.queue_patcher = patch("ocpp.rfid.reader.queue_camera_snapshot")
177
+ self.mock_queue = self.queue_patcher.start()
178
+
179
+ def tearDown(self):
180
+ self.queue_patcher.stop()
181
+ super().tearDown()
182
+
183
+ def _mock_reader(self):
184
+ class MockReader:
185
+ MI_OK = 1
186
+ PICC_REQIDL = 0
187
+
188
+ def MFRC522_Request(self, _):
189
+ return (self.MI_OK, None)
190
+
191
+ def MFRC522_Anticoll(self):
192
+ return (self.MI_OK, [0xAB, 0xCD, 0x12, 0x34, 0x56])
193
+
194
+ def MFRC522_SelectTag(self, _uid):
195
+ self.select_called = True
196
+ return self.MI_OK
197
+
198
+ def MFRC522_StopCrypto1(self):
199
+ self.stop_called = True
200
+
201
+ return MockReader()
202
+
203
+ @patch("ocpp.rfid.reader.notify_async")
204
+ @patch("ocpp.rfid.reader.RFID.register_scan")
205
+ def test_notify_on_allowed_tag(self, mock_register, mock_notify):
206
+ reference = MagicMock(value="https://example.com")
207
+ tag = MagicMock(
208
+ label_id=1,
209
+ pk=1,
210
+ allowed=True,
211
+ color="B",
212
+ released=False,
213
+ reference=reference,
214
+ )
215
+ mock_register.return_value = (tag, False)
216
+
217
+ reader = self._mock_reader()
218
+ result = read_rfid(mfrc=reader, cleanup=False)
219
+ self.assertEqual(result["label_id"], 1)
220
+ self.assertEqual(result["kind"], RFID.CLASSIC)
221
+ self.assertEqual(result["reference"], "https://example.com")
222
+ self.assertEqual(mock_notify.call_count, 1)
223
+ mock_notify.assert_has_calls([call("RFID 1 OK", f"{result['rfid']} B")])
224
+ self.assertTrue(getattr(reader, "select_called", False))
225
+ self.assertTrue(getattr(reader, "stop_called", False))
226
+
227
+ @patch("ocpp.rfid.reader.notify_async")
228
+ @patch("ocpp.rfid.reader.RFID.register_scan")
229
+ def test_notify_on_disallowed_tag(self, mock_register, mock_notify):
230
+ tag = MagicMock(
231
+ label_id=2,
232
+ pk=2,
233
+ allowed=False,
234
+ color="B",
235
+ released=False,
236
+ reference=None,
237
+ )
238
+ mock_register.return_value = (tag, False)
239
+
240
+ reader = self._mock_reader()
241
+ result = read_rfid(mfrc=reader, cleanup=False)
242
+ self.assertEqual(result["kind"], RFID.CLASSIC)
243
+ self.assertEqual(mock_notify.call_count, 1)
244
+ mock_notify.assert_has_calls([call("RFID 2 BAD", f"{result['rfid']} B")])
245
+ self.assertTrue(getattr(reader, "select_called", False))
246
+ self.assertTrue(getattr(reader, "stop_called", False))
247
+
248
+ @patch("ocpp.rfid.reader.notify_async")
249
+ @patch("ocpp.rfid.reader.RFID.register_scan")
250
+ def test_snapshot_metadata_passed_to_queue(self, mock_register, mock_notify):
251
+ tag = MagicMock(
252
+ label_id=5,
253
+ pk=5,
254
+ allowed=True,
255
+ color="",
256
+ released=False,
257
+ reference=None,
258
+ )
259
+ mock_register.return_value = (tag, True)
260
+
261
+ reader = self._mock_reader()
262
+ result = read_rfid(mfrc=reader, cleanup=False)
263
+
264
+ self.assertTrue(self.mock_queue.called)
265
+ args, kwargs = self.mock_queue.call_args
266
+ self.assertEqual(args[0], result["rfid"])
267
+ self.assertEqual(args[1]["label_id"], tag.pk)
268
+ self.assertTrue(args[1]["created"])
269
+
270
+
271
+ class CameraSnapshotTriggerTests(SimpleTestCase):
272
+ @patch("ocpp.rfid.camera._camera_feature_enabled", return_value=False)
273
+ @patch("ocpp.rfid.camera.threading.Thread")
274
+ def test_queue_skips_without_feature(self, mock_thread, mock_enabled):
275
+ camera.queue_camera_snapshot("ABC123", {"label_id": 1})
276
+ mock_thread.assert_not_called()
277
+
278
+ @patch("ocpp.rfid.camera.timezone.now")
279
+ @patch("ocpp.rfid.camera.threading.Thread")
280
+ @patch("ocpp.rfid.camera._camera_feature_enabled", return_value=True)
281
+ def test_queue_spawns_thread_with_metadata(
282
+ self, mock_enabled, mock_thread, mock_now
283
+ ):
284
+ thread_instance = MagicMock()
285
+ mock_thread.return_value = thread_instance
286
+ mock_now.return_value = datetime(2023, 1, 1, tzinfo=dt_timezone.utc)
287
+
288
+ camera.queue_camera_snapshot("ABC123", {"label_id": 7})
289
+
290
+ mock_thread.assert_called_once()
291
+ kwargs = mock_thread.call_args.kwargs
292
+ self.assertTrue(kwargs.get("daemon"))
293
+ metadata = kwargs["args"][0]
294
+ self.assertEqual(metadata["rfid"], "ABC123")
295
+ self.assertEqual(metadata["label_id"], 7)
296
+ self.assertEqual(metadata["source"], "rfid-scan")
297
+ self.assertEqual(metadata["captured_at"], "2023-01-01T00:00:00+00:00")
298
+ thread_instance.start.assert_called_once()
299
+
300
+ @patch("ocpp.rfid.camera.close_old_connections")
301
+ @patch("ocpp.rfid.camera.save_screenshot")
302
+ @patch("ocpp.rfid.camera.capture_rpi_snapshot")
303
+ def test_worker_saves_snapshot(
304
+ self, mock_capture, mock_save, mock_close
305
+ ):
306
+ mock_capture.return_value = Path("/tmp/test.jpg")
307
+
308
+ camera._capture_snapshot_worker({"rfid": "ABC", "label_id": 3})
309
+
310
+ mock_capture.assert_called_once()
311
+ mock_save.assert_called_once()
312
+ _, kwargs = mock_save.call_args
313
+ self.assertEqual(kwargs["method"], "RFID_SCAN")
314
+ metadata = json.loads(kwargs["content"])
315
+ self.assertEqual(metadata["rfid"], "ABC")
316
+ self.assertEqual(metadata["label_id"], 3)
317
+ self.assertGreaterEqual(mock_close.call_count, 2)
318
+
319
+ @patch("ocpp.rfid.camera.close_old_connections")
320
+ @patch("ocpp.rfid.camera.save_screenshot")
321
+ @patch("ocpp.rfid.camera.capture_rpi_snapshot", side_effect=RuntimeError("boom"))
322
+ def test_worker_handles_capture_failure(
323
+ self, mock_capture, mock_save, mock_close
324
+ ):
325
+ camera._capture_snapshot_worker({"rfid": "XYZ"})
326
+ mock_save.assert_not_called()
327
+ self.assertGreaterEqual(mock_close.call_count, 2)
328
+
329
+
330
+ class ValidateRfidValueTests(SimpleTestCase):
331
+ @patch("ocpp.rfid.reader.timezone.now")
332
+ @patch("ocpp.rfid.reader.notify_async")
333
+ @patch("ocpp.rfid.reader.RFID.register_scan")
334
+ def test_creates_new_tag(self, mock_register, mock_notify, mock_now):
335
+ fake_now = object()
336
+ mock_now.return_value = fake_now
337
+ tag = MagicMock()
338
+ tag.pk = 1
339
+ tag.label_id = 1
340
+ tag.allowed = True
341
+ tag.color = "B"
342
+ tag.released = False
343
+ tag.reference = None
344
+ tag.kind = RFID.CLASSIC
345
+ tag.endianness = RFID.BIG_ENDIAN
346
+ mock_register.return_value = (tag, True)
347
+
348
+ result = validate_rfid_value("abcd1234")
349
+
350
+ mock_register.assert_called_once_with(
351
+ "ABCD1234", kind=None, endianness=RFID.BIG_ENDIAN
352
+ )
353
+ tag.save.assert_called_once_with(update_fields=["last_seen_on"])
354
+ self.assertIs(tag.last_seen_on, fake_now)
355
+ mock_notify.assert_called_once_with("RFID 1 OK", "ABCD1234 B")
356
+ self.assertTrue(result["created"])
357
+ self.assertEqual(result["rfid"], "ABCD1234")
358
+ self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
359
+
360
+ @patch("ocpp.rfid.reader.timezone.now")
361
+ @patch("ocpp.rfid.reader.notify_async")
362
+ @patch("ocpp.rfid.reader.RFID.register_scan")
363
+ def test_updates_existing_tag_kind(self, mock_register, mock_notify, mock_now):
364
+ fake_now = object()
365
+ mock_now.return_value = fake_now
366
+ tag = MagicMock()
367
+ tag.pk = 5
368
+ tag.label_id = 5
369
+ tag.allowed = False
370
+ tag.color = "G"
371
+ tag.released = True
372
+ tag.reference = None
373
+ tag.kind = RFID.CLASSIC
374
+ tag.endianness = RFID.BIG_ENDIAN
375
+ mock_register.return_value = (tag, False)
376
+
377
+ result = validate_rfid_value("abcd", kind=RFID.NTAG215)
378
+
379
+ mock_register.assert_called_once_with(
380
+ "ABCD", kind=RFID.NTAG215, endianness=RFID.BIG_ENDIAN
381
+ )
382
+ tag.save.assert_called_once_with(update_fields=["kind", "last_seen_on"])
383
+ self.assertIs(tag.last_seen_on, fake_now)
384
+ self.assertEqual(tag.kind, RFID.NTAG215)
385
+ mock_notify.assert_called_once_with("RFID 5 BAD", "ABCD G")
386
+ self.assertFalse(result["allowed"])
387
+ self.assertFalse(result["created"])
388
+ self.assertEqual(result["kind"], RFID.NTAG215)
389
+ self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
390
+
391
+ @patch("ocpp.rfid.reader.timezone.now")
392
+ @patch("ocpp.rfid.reader.notify_async")
393
+ @patch("ocpp.rfid.reader.RFID.register_scan")
394
+ def test_registers_little_endian_value(
395
+ self, mock_register, mock_notify, mock_now
396
+ ):
397
+ fake_now = object()
398
+ mock_now.return_value = fake_now
399
+ tag = MagicMock()
400
+ tag.pk = 7
401
+ tag.label_id = 7
402
+ tag.allowed = True
403
+ tag.color = "B"
404
+ tag.released = False
405
+ tag.reference = None
406
+ tag.kind = RFID.CLASSIC
407
+ tag.endianness = RFID.LITTLE_ENDIAN
408
+ mock_register.return_value = (tag, True)
409
+
410
+ result = validate_rfid_value("A1B2C3D4", endianness=RFID.LITTLE_ENDIAN)
411
+
412
+ mock_register.assert_called_once_with(
413
+ "D4C3B2A1", kind=None, endianness=RFID.LITTLE_ENDIAN
414
+ )
415
+ tag.save.assert_called_once_with(update_fields=["last_seen_on"])
416
+ self.assertEqual(result["rfid"], "D4C3B2A1")
417
+ self.assertEqual(result["endianness"], RFID.LITTLE_ENDIAN)
418
+ mock_notify.assert_called_once()
419
+
420
+ def test_rejects_invalid_value(self):
421
+ result = validate_rfid_value("invalid!")
422
+ self.assertEqual(result, {"error": "RFID must be hexadecimal digits"})
423
+
424
+ def test_rejects_non_string_values(self):
425
+ result = validate_rfid_value(12345)
426
+ self.assertEqual(result, {"error": "RFID must be a string"})
427
+
428
+ def test_rejects_missing_value(self):
429
+ result = validate_rfid_value(None)
430
+ self.assertEqual(result, {"error": "RFID value is required"})
431
+
432
+
433
+ @patch("ocpp.rfid.reader.timezone.now")
434
+ @patch("ocpp.rfid.reader.notify_async")
435
+ @patch("ocpp.rfid.reader.subprocess.Popen")
436
+ @patch("ocpp.rfid.reader.subprocess.run")
437
+ @patch("ocpp.rfid.reader.RFID.register_scan")
438
+ def test_external_command_success(
439
+ self, mock_register, mock_run, mock_popen, mock_notify, mock_now
440
+ ):
441
+ fake_now = object()
442
+ mock_now.return_value = fake_now
443
+ tag = MagicMock()
444
+ tag.pk = 1
445
+ tag.label_id = 1
446
+ tag.allowed = True
447
+ tag.external_command = "echo ok"
448
+ tag.color = "B"
449
+ tag.released = False
450
+ tag.reference = None
451
+ tag.kind = RFID.CLASSIC
452
+ tag.endianness = RFID.BIG_ENDIAN
453
+ mock_register.return_value = (tag, False)
454
+ mock_run.return_value = types.SimpleNamespace(
455
+ returncode=0, stdout="ok\n", stderr=""
456
+ )
457
+ mock_popen.return_value = object()
458
+
459
+ result = validate_rfid_value("abcd1234")
460
+
461
+ mock_run.assert_called_once()
462
+ run_args, run_kwargs = mock_run.call_args
463
+ self.assertEqual(run_args[0], "echo ok")
464
+ self.assertTrue(run_kwargs.get("shell"))
465
+ env = run_kwargs.get("env", {})
466
+ self.assertEqual(env.get("RFID_VALUE"), "ABCD1234")
467
+ self.assertEqual(env.get("RFID_LABEL_ID"), "1")
468
+ self.assertEqual(env.get("RFID_ENDIANNESS"), RFID.BIG_ENDIAN)
469
+ mock_popen.assert_not_called()
470
+ mock_notify.assert_called_once_with("RFID 1 OK", "ABCD1234 B")
471
+ tag.save.assert_called_once_with(update_fields=["last_seen_on"])
472
+ self.assertTrue(result["allowed"])
473
+ output = result.get("command_output")
474
+ self.assertIsInstance(output, dict)
475
+ self.assertEqual(output.get("stdout"), "ok\n")
476
+ self.assertEqual(output.get("stderr"), "")
477
+ self.assertEqual(output.get("returncode"), 0)
478
+ self.assertEqual(output.get("error"), "")
479
+ self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
480
+
481
+ @patch("ocpp.rfid.reader.timezone.now")
482
+ @patch("ocpp.rfid.reader.notify_async")
483
+ @patch("ocpp.rfid.reader.subprocess.Popen")
484
+ @patch("ocpp.rfid.reader.subprocess.run")
485
+ @patch("ocpp.rfid.reader.RFID.register_scan")
486
+ def test_external_command_failure_blocks_tag(
487
+ self, mock_register, mock_run, mock_popen, mock_notify, mock_now
488
+ ):
489
+ fake_now = object()
490
+ mock_now.return_value = fake_now
491
+ tag = MagicMock()
492
+ tag.pk = 2
493
+ tag.label_id = 2
494
+ tag.allowed = True
495
+ tag.external_command = "exit 1"
496
+ tag.color = "G"
497
+ tag.released = False
498
+ tag.reference = None
499
+ tag.kind = RFID.CLASSIC
500
+ tag.endianness = RFID.BIG_ENDIAN
501
+ mock_register.return_value = (tag, False)
502
+ mock_run.return_value = types.SimpleNamespace(
503
+ returncode=1, stdout="", stderr="failure"
504
+ )
505
+ mock_popen.return_value = object()
506
+
507
+ result = validate_rfid_value("ffff")
508
+
509
+ mock_run.assert_called_once()
510
+ mock_notify.assert_called_once_with("RFID 2 BAD", "FFFF G")
511
+ tag.save.assert_called_once_with(update_fields=["last_seen_on"])
512
+ self.assertFalse(result["allowed"])
513
+ output = result.get("command_output")
514
+ self.assertIsInstance(output, dict)
515
+ self.assertEqual(output.get("returncode"), 1)
516
+ self.assertEqual(output.get("stdout"), "")
517
+ self.assertEqual(output.get("stderr"), "failure")
518
+ self.assertEqual(output.get("error"), "")
519
+ mock_popen.assert_not_called()
520
+ self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
521
+
522
+ @patch("ocpp.rfid.reader.timezone.now")
523
+ @patch("ocpp.rfid.reader.notify_async")
524
+ @patch("ocpp.rfid.reader.subprocess.Popen")
525
+ @patch("ocpp.rfid.reader.subprocess.run")
526
+ @patch("ocpp.rfid.reader.RFID.register_scan")
527
+ def test_external_command_strips_trailing_percent_tokens(
528
+ self, mock_register, mock_run, mock_popen, mock_notify, mock_now
529
+ ):
530
+ mock_now.return_value = timezone.now()
531
+ tag = MagicMock()
532
+ tag.pk = 3
533
+ tag.label_id = 3
534
+ tag.allowed = True
535
+ tag.external_command = "echo weird"
536
+ tag.color = "Y"
537
+ tag.released = False
538
+ tag.reference = None
539
+ tag.kind = RFID.CLASSIC
540
+ tag.endianness = RFID.BIG_ENDIAN
541
+ mock_register.return_value = (tag, False)
542
+ mock_run.return_value = types.SimpleNamespace(
543
+ returncode=0,
544
+ stdout="first %\nsecond 50%\r\nthird % %\n",
545
+ stderr="oops %\n",
546
+ )
547
+
548
+ result = validate_rfid_value("abc3")
549
+
550
+ output = result.get("command_output")
551
+ self.assertIsNotNone(output)
552
+ self.assertEqual(
553
+ output.get("stdout"), "first\nsecond 50%\r\nthird\n"
554
+ )
555
+ self.assertEqual(output.get("stderr"), "oops\n")
556
+ self.assertEqual(output.get("returncode"), 0)
557
+ self.assertEqual(output.get("error"), "")
558
+ mock_popen.assert_not_called()
559
+
560
+ @patch("ocpp.rfid.reader.timezone.now")
561
+ @patch("ocpp.rfid.reader.notify_async")
562
+ @patch("ocpp.rfid.reader.subprocess.Popen")
563
+ @patch("ocpp.rfid.reader.subprocess.run")
564
+ @patch("ocpp.rfid.reader.RFID.register_scan")
565
+ def test_external_command_error_strips_trailing_percent_tokens(
566
+ self, mock_register, mock_run, mock_popen, mock_notify, mock_now
567
+ ):
568
+ mock_now.return_value = timezone.now()
569
+ tag = MagicMock()
570
+ tag.pk = 4
571
+ tag.label_id = 4
572
+ tag.allowed = True
573
+ tag.external_command = "echo boom"
574
+ tag.color = "R"
575
+ tag.released = False
576
+ tag.reference = None
577
+ tag.kind = RFID.CLASSIC
578
+ tag.endianness = RFID.BIG_ENDIAN
579
+ mock_register.return_value = (tag, False)
580
+ mock_run.side_effect = RuntimeError("bad % %")
581
+
582
+ result = validate_rfid_value("abcd")
583
+
584
+ output = result.get("command_output")
585
+ self.assertIsInstance(output, dict)
586
+ self.assertEqual(output.get("stdout"), "")
587
+ self.assertEqual(output.get("stderr"), "")
588
+ self.assertEqual(output.get("error"), "bad")
589
+ self.assertFalse(result["allowed"])
590
+ mock_popen.assert_not_called()
591
+
592
+ @patch("ocpp.rfid.reader.timezone.now")
593
+ @patch("ocpp.rfid.reader.notify_async")
594
+ @patch("ocpp.rfid.reader.subprocess.Popen")
595
+ @patch("ocpp.rfid.reader.subprocess.run")
596
+ @patch("ocpp.rfid.reader.RFID.register_scan")
597
+ def test_post_command_runs_after_success(
598
+ self, mock_register, mock_run, mock_popen, mock_notify, mock_now
599
+ ):
600
+ fake_now = object()
601
+ mock_now.return_value = fake_now
602
+ tag = MagicMock()
603
+ tag.pk = 3
604
+ tag.label_id = 3
605
+ tag.allowed = True
606
+ tag.external_command = ""
607
+ tag.post_auth_command = "echo done"
608
+ tag.color = "B"
609
+ tag.released = False
610
+ tag.reference = None
611
+ tag.kind = RFID.CLASSIC
612
+ tag.endianness = RFID.BIG_ENDIAN
613
+ mock_register.return_value = (tag, False)
614
+ result = validate_rfid_value("abcdef")
615
+
616
+ mock_run.assert_not_called()
617
+ mock_popen.assert_called_once()
618
+ args, kwargs = mock_popen.call_args
619
+ self.assertEqual(args[0], "echo done")
620
+ env = kwargs.get("env", {})
621
+ self.assertEqual(env.get("RFID_VALUE"), "ABCDEF")
622
+ self.assertEqual(env.get("RFID_LABEL_ID"), "3")
623
+ self.assertEqual(env.get("RFID_ENDIANNESS"), RFID.BIG_ENDIAN)
624
+ self.assertIs(kwargs.get("stdout"), subprocess.DEVNULL)
625
+ self.assertIs(kwargs.get("stderr"), subprocess.DEVNULL)
626
+ self.assertTrue(result["allowed"])
627
+ mock_notify.assert_called_once_with("RFID 3 OK", "ABCDEF B")
628
+ self.assertEqual(result["endianness"], RFID.BIG_ENDIAN)
629
+
630
+
631
+ class CardTypeDetectionTests(TestCase):
632
+ def _mock_ntag_reader(self):
633
+ class MockReader:
634
+ MI_OK = 1
635
+ PICC_REQIDL = 0
636
+
637
+ def MFRC522_Request(self, _):
638
+ return (self.MI_OK, None)
639
+
640
+ def MFRC522_Anticoll(self):
641
+ return (
642
+ self.MI_OK,
643
+ [0x04, 0xD3, 0x2A, 0x1B, 0x5F, 0x23, 0x19],
644
+ )
645
+
646
+ def MFRC522_SelectTag(self, _uid):
647
+ self.select_called = True
648
+ return self.MI_OK
649
+
650
+ def MFRC522_StopCrypto1(self):
651
+ self.stop_called = True
652
+
653
+ return MockReader()
654
+
655
+ @patch("ocpp.rfid.reader.notify_async")
656
+ @patch("ocpp.rfid.reader.RFID.register_scan")
657
+ def test_detects_ntag215(self, mock_register, _mock_notify):
658
+ tag = MagicMock(
659
+ pk=1,
660
+ label_id=1,
661
+ allowed=True,
662
+ color="B",
663
+ released=False,
664
+ reference=None,
665
+ kind=RFID.NTAG215,
666
+ )
667
+ mock_register.return_value = (tag, True)
668
+ reader = self._mock_ntag_reader()
669
+ result = read_rfid(mfrc=reader, cleanup=False)
670
+ self.assertEqual(result["kind"], RFID.NTAG215)
671
+ self.assertTrue(getattr(reader, "select_called", False))
672
+ self.assertTrue(getattr(reader, "stop_called", False))
673
+
674
+
675
+ class RFIDLastSeenTests(TestCase):
676
+ def _mock_reader(self):
677
+ class MockReader:
678
+ MI_OK = 1
679
+ PICC_REQIDL = 0
680
+
681
+ def MFRC522_Request(self, _):
682
+ return (self.MI_OK, None)
683
+
684
+ def MFRC522_Anticoll(self):
685
+ return (self.MI_OK, [0xAB, 0xCD, 0x12, 0x34])
686
+
687
+ def MFRC522_SelectTag(self, _uid):
688
+ self.select_called = True
689
+ return self.MI_OK
690
+
691
+ def MFRC522_StopCrypto1(self):
692
+ self.stop_called = True
693
+
694
+ return MockReader()
695
+
696
+ @patch("ocpp.rfid.reader.notify_async")
697
+ def test_last_seen_updated_on_read(self, _mock_notify):
698
+ tag = RFID.objects.create(rfid="ABCD1234")
699
+ reader = self._mock_reader()
700
+ result = read_rfid(mfrc=reader, cleanup=False)
701
+ tag.refresh_from_db()
702
+ self.assertIsNotNone(tag.last_seen_on)
703
+ self.assertEqual(result["kind"], RFID.CLASSIC)
704
+ self.assertTrue(getattr(reader, "select_called", False))
705
+ self.assertTrue(getattr(reader, "stop_called", False))
706
+
707
+
708
+ class RFIDDetectionScriptTests(SimpleTestCase):
709
+ @patch("ocpp.rfid.detect._ensure_django")
710
+ @patch(
711
+ "ocpp.rfid.irq_wiring_check.check_irq_pin",
712
+ return_value={"irq_pin": DEFAULT_IRQ_PIN},
713
+ )
714
+ def test_detect_scanner_success(self, mock_check, _mock_setup):
715
+ result = detect_scanner()
716
+ self.assertEqual(
717
+ result,
718
+ {
719
+ "detected": True,
720
+ "irq_pin": DEFAULT_IRQ_PIN,
721
+ },
722
+ )
723
+ mock_check.assert_called_once()
724
+
725
+ @patch("ocpp.rfid.detect._ensure_django")
726
+ @patch(
727
+ "ocpp.rfid.irq_wiring_check.check_irq_pin",
728
+ return_value={"error": "no scanner detected"},
729
+ )
730
+ def test_detect_scanner_failure(self, mock_check, _mock_setup):
731
+ result = detect_scanner()
732
+ self.assertFalse(result["detected"])
733
+ self.assertEqual(result["reason"], "no scanner detected")
734
+ mock_check.assert_called_once()
735
+
736
+ @patch(
737
+ "ocpp.rfid.detect.detect_scanner",
738
+ return_value={"detected": True, "irq_pin": DEFAULT_IRQ_PIN},
739
+ )
740
+ def test_detect_main_success_output(self, mock_detect):
741
+ buffer = io.StringIO()
742
+ with patch("sys.stdout", new=buffer):
743
+ exit_code = detect_main([])
744
+ self.assertEqual(exit_code, 0)
745
+ self.assertIn("IRQ pin", buffer.getvalue())
746
+ mock_detect.assert_called_once()
747
+
748
+ @patch(
749
+ "ocpp.rfid.detect.detect_scanner",
750
+ return_value={"detected": False, "reason": "missing hardware"},
751
+ )
752
+ def test_detect_main_failure_output(self, mock_detect):
753
+ buffer = io.StringIO()
754
+ with patch("sys.stdout", new=buffer):
755
+ exit_code = detect_main([])
756
+ self.assertEqual(exit_code, 1)
757
+ self.assertIn("missing hardware", buffer.getvalue())
758
+ mock_detect.assert_called_once()
759
+
760
+
761
+ class RFIDLandingTests(TestCase):
762
+ def test_scanner_view_registered_as_landing(self):
763
+ role, _ = NodeRole.objects.get_or_create(name="Terminal")
764
+ Node.objects.update_or_create(
765
+ mac_address=Node.get_current_mac(),
766
+ defaults={"hostname": "localhost", "address": "127.0.0.1", "role": role},
767
+ )
768
+ Site.objects.update_or_create(
769
+ id=1, defaults={"domain": "testserver", "name": ""}
770
+ )
771
+ app = Application.objects.create(name="Ocpp")
772
+ module = Module.objects.create(node_role=role, application=app, path="/ocpp/")
773
+ module.create_landings()
774
+ self.assertTrue(
775
+ module.landings.filter(path="/ocpp/rfid/validator/").exists()
776
+ )
777
+
778
+
779
+ class ScannerTemplateTests(TestCase):
780
+ def setUp(self):
781
+ self.url = reverse("rfid-reader")
782
+ User = get_user_model()
783
+ self.user = User.objects.create_user("scanner-user", password="pwd")
784
+
785
+ def test_configure_link_for_staff(self):
786
+ User = get_user_model()
787
+ staff = User.objects.create_user("staff", password="pwd", is_staff=True)
788
+ self.client.force_login(staff)
789
+ resp = self.client.get(self.url)
790
+ self.assertContains(resp, 'id="rfid-configure"')
791
+ self.assertContains(resp, 'id="rfid-connect-local"')
792
+ self.assertNotContains(resp, 'Restart & Test Scanner')
793
+
794
+ def test_redirect_for_anonymous(self):
795
+ self.client.logout()
796
+ resp = self.client.get(self.url)
797
+ self.assertEqual(resp.status_code, 302)
798
+ self.assertIn(reverse("pages:login"), resp.url)
799
+
800
+ def test_advanced_fields_for_staff(self):
801
+ User = get_user_model()
802
+ staff = User.objects.create_user("staff2", password="pwd", is_staff=True)
803
+ self.client.force_login(staff)
804
+ resp = self.client.get(self.url)
805
+ self.assertContains(resp, 'id="rfid-kind"')
806
+ self.assertContains(resp, 'id="rfid-rfid"')
807
+ self.assertContains(resp, 'id="rfid-released"')
808
+ self.assertContains(resp, 'id="rfid-reference"')
809
+ self.assertContains(resp, 'id="rfid-deep-details"')
810
+
811
+ def test_basic_fields_for_authenticated_user(self):
812
+ self.client.logout()
813
+ self.client.force_login(self.user)
814
+ resp = self.client.get(self.url)
815
+ self.assertContains(resp, 'id="rfid-kind"')
816
+ self.assertNotContains(resp, 'id="rfid-connect-local"')
817
+ self.assertNotContains(resp, 'id="rfid-rfid"')
818
+ self.assertNotContains(resp, 'id="rfid-released"')
819
+ self.assertNotContains(resp, 'id="rfid-reference"')
820
+ self.assertNotContains(resp, 'id="rfid-deep-details"')
821
+ self.assertNotContains(resp, 'Restart & Test Scanner')
822
+
823
+ def test_deep_read_button_for_staff(self):
824
+ User = get_user_model()
825
+ staff = User.objects.create_user("staff3", password="pwd", is_staff=True)
826
+ self.client.force_login(staff)
827
+ resp = self.client.get(self.url)
828
+ self.assertContains(resp, 'id="rfid-deep-read"')
829
+
830
+ def test_no_deep_read_button_for_authenticated_user(self):
831
+ self.client.logout()
832
+ self.client.force_login(self.user)
833
+ resp = self.client.get(self.url)
834
+ self.assertNotContains(resp, 'id="rfid-deep-read"')
835
+
836
+
837
+ class ReaderPollingTests(SimpleTestCase):
838
+ def _mock_reader_no_tag(self):
839
+ class MockReader:
840
+ MI_OK = 1
841
+ PICC_REQIDL = 0
842
+
843
+ def MFRC522_Request(self, _):
844
+ return (0, None)
845
+
846
+ return MockReader()
847
+
848
+ @patch("ocpp.rfid.reader.time.sleep")
849
+ def test_poll_interval_used(self, mock_sleep):
850
+ read_rfid(
851
+ mfrc=self._mock_reader_no_tag(),
852
+ cleanup=False,
853
+ timeout=0.002,
854
+ poll_interval=0.001,
855
+ )
856
+ mock_sleep.assert_called_with(0.001)
857
+
858
+ @patch("ocpp.rfid.reader.time.sleep")
859
+ def test_use_irq_skips_sleep(self, mock_sleep):
860
+ read_rfid(
861
+ mfrc=self._mock_reader_no_tag(),
862
+ cleanup=False,
863
+ timeout=0.002,
864
+ use_irq=True,
865
+ )
866
+ mock_sleep.assert_not_called()
867
+
868
+
869
+ class DeepReadViewTests(TestCase):
870
+ @patch("config.middleware.Node.get_local", return_value=None)
871
+ @patch("config.middleware.get_site")
872
+ @patch(
873
+ "ocpp.rfid.views.enable_deep_read_mode",
874
+ return_value={"status": "deep read enabled", "enabled": True},
875
+ )
876
+ def test_enable_deep_read(self, mock_enable, mock_site, mock_node):
877
+ User = get_user_model()
878
+ staff = User.objects.create_user("staff4", password="pwd", is_staff=True)
879
+ self.client.force_login(staff)
880
+ resp = self.client.post(reverse("rfid-scan-deep"))
881
+ self.assertEqual(resp.status_code, 200)
882
+ self.assertEqual(
883
+ resp.json(), {"status": "deep read enabled", "enabled": True}
884
+ )
885
+ mock_enable.assert_called_once()
886
+
887
+ def test_forbidden_for_anonymous(self):
888
+ resp = self.client.post(reverse("rfid-scan-deep"))
889
+ self.assertNotEqual(resp.status_code, 200)
890
+
891
+
892
+ class DeepReadAuthTests(TestCase):
893
+ def setUp(self):
894
+ super().setUp()
895
+ self.queue_patcher = patch("ocpp.rfid.reader.queue_camera_snapshot")
896
+ self.queue_patcher.start()
897
+
898
+ def tearDown(self):
899
+ self.queue_patcher.stop()
900
+ super().tearDown()
901
+
902
+ class MockReader:
903
+ MI_OK = 1
904
+ MI_ERR = 2
905
+ PICC_REQIDL = 0
906
+ PICC_AUTHENT1A = 0x60
907
+ PICC_AUTHENT1B = 0x61
908
+
909
+ def __init__(self):
910
+ self.auth_calls = []
911
+
912
+ def MFRC522_Request(self, _):
913
+ return (self.MI_OK, None)
914
+
915
+ def MFRC522_Anticoll(self):
916
+ return (self.MI_OK, [0xAA, 0xBB, 0xCC, 0xDD, 0xEE])
917
+
918
+ def MFRC522_Auth(self, mode, block, key, uid):
919
+ self.auth_calls.append(mode)
920
+ return self.MI_ERR if mode == self.PICC_AUTHENT1A else self.MI_OK
921
+
922
+ def MFRC522_Read(self, block):
923
+ return (self.MI_OK, [0] * 16)
924
+
925
+ @patch("core.notifications.notify_async")
926
+ @patch("ocpp.rfid.reader.RFID.register_scan")
927
+ def test_auth_tries_key_a_then_b(self, mock_register, mock_notify):
928
+ tag = MagicMock(
929
+ label_id=1,
930
+ pk=1,
931
+ allowed=True,
932
+ color="B",
933
+ released=False,
934
+ reference=None,
935
+ )
936
+ tag.key_a = "A1A2A3A4A5A6"
937
+ tag.key_b = "B1B2B3B4B5B6"
938
+ tag.key_a_verified = True
939
+ tag.key_b_verified = False
940
+ tag.data = []
941
+ tag.save = MagicMock()
942
+ mock_register.return_value = (tag, False)
943
+ reader = self.MockReader()
944
+ enable_deep_read(60)
945
+ result = read_rfid(mfrc=reader, cleanup=False)
946
+ self.assertGreaterEqual(len(reader.auth_calls), 2)
947
+ self.assertEqual(reader.auth_calls[0], reader.PICC_AUTHENT1A)
948
+ self.assertEqual(reader.auth_calls[1], reader.PICC_AUTHENT1B)
949
+ self.assertTrue(result.get("deep_read"))
950
+ self.assertIn("dump", result)
951
+ self.assertIn("keys", result)
952
+ self.assertEqual(result["keys"].get("a"), "A1A2A3A4A5A6")
953
+ self.assertTrue(result["keys"].get("b_verified"))
954
+ self.assertTrue(tag.key_b_verified)
955
+ self.assertTrue(any(entry.get("key") == "B" for entry in result["dump"]))
956
+ self.assertEqual(tag.data, result["dump"])
957
+ self.assertTrue(
958
+ any(
959
+ "data" in set(kwargs.get("update_fields", []) or [])
960
+ for _args, kwargs in tag.save.call_args_list
961
+ )
962
+ )
963
+
964
+ @patch("core.notifications.notify_async")
965
+ @patch("ocpp.rfid.reader.RFID.register_scan")
966
+ def test_heuristic_verifies_unverified_keys(
967
+ self, mock_register, mock_notify
968
+ ):
969
+ tag = MagicMock(
970
+ label_id=1,
971
+ pk=1,
972
+ allowed=True,
973
+ color="B",
974
+ released=False,
975
+ reference=None,
976
+ )
977
+ tag.key_a = "111111111111"
978
+ tag.key_b = "FFFFFFFFFFFF"
979
+ tag.key_a_verified = False
980
+ tag.key_b_verified = True
981
+ tag.save = MagicMock()
982
+ mock_register.return_value = (tag, False)
983
+
984
+ class HeuristicReader:
985
+ MI_OK = 1
986
+ MI_ERR = 2
987
+ PICC_REQIDL = 0
988
+ PICC_AUTHENT1A = 0x60
989
+ PICC_AUTHENT1B = 0x61
990
+
991
+ def __init__(self):
992
+ self.auth_calls: list[tuple[int, str]] = []
993
+
994
+ def MFRC522_Request(self, _):
995
+ return (self.MI_OK, None)
996
+
997
+ def MFRC522_Anticoll(self):
998
+ return (self.MI_OK, [0x01, 0x02, 0x03, 0x04])
999
+
1000
+ def MFRC522_Auth(self, mode, block, key, uid):
1001
+ key_hex = "".join(f"{value:02X}" for value in key)
1002
+ self.auth_calls.append((mode, key_hex))
1003
+ if mode == self.PICC_AUTHENT1A and key_hex == "A0A1A2A3A4A5":
1004
+ return self.MI_OK
1005
+ return self.MI_ERR
1006
+
1007
+ def MFRC522_Read(self, block):
1008
+ return (self.MI_OK, list(range(16)))
1009
+
1010
+ reader = HeuristicReader()
1011
+ enable_deep_read(60)
1012
+ result = read_rfid(mfrc=reader, cleanup=False)
1013
+
1014
+ self.assertIn((reader.PICC_AUTHENT1A, "A0A1A2A3A4A5"), reader.auth_calls)
1015
+ self.assertEqual(result["keys"].get("a"), "A0A1A2A3A4A5")
1016
+ self.assertTrue(result["keys"].get("a_verified"))
1017
+ self.assertEqual(tag.key_a, "A0A1A2A3A4A5")
1018
+ self.assertTrue(tag.key_a_verified)
1019
+ self.assertIn(
1020
+ call(update_fields=["key_a", "key_a_verified"]),
1021
+ tag.save.call_args_list,
1022
+ )
1023
+
1024
+
1025
+ class RFIDWiringConfigTests(SimpleTestCase):
1026
+ def test_module_wiring_map(self):
1027
+ expected = [
1028
+ ("SDA", "CE0"),
1029
+ ("SCK", "SCLK"),
1030
+ ("MOSI", "MOSI"),
1031
+ ("MISO", "MISO"),
1032
+ ("IRQ", "IO4"),
1033
+ ("GND", "GND"),
1034
+ ("RST", "IO25"),
1035
+ ("3v3", "3v3"),
1036
+ ]
1037
+ self.assertEqual(list(MODULE_WIRING.items()), expected)
1038
+ self.assertEqual(DEFAULT_IRQ_PIN, 4)
1039
+ self.assertEqual(DEFAULT_RST_PIN, 25)
1040
+
1041
+ def test_background_reader_uses_default_irq_pin(self):
1042
+ self.assertEqual(background_reader.IRQ_PIN, DEFAULT_IRQ_PIN)
1043
+
1044
+ def test_reader_instantiation_uses_configured_pins(self):
1045
+ class DummyReader:
1046
+ init_args = None
1047
+ init_kwargs = None
1048
+
1049
+ def __init__(self, *args, **kwargs):
1050
+ DummyReader.init_args = args
1051
+ DummyReader.init_kwargs = kwargs
1052
+ self.MI_OK = 1
1053
+ self.PICC_REQIDL = 0
1054
+
1055
+ fake_mfrc = types.ModuleType("mfrc522")
1056
+ fake_mfrc.MFRC522 = DummyReader
1057
+ fake_gpio = types.ModuleType("RPi.GPIO")
1058
+ fake_rpi = types.ModuleType("RPi")
1059
+ fake_rpi.GPIO = fake_gpio
1060
+
1061
+ with patch.dict(
1062
+ "sys.modules",
1063
+ {"mfrc522": fake_mfrc, "RPi": fake_rpi, "RPi.GPIO": fake_gpio},
1064
+ ):
1065
+ result = read_rfid(timeout=0, cleanup=False)
1066
+
1067
+ self.assertEqual(result, {"rfid": None, "label_id": None})
1068
+ self.assertIsNotNone(DummyReader.init_kwargs)
1069
+ self.assertEqual(DummyReader.init_kwargs["bus"], SPI_BUS)
1070
+ self.assertEqual(DummyReader.init_kwargs["device"], SPI_DEVICE)
1071
+ self.assertEqual(DummyReader.init_kwargs["pin_mode"], GPIO_PIN_MODE_BCM)
1072
+ self.assertEqual(DummyReader.init_kwargs["pin_rst"], DEFAULT_RST_PIN)