arthexis 0.1.3__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 (73) hide show
  1. arthexis-0.1.3.dist-info/METADATA +126 -0
  2. arthexis-0.1.3.dist-info/RECORD +73 -0
  3. arthexis-0.1.3.dist-info/WHEEL +5 -0
  4. arthexis-0.1.3.dist-info/licenses/LICENSE +21 -0
  5. arthexis-0.1.3.dist-info/top_level.txt +5 -0
  6. config/__init__.py +6 -0
  7. config/active_app.py +15 -0
  8. config/asgi.py +29 -0
  9. config/auth_app.py +8 -0
  10. config/celery.py +19 -0
  11. config/context_processors.py +68 -0
  12. config/loadenv.py +11 -0
  13. config/logging.py +43 -0
  14. config/middleware.py +25 -0
  15. config/offline.py +47 -0
  16. config/settings.py +374 -0
  17. config/urls.py +91 -0
  18. config/wsgi.py +17 -0
  19. core/__init__.py +0 -0
  20. core/admin.py +830 -0
  21. core/apps.py +67 -0
  22. core/backends.py +82 -0
  23. core/entity.py +97 -0
  24. core/environment.py +43 -0
  25. core/fields.py +70 -0
  26. core/lcd_screen.py +77 -0
  27. core/middleware.py +34 -0
  28. core/models.py +1277 -0
  29. core/notifications.py +95 -0
  30. core/release.py +451 -0
  31. core/system.py +111 -0
  32. core/tasks.py +100 -0
  33. core/tests.py +483 -0
  34. core/urls.py +11 -0
  35. core/user_data.py +333 -0
  36. core/views.py +431 -0
  37. nodes/__init__.py +0 -0
  38. nodes/actions.py +72 -0
  39. nodes/admin.py +347 -0
  40. nodes/apps.py +76 -0
  41. nodes/lcd.py +151 -0
  42. nodes/models.py +577 -0
  43. nodes/tasks.py +50 -0
  44. nodes/tests.py +1072 -0
  45. nodes/urls.py +13 -0
  46. nodes/utils.py +62 -0
  47. nodes/views.py +262 -0
  48. ocpp/__init__.py +0 -0
  49. ocpp/admin.py +392 -0
  50. ocpp/apps.py +24 -0
  51. ocpp/consumers.py +267 -0
  52. ocpp/evcs.py +911 -0
  53. ocpp/models.py +300 -0
  54. ocpp/routing.py +9 -0
  55. ocpp/simulator.py +357 -0
  56. ocpp/store.py +175 -0
  57. ocpp/tasks.py +27 -0
  58. ocpp/test_export_import.py +129 -0
  59. ocpp/test_rfid.py +345 -0
  60. ocpp/tests.py +1229 -0
  61. ocpp/transactions_io.py +119 -0
  62. ocpp/urls.py +17 -0
  63. ocpp/views.py +359 -0
  64. pages/__init__.py +0 -0
  65. pages/admin.py +231 -0
  66. pages/apps.py +10 -0
  67. pages/checks.py +41 -0
  68. pages/context_processors.py +72 -0
  69. pages/models.py +224 -0
  70. pages/tests.py +628 -0
  71. pages/urls.py +17 -0
  72. pages/utils.py +13 -0
  73. pages/views.py +191 -0
core/tasks.py ADDED
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ from celery import shared_task
8
+
9
+
10
+ @shared_task
11
+ def check_github_updates() -> None:
12
+ """Check the GitHub repo for updates and upgrade if needed."""
13
+ base_dir = Path(__file__).resolve().parent.parent
14
+ mode_file = base_dir / "AUTO_UPGRADE"
15
+ mode = "version"
16
+ if mode_file.exists():
17
+ mode = mode_file.read_text().strip()
18
+
19
+ branch = "main"
20
+ subprocess.run(["git", "fetch", "origin", branch], cwd=base_dir, check=True)
21
+
22
+ log_dir = base_dir / "logs"
23
+ log_dir.mkdir(parents=True, exist_ok=True)
24
+ log_file = log_dir / "auto-upgrade.log"
25
+ with log_file.open("a") as fh:
26
+ fh.write(f"{datetime.utcnow().isoformat()} check_github_updates triggered\n")
27
+
28
+ notify = None
29
+ startup = None
30
+ try: # pragma: no cover - optional dependency
31
+ from core.notifications import notify # type: ignore
32
+ except Exception:
33
+ notify = None
34
+ try: # pragma: no cover - optional dependency
35
+ from nodes.apps import _startup_notification as startup # type: ignore
36
+ except Exception:
37
+ startup = None
38
+
39
+ if mode == "latest":
40
+ local = subprocess.check_output(["git", "rev-parse", branch], cwd=base_dir).decode().strip()
41
+ remote = subprocess.check_output([
42
+ "git",
43
+ "rev-parse",
44
+ f"origin/{branch}",
45
+ ], cwd=base_dir).decode().strip()
46
+ if local == remote:
47
+ if startup:
48
+ startup()
49
+ return
50
+ if notify:
51
+ notify("Upgrading...", "")
52
+ args = ["./upgrade.sh", "--latest", "--no-restart"]
53
+ else:
54
+ local = "0"
55
+ version_file = base_dir / "VERSION"
56
+ if version_file.exists():
57
+ local = version_file.read_text().strip()
58
+ remote = subprocess.check_output([
59
+ "git",
60
+ "show",
61
+ f"origin/{branch}:VERSION",
62
+ ], cwd=base_dir).decode().strip()
63
+ if local == remote:
64
+ if startup:
65
+ startup()
66
+ return
67
+ if notify:
68
+ notify("Upgrading...", "")
69
+ args = ["./upgrade.sh", "--no-restart"]
70
+
71
+ with log_file.open("a") as fh:
72
+ fh.write(f"{datetime.utcnow().isoformat()} running: {' '.join(args)}\n")
73
+
74
+ subprocess.run(args, cwd=base_dir, check=True)
75
+
76
+ service_file = base_dir / "locks/service.lck"
77
+ if service_file.exists():
78
+ service = service_file.read_text().strip()
79
+ subprocess.run([
80
+ "sudo",
81
+ "systemctl",
82
+ "kill",
83
+ "--signal=TERM",
84
+ service,
85
+ ])
86
+ else:
87
+ subprocess.run(["pkill", "-f", "manage.py runserver"])
88
+
89
+
90
+ @shared_task
91
+ def poll_email_collectors() -> None:
92
+ """Poll all configured email collectors for new messages."""
93
+ try:
94
+ from .models import EmailCollector
95
+ except Exception: # pragma: no cover - app not ready
96
+ return
97
+
98
+ for collector in EmailCollector.objects.all():
99
+ collector.collect()
100
+
core/tests.py ADDED
@@ -0,0 +1,483 @@
1
+ import os
2
+
3
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
4
+ import django
5
+ django.setup()
6
+
7
+ from django.test import Client, TestCase
8
+ from django.urls import reverse
9
+ from django.http import HttpRequest
10
+ import json
11
+ from unittest import mock
12
+
13
+ from django.utils import timezone
14
+ from .models import (
15
+ User,
16
+ EnergyAccount,
17
+ ElectricVehicle,
18
+ EnergyCredit,
19
+ Address,
20
+ Product,
21
+ Subscription,
22
+ Brand,
23
+ EVModel,
24
+ RFID,
25
+ FediverseProfile,
26
+ SecurityGroup,
27
+ )
28
+ from ocpp.models import Transaction, Charger
29
+
30
+ from django.core.exceptions import ValidationError
31
+ from django.core.management import call_command
32
+ from django.db import IntegrityError
33
+ from .backends import LocalhostAdminBackend
34
+
35
+
36
+ class DefaultAdminTests(TestCase):
37
+ def test_arthexis_is_default_user(self):
38
+ self.assertTrue(User.objects.filter(username="arthexis").exists())
39
+ self.assertFalse(User.all_objects.filter(username="admin").exists())
40
+
41
+ def test_admin_created_and_local_only(self):
42
+ backend = LocalhostAdminBackend()
43
+ req = HttpRequest()
44
+ req.META["REMOTE_ADDR"] = "127.0.0.1"
45
+ user = backend.authenticate(req, username="admin", password="admin")
46
+ self.assertIsNotNone(user)
47
+ self.assertEqual(user.pk, 2)
48
+
49
+ remote = HttpRequest()
50
+ remote.META["REMOTE_ADDR"] = "10.0.0.1"
51
+ self.assertIsNone(
52
+ backend.authenticate(remote, username="admin", password="admin")
53
+ )
54
+
55
+ def test_admin_respects_forwarded_for(self):
56
+ backend = LocalhostAdminBackend()
57
+
58
+ req = HttpRequest()
59
+ req.META["REMOTE_ADDR"] = "10.0.0.1"
60
+ req.META["HTTP_X_FORWARDED_FOR"] = "127.0.0.1"
61
+ self.assertIsNotNone(
62
+ backend.authenticate(req, username="admin", password="admin"),
63
+ "X-Forwarded-For should permit allowed IP",
64
+ )
65
+
66
+ blocked = HttpRequest()
67
+ blocked.META["REMOTE_ADDR"] = "10.0.0.1"
68
+ blocked.META["HTTP_X_FORWARDED_FOR"] = "8.8.8.8"
69
+ self.assertIsNone(
70
+ backend.authenticate(blocked, username="admin", password="admin")
71
+ )
72
+
73
+
74
+ class RFIDLoginTests(TestCase):
75
+ def setUp(self):
76
+ self.client = Client()
77
+ self.user = User.objects.create_user(username="alice", password="secret")
78
+ self.account = EnergyAccount.objects.create(user=self.user, name="ALICE")
79
+ tag = RFID.objects.create(rfid="CARD123")
80
+ self.account.rfids.add(tag)
81
+
82
+ def test_rfid_login_success(self):
83
+ response = self.client.post(
84
+ reverse("rfid-login"),
85
+ data={"rfid": "CARD123"},
86
+ content_type="application/json",
87
+ )
88
+ self.assertEqual(response.status_code, 200)
89
+ self.assertEqual(response.json()["username"], "alice")
90
+
91
+ def test_rfid_login_invalid(self):
92
+ response = self.client.post(
93
+ reverse("rfid-login"),
94
+ data={"rfid": "UNKNOWN"},
95
+ content_type="application/json",
96
+ )
97
+ self.assertEqual(response.status_code, 401)
98
+
99
+
100
+ class RFIDBatchApiTests(TestCase):
101
+ def setUp(self):
102
+ self.client = Client()
103
+ self.user = User.objects.create_user(username="bob", password="secret")
104
+ self.account = EnergyAccount.objects.create(user=self.user, name="BOB")
105
+ self.client.force_login(self.user)
106
+
107
+ def test_export_rfids(self):
108
+ tag_black = RFID.objects.create(rfid="CARD999")
109
+ tag_white = RFID.objects.create(rfid="CARD998", color=RFID.WHITE)
110
+ self.account.rfids.add(tag_black, tag_white)
111
+ response = self.client.get(reverse("rfid-batch"))
112
+ self.assertEqual(response.status_code, 200)
113
+ self.assertEqual(
114
+ response.json(),
115
+ {
116
+ "rfids": [
117
+ {
118
+ "rfid": "CARD999",
119
+ "energy_accounts": [self.account.id],
120
+ "allowed": True,
121
+ "color": "B",
122
+ "released": False,
123
+ }
124
+ ]
125
+ },
126
+ )
127
+
128
+ def test_export_rfids_color_filter(self):
129
+ RFID.objects.create(rfid="CARD111", color=RFID.WHITE)
130
+ response = self.client.get(reverse("rfid-batch"), {"color": "W"})
131
+ self.assertEqual(
132
+ response.json(),
133
+ {
134
+ "rfids": [
135
+ {
136
+ "rfid": "CARD111",
137
+ "energy_accounts": [],
138
+ "allowed": True,
139
+ "color": "W",
140
+ "released": False,
141
+ }
142
+ ]
143
+ },
144
+ )
145
+
146
+ def test_export_rfids_released_filter(self):
147
+ RFID.objects.create(rfid="CARD112", released=True)
148
+ RFID.objects.create(rfid="CARD113", released=False)
149
+ response = self.client.get(reverse("rfid-batch"), {"released": "true"})
150
+ self.assertEqual(
151
+ response.json(),
152
+ {
153
+ "rfids": [
154
+ {
155
+ "rfid": "CARD112",
156
+ "energy_accounts": [],
157
+ "allowed": True,
158
+ "color": "B",
159
+ "released": True,
160
+ }
161
+ ]
162
+ },
163
+ )
164
+
165
+ def test_import_rfids(self):
166
+ data = {
167
+ "rfids": [
168
+ {
169
+ "rfid": "A1B2C3D4",
170
+ "energy_accounts": [self.account.id],
171
+ "allowed": True,
172
+ "color": "W",
173
+ "released": True,
174
+ }
175
+ ]
176
+ }
177
+ response = self.client.post(
178
+ reverse("rfid-batch"),
179
+ data=json.dumps(data),
180
+ content_type="application/json",
181
+ )
182
+ self.assertEqual(response.status_code, 200)
183
+ self.assertEqual(response.json()["imported"], 1)
184
+ self.assertTrue(
185
+ RFID.objects.filter(
186
+ rfid="A1B2C3D4",
187
+ energy_accounts=self.account,
188
+ color=RFID.WHITE,
189
+ released=True,
190
+ ).exists()
191
+ )
192
+
193
+
194
+ class AllowedRFIDTests(TestCase):
195
+ def setUp(self):
196
+ self.user = User.objects.create_user(username="eve", password="secret")
197
+ self.account = EnergyAccount.objects.create(user=self.user, name="EVE")
198
+ self.rfid = RFID.objects.create(rfid="BAD123")
199
+ self.account.rfids.add(self.rfid)
200
+
201
+ def test_disallow_removes_and_blocks(self):
202
+ self.rfid.allowed = False
203
+ self.rfid.save()
204
+ self.account.refresh_from_db()
205
+ self.assertFalse(self.account.rfids.exists())
206
+
207
+ with self.assertRaises(IntegrityError):
208
+ RFID.objects.create(rfid="BAD123")
209
+
210
+
211
+ class RFIDValidationTests(TestCase):
212
+ def test_invalid_format_raises(self):
213
+ tag = RFID(rfid="xyz")
214
+ with self.assertRaises(ValidationError):
215
+ tag.full_clean()
216
+
217
+ def test_lowercase_saved_uppercase(self):
218
+ tag = RFID.objects.create(rfid="deadbeef")
219
+ self.assertEqual(tag.rfid, "DEADBEEF")
220
+
221
+ def test_long_rfid_allowed(self):
222
+ tag = RFID.objects.create(rfid="DEADBEEF10")
223
+ self.assertEqual(tag.rfid, "DEADBEEF10")
224
+
225
+ def test_find_user_by_rfid(self):
226
+ user = User.objects.create_user(username="finder", password="pwd")
227
+ acc = EnergyAccount.objects.create(user=user, name="FINDER")
228
+ tag = RFID.objects.create(rfid="ABCD1234")
229
+ acc.rfids.add(tag)
230
+ found = RFID.get_account_by_rfid("abcd1234")
231
+ self.assertEqual(found, acc)
232
+
233
+
234
+ class RFIDAssignmentTests(TestCase):
235
+ def setUp(self):
236
+ self.user1 = User.objects.create_user(username="user1", password="x")
237
+ self.user2 = User.objects.create_user(username="user2", password="x")
238
+ self.acc1 = EnergyAccount.objects.create(user=self.user1, name="USER1")
239
+ self.acc2 = EnergyAccount.objects.create(user=self.user2, name="USER2")
240
+ self.tag = RFID.objects.create(rfid="ABCDEF12")
241
+
242
+ def test_rfid_can_only_attach_to_one_account(self):
243
+ self.acc1.rfids.add(self.tag)
244
+ with self.assertRaises(ValidationError):
245
+ self.acc2.rfids.add(self.tag)
246
+
247
+
248
+ class EnergyAccountTests(TestCase):
249
+ def test_balance_calculation(self):
250
+ user = User.objects.create_user(username="balance", password="x")
251
+ acc = EnergyAccount.objects.create(user=user, name="BALANCE")
252
+ EnergyCredit.objects.create(account=acc, amount_kw=50)
253
+ charger = Charger.objects.create(charger_id="T1")
254
+ Transaction.objects.create(
255
+ charger=charger,
256
+ account=acc,
257
+ meter_start=0,
258
+ meter_stop=20,
259
+ start_time=timezone.now(),
260
+ stop_time=timezone.now(),
261
+ )
262
+ self.assertEqual(acc.total_kw_spent, 20)
263
+ self.assertEqual(acc.balance_kw, 30)
264
+
265
+ def test_authorization_requires_positive_balance(self):
266
+ user = User.objects.create_user(username="auth", password="x")
267
+ acc = EnergyAccount.objects.create(user=user, name="AUTH")
268
+ self.assertFalse(acc.can_authorize())
269
+
270
+ EnergyCredit.objects.create(account=acc, amount_kw=5)
271
+ self.assertTrue(acc.can_authorize())
272
+
273
+ def test_service_account_ignores_balance(self):
274
+ user = User.objects.create_user(username="service", password="x")
275
+ acc = EnergyAccount.objects.create(user=user, service_account=True, name="SERVICE")
276
+ self.assertTrue(acc.can_authorize())
277
+
278
+ def test_account_without_user(self):
279
+ acc = EnergyAccount.objects.create(name="NOUSER")
280
+ tag = RFID.objects.create(rfid="NOUSER1")
281
+ acc.rfids.add(tag)
282
+ self.assertIsNone(acc.user)
283
+ self.assertTrue(acc.rfids.filter(rfid="NOUSER1").exists())
284
+
285
+
286
+ class ElectricVehicleTests(TestCase):
287
+ def test_account_can_have_multiple_vehicles(self):
288
+ user = User.objects.create_user(username="cars", password="x")
289
+ acc = EnergyAccount.objects.create(user=user, name="CARS")
290
+ tesla = Brand.objects.create(name="Tesla")
291
+ nissan = Brand.objects.create(name="Nissan")
292
+ model_s = EVModel.objects.create(brand=tesla, name="Model S")
293
+ leaf = EVModel.objects.create(brand=nissan, name="Leaf")
294
+ ElectricVehicle.objects.create(
295
+ account=acc, brand=tesla, model=model_s, vin="VIN12345678901234"
296
+ )
297
+ ElectricVehicle.objects.create(
298
+ account=acc, brand=nissan, model=leaf, vin="VIN23456789012345"
299
+ )
300
+ self.assertEqual(acc.vehicles.count(), 2)
301
+
302
+
303
+ class AddressTests(TestCase):
304
+ def test_invalid_municipality_state(self):
305
+ addr = Address(
306
+ street="Main",
307
+ number="1",
308
+ municipality="Monterrey",
309
+ state=Address.State.COAHUILA,
310
+ postal_code="00000",
311
+ )
312
+ with self.assertRaises(ValidationError):
313
+ addr.full_clean()
314
+
315
+ def test_user_link(self):
316
+ addr = Address.objects.create(
317
+ street="Main",
318
+ number="2",
319
+ municipality="Monterrey",
320
+ state=Address.State.NUEVO_LEON,
321
+ postal_code="64000",
322
+ )
323
+ user = User.objects.create_user(username="addr", password="pwd", address=addr)
324
+ self.assertEqual(user.address, addr)
325
+
326
+
327
+ class SubscriptionTests(TestCase):
328
+ def setUp(self):
329
+ self.client = Client()
330
+ self.user = User.objects.create_user(username="bob", password="pwd")
331
+ self.account = EnergyAccount.objects.create(user=self.user, name="SUBSCRIBER")
332
+ self.product = Product.objects.create(name="Gold", renewal_period=30)
333
+ self.client.force_login(self.user)
334
+
335
+ def test_create_and_list_subscription(self):
336
+ response = self.client.post(
337
+ reverse("add-subscription"),
338
+ data={"account_id": self.account.id, "product_id": self.product.id},
339
+ content_type="application/json",
340
+ )
341
+ self.assertEqual(response.status_code, 200)
342
+ self.assertEqual(Subscription.objects.count(), 1)
343
+
344
+ list_resp = self.client.get(
345
+ reverse("subscription-list"), {"account_id": self.account.id}
346
+ )
347
+ self.assertEqual(list_resp.status_code, 200)
348
+ data = list_resp.json()
349
+ self.assertEqual(len(data["subscriptions"]), 1)
350
+ self.assertEqual(data["subscriptions"][0]["product__name"], "Gold")
351
+
352
+ def test_product_list(self):
353
+ response = self.client.get(reverse("product-list"))
354
+ self.assertEqual(response.status_code, 200)
355
+ data = response.json()
356
+ self.assertEqual(len(data["products"]), 1)
357
+ self.assertEqual(data["products"][0]["name"], "Gold")
358
+
359
+
360
+ class OnboardingWizardTests(TestCase):
361
+ def setUp(self):
362
+ self.client = Client()
363
+ User.objects.create_superuser("super", "super@example.com", "pwd")
364
+ self.client.force_login(User.objects.get(username="super"))
365
+
366
+ def test_onboarding_flow_creates_account(self):
367
+ details_url = reverse("admin:core_energyaccount_onboard_details")
368
+ response = self.client.get(details_url)
369
+ self.assertEqual(response.status_code, 200)
370
+ data = {
371
+ "first_name": "John",
372
+ "last_name": "Doe",
373
+ "rfid": "ABCD1234",
374
+ "vehicle_id": "VIN12345678901234",
375
+ }
376
+ resp = self.client.post(details_url, data)
377
+ self.assertEqual(resp.status_code, 302)
378
+ self.assertEqual(resp.url, reverse("admin:core_energyaccount_changelist"))
379
+ user = User.objects.get(first_name="John", last_name="Doe")
380
+ self.assertFalse(user.is_active)
381
+ account = EnergyAccount.objects.get(user=user)
382
+ self.assertTrue(account.rfids.filter(rfid="ABCD1234").exists())
383
+ self.assertTrue(account.vehicles.filter(vin="VIN12345678901234").exists())
384
+
385
+
386
+ class EVBrandFixtureTests(TestCase):
387
+ def test_ev_brand_fixture_loads(self):
388
+ call_command(
389
+ "loaddata",
390
+ "core/fixtures/ev_brands.json",
391
+ "core/fixtures/ev_models.json",
392
+ verbosity=0,
393
+ )
394
+ porsche = Brand.objects.get(name="Porsche")
395
+ audi = Brand.objects.get(name="Audi")
396
+ self.assertTrue(
397
+ {"WP0", "WP1"} <= set(porsche.wmi_codes.values_list("code", flat=True))
398
+ )
399
+ self.assertTrue(
400
+ set(audi.wmi_codes.values_list("code", flat=True)) >= {"WAU", "TRU"}
401
+ )
402
+ self.assertTrue(EVModel.objects.filter(brand=porsche, name="Taycan").exists())
403
+ self.assertTrue(EVModel.objects.filter(brand=audi, name="e-tron GT").exists())
404
+
405
+ def test_brand_from_vin(self):
406
+ call_command(
407
+ "loaddata",
408
+ "core/fixtures/ev_brands.json",
409
+ verbosity=0,
410
+ )
411
+ self.assertEqual(Brand.from_vin("WP0ZZZ12345678901").name, "Porsche")
412
+ self.assertEqual(Brand.from_vin("WAUZZZ12345678901").name, "Audi")
413
+ self.assertIsNone(Brand.from_vin("XYZ12345678901234"))
414
+
415
+
416
+ class RFIDFixtureTests(TestCase):
417
+ def test_fixture_assigns_gelectriic_rfid(self):
418
+ call_command(
419
+ "loaddata",
420
+ "core/fixtures/users.json",
421
+ "core/fixtures/energy_accounts.json",
422
+ "core/fixtures/rfids.json",
423
+ verbosity=0,
424
+ )
425
+ account = EnergyAccount.objects.get(name="GELECTRIIC")
426
+ tag = RFID.objects.get(rfid="FFFFFFFF")
427
+ self.assertIn(account, tag.energy_accounts.all())
428
+ self.assertEqual(tag.energy_accounts.count(), 1)
429
+
430
+
431
+ class RFIDKeyVerificationFlagTests(TestCase):
432
+ def test_flags_reset_on_key_change(self):
433
+ tag = RFID.objects.create(
434
+ rfid="ABC12345", key_a_verified=True, key_b_verified=True
435
+ )
436
+ tag.key_a = "A1A1A1A1A1A1"
437
+ tag.save()
438
+ self.assertFalse(tag.key_a_verified)
439
+ tag.key_b = "B1B1B1B1B1B1"
440
+ tag.save()
441
+ self.assertFalse(tag.key_b_verified)
442
+
443
+
444
+ class SecurityGroupTests(TestCase):
445
+ def test_parent_and_user_assignment(self):
446
+ parent = SecurityGroup.objects.create(name="Parents")
447
+ child = SecurityGroup.objects.create(name="Children", parent=parent)
448
+ user = User.objects.create_user(username="sg_user", password="secret")
449
+ child.user_set.add(user)
450
+ self.assertEqual(child.parent, parent)
451
+ self.assertIn(user, child.user_set.all())
452
+
453
+
454
+ class FediverseProfileTests(TestCase):
455
+ def setUp(self):
456
+ self.user = User.objects.create_user(username="fed", password="secret")
457
+
458
+ @mock.patch("requests.get")
459
+ def test_connection_success_sets_verified(self, mock_get):
460
+ mock_get.return_value.ok = True
461
+ mock_get.return_value.raise_for_status.return_value = None
462
+ profile = FediverseProfile.objects.create(
463
+ user=self.user,
464
+ service=FediverseProfile.MASTODON,
465
+ host="example.com",
466
+ handle="fed",
467
+ access_token="tok",
468
+ )
469
+ self.assertTrue(profile.test_connection())
470
+ self.assertIsNotNone(profile.verified_on)
471
+
472
+ @mock.patch("requests.get", side_effect=Exception("boom"))
473
+ def test_connection_failure_raises(self, mock_get):
474
+ profile = FediverseProfile.objects.create(
475
+ user=self.user,
476
+ service=FediverseProfile.MASTODON,
477
+ host="example.com",
478
+ handle="fed",
479
+ )
480
+ with self.assertRaises(ValidationError):
481
+ profile.test_connection()
482
+ self.assertIsNone(profile.verified_on)
483
+
core/urls.py ADDED
@@ -0,0 +1,11 @@
1
+ from django.urls import path
2
+
3
+ from . import views
4
+
5
+ urlpatterns = [
6
+ path("rfid-login/", views.rfid_login, name="rfid-login"),
7
+ path("rfids/", views.rfid_batch, name="rfid-batch"),
8
+ path("products/", views.product_list, name="product-list"),
9
+ path("subscribe/", views.add_subscription, name="add-subscription"),
10
+ path("list/", views.subscription_list, name="subscription-list"),
11
+ ]