arthexis 0.1.9__py3-none-any.whl → 0.1.11__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 (51) hide show
  1. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/METADATA +76 -23
  2. arthexis-0.1.11.dist-info/RECORD +99 -0
  3. config/context_processors.py +1 -0
  4. config/settings.py +245 -26
  5. config/urls.py +11 -4
  6. core/admin.py +585 -57
  7. core/apps.py +29 -1
  8. core/auto_upgrade.py +57 -0
  9. core/backends.py +115 -3
  10. core/environment.py +23 -5
  11. core/fields.py +93 -0
  12. core/mailer.py +3 -1
  13. core/models.py +482 -38
  14. core/reference_utils.py +108 -0
  15. core/sigil_builder.py +23 -5
  16. core/sigil_resolver.py +35 -4
  17. core/system.py +400 -140
  18. core/tasks.py +151 -8
  19. core/temp_passwords.py +181 -0
  20. core/test_system_info.py +97 -1
  21. core/tests.py +393 -15
  22. core/user_data.py +154 -16
  23. core/views.py +499 -20
  24. nodes/admin.py +149 -6
  25. nodes/backends.py +125 -18
  26. nodes/dns.py +203 -0
  27. nodes/models.py +498 -9
  28. nodes/tests.py +682 -3
  29. nodes/views.py +154 -7
  30. ocpp/admin.py +63 -3
  31. ocpp/consumers.py +255 -41
  32. ocpp/evcs.py +6 -3
  33. ocpp/models.py +52 -7
  34. ocpp/reference_utils.py +42 -0
  35. ocpp/simulator.py +62 -5
  36. ocpp/store.py +30 -0
  37. ocpp/test_rfid.py +169 -7
  38. ocpp/tests.py +414 -8
  39. ocpp/views.py +109 -76
  40. pages/admin.py +9 -1
  41. pages/context_processors.py +24 -4
  42. pages/defaults.py +14 -0
  43. pages/forms.py +131 -0
  44. pages/models.py +53 -14
  45. pages/tests.py +450 -14
  46. pages/urls.py +4 -0
  47. pages/views.py +419 -110
  48. arthexis-0.1.9.dist-info/RECORD +0 -92
  49. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
  50. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
  51. {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/top_level.txt +0 -0
ocpp/tests.py CHANGED
@@ -1,10 +1,20 @@
1
+ import os
2
+
3
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
4
+
5
+ import django
6
+
7
+ django.setup()
8
+
1
9
  from asgiref.testing import ApplicationCommunicator
2
10
  from channels.testing import WebsocketCommunicator
3
11
  from channels.db import database_sync_to_async
4
12
  from asgiref.sync import async_to_sync
5
13
  from django.test import Client, TransactionTestCase, TestCase, override_settings
6
14
  from unittest import skip
7
- from unittest.mock import patch
15
+ from contextlib import suppress
16
+ from types import SimpleNamespace
17
+ from unittest.mock import patch, Mock
8
18
  from django.contrib.auth import get_user_model
9
19
  from django.urls import reverse
10
20
  from django.utils import timezone
@@ -17,8 +27,8 @@ from nodes.models import Node, NodeRole
17
27
  from config.asgi import application
18
28
 
19
29
  from .models import Transaction, Charger, Simulator, MeterReading, Location
20
- from core.models import EnergyAccount, EnergyCredit
21
- from core.models import RFID
30
+ from .consumers import CSMSConsumer
31
+ from core.models import EnergyAccount, EnergyCredit, Reference, RFID
22
32
  from . import store
23
33
  from django.db.models.deletion import ProtectedError
24
34
  from decimal import Decimal
@@ -131,6 +141,17 @@ class ChargerUrlFallbackTests(TestCase):
131
141
  self.assertTrue(charger.reference.value.startswith("http://fallback.example"))
132
142
  self.assertTrue(charger.reference.value.endswith("/c/NO_SITE/"))
133
143
 
144
+ def test_reference_not_created_for_loopback_domain(self):
145
+ site = Site.objects.get_current()
146
+ site.domain = "127.0.0.1"
147
+ site.save()
148
+ Site.objects.clear_cache()
149
+
150
+ charger = Charger.objects.create(charger_id="LOCAL_LOOP")
151
+ charger.refresh_from_db()
152
+
153
+ self.assertIsNone(charger.reference)
154
+
134
155
 
135
156
  class SinkConsumerTests(TransactionTestCase):
136
157
  async def test_sink_replies(self):
@@ -251,11 +272,83 @@ class CSMSConsumerTests(TransactionTestCase):
251
272
 
252
273
  mock_broadcast.assert_called_once()
253
274
  _, kwargs = mock_broadcast.call_args
254
- self.assertEqual(kwargs["subject"], "charging-started")
255
- payload = json.loads(kwargs["body"])
256
- self.assertEqual(payload["location"], "Test Location")
257
- self.assertEqual(payload["sn"], "NETMSG")
258
- self.assertEqual(payload["cid"], "1")
275
+ self.assertEqual(kwargs["subject"], "NETMSG")
276
+ body = kwargs["body"]
277
+ self.assertRegex(body, r"^\d+\.\d kWh \d{2}:\d{2}$")
278
+
279
+ async def test_consumption_message_updates_existing_entry(self):
280
+ original_interval = CSMSConsumer.consumption_update_interval
281
+ CSMSConsumer.consumption_update_interval = 0.01
282
+ await database_sync_to_async(Charger.objects.create)(charger_id="UPDATEMSG")
283
+ communicator = WebsocketCommunicator(application, "/UPDATEMSG/")
284
+ connected, _ = await communicator.connect()
285
+ self.assertTrue(connected)
286
+
287
+ message_mock = Mock()
288
+ message_mock.uuid = "mock-uuid"
289
+ message_mock.save = Mock()
290
+ message_mock.propagate = Mock()
291
+
292
+ filter_mock = Mock()
293
+ filter_mock.first.return_value = message_mock
294
+
295
+ broadcast_result = SimpleNamespace(uuid="mock-uuid")
296
+
297
+ try:
298
+ with patch(
299
+ "nodes.models.NetMessage.broadcast", return_value=broadcast_result
300
+ ) as mock_broadcast, patch(
301
+ "nodes.models.NetMessage.objects.filter", return_value=filter_mock
302
+ ):
303
+ await communicator.send_json_to(
304
+ [2, "1", "StartTransaction", {"meterStart": 1}]
305
+ )
306
+ await communicator.receive_json_from()
307
+ mock_broadcast.assert_called_once()
308
+ await asyncio.sleep(0.05)
309
+ await communicator.disconnect()
310
+ finally:
311
+ CSMSConsumer.consumption_update_interval = original_interval
312
+ with suppress(Exception):
313
+ await communicator.disconnect()
314
+
315
+ self.assertTrue(message_mock.save.called)
316
+ self.assertTrue(message_mock.propagate.called)
317
+
318
+ async def test_consumption_message_final_update_on_disconnect(self):
319
+ await database_sync_to_async(Charger.objects.create)(charger_id="FINALMSG")
320
+ communicator = WebsocketCommunicator(application, "/FINALMSG/")
321
+ connected, _ = await communicator.connect()
322
+ self.assertTrue(connected)
323
+
324
+ message_mock = Mock()
325
+ message_mock.uuid = "mock-uuid"
326
+ message_mock.save = Mock()
327
+ message_mock.propagate = Mock()
328
+
329
+ filter_mock = Mock()
330
+ filter_mock.first.return_value = message_mock
331
+
332
+ broadcast_result = SimpleNamespace(uuid="mock-uuid")
333
+
334
+ try:
335
+ with patch(
336
+ "nodes.models.NetMessage.broadcast", return_value=broadcast_result
337
+ ) as mock_broadcast, patch(
338
+ "nodes.models.NetMessage.objects.filter", return_value=filter_mock
339
+ ):
340
+ await communicator.send_json_to(
341
+ [2, "1", "StartTransaction", {"meterStart": 1}]
342
+ )
343
+ await communicator.receive_json_from()
344
+ mock_broadcast.assert_called_once()
345
+ await communicator.disconnect()
346
+ finally:
347
+ with suppress(Exception):
348
+ await communicator.disconnect()
349
+
350
+ self.assertTrue(message_mock.save.called)
351
+ self.assertTrue(message_mock.propagate.called)
259
352
 
260
353
  async def test_rfid_unbound_instance_created(self):
261
354
  await database_sync_to_async(Charger.objects.create)(charger_id="NEWRFID")
@@ -529,6 +622,81 @@ class CSMSConsumerTests(TransactionTestCase):
529
622
  self.assertIn(2, connectors)
530
623
  self.assertIn(None, connectors)
531
624
 
625
+ async def test_console_reference_created_for_aggregate_connector(self):
626
+ communicator = ClientWebsocketCommunicator(
627
+ application,
628
+ "/CONREF/",
629
+ client=("203.0.113.5", 12345),
630
+ )
631
+ connected, _ = await communicator.connect()
632
+ self.assertTrue(connected)
633
+
634
+ await communicator.send_json_to([2, "1", "BootNotification", {}])
635
+ await communicator.receive_json_from()
636
+
637
+ reference = await database_sync_to_async(
638
+ lambda: Reference.objects.get(alt_text="CONREF Console")
639
+ )()
640
+ self.assertEqual(reference.value, "http://203.0.113.5:8900")
641
+ self.assertTrue(reference.show_in_header)
642
+
643
+ await communicator.send_json_to(
644
+ [
645
+ 2,
646
+ "2",
647
+ "StatusNotification",
648
+ {"connectorId": 1, "status": "Available"},
649
+ ]
650
+ )
651
+ await communicator.receive_json_from()
652
+
653
+ count = await database_sync_to_async(
654
+ lambda: Reference.objects.filter(alt_text="CONREF Console").count()
655
+ )()
656
+ self.assertEqual(count, 1)
657
+
658
+ await communicator.disconnect()
659
+
660
+ async def test_console_reference_uses_forwarded_for_header(self):
661
+ communicator = ClientWebsocketCommunicator(
662
+ application,
663
+ "/FORWARDED/",
664
+ client=("127.0.0.1", 23456),
665
+ headers=[(b"x-forwarded-for", b"198.51.100.75, 127.0.0.1")],
666
+ )
667
+ connected, _ = await communicator.connect()
668
+ self.assertTrue(connected)
669
+ self.assertIn("198.51.100.75", store.ip_connections)
670
+
671
+ await communicator.send_json_to([2, "1", "BootNotification", {}])
672
+ await communicator.receive_json_from()
673
+
674
+ reference = await database_sync_to_async(
675
+ lambda: Reference.objects.get(alt_text="FORWARDED Console")
676
+ )()
677
+ self.assertEqual(reference.value, "http://198.51.100.75:8900")
678
+
679
+ await communicator.disconnect()
680
+
681
+ async def test_console_reference_skips_loopback_ip(self):
682
+ communicator = ClientWebsocketCommunicator(
683
+ application,
684
+ "/LOCAL/",
685
+ client=("127.0.0.1", 34567),
686
+ )
687
+ connected, _ = await communicator.connect()
688
+ self.assertTrue(connected)
689
+
690
+ await communicator.send_json_to([2, "1", "BootNotification", {}])
691
+ await communicator.receive_json_from()
692
+
693
+ exists = await database_sync_to_async(
694
+ lambda: Reference.objects.filter(alt_text="LOCAL Console").exists()
695
+ )()
696
+ self.assertFalse(exists)
697
+
698
+ await communicator.disconnect()
699
+
532
700
  async def test_transaction_created_from_meter_values(self):
533
701
  communicator = WebsocketCommunicator(application, "/NOSTART/")
534
702
  connected, _ = await communicator.connect()
@@ -914,6 +1082,78 @@ class CSMSConsumerTests(TransactionTestCase):
914
1082
  store.transactions.pop(key2, None)
915
1083
 
916
1084
 
1085
+ async def test_rate_limit_blocks_third_connection(self):
1086
+ store.ip_connections.clear()
1087
+ ip = "203.0.113.10"
1088
+ communicator1 = ClientWebsocketCommunicator(
1089
+ application, "/IPLIMIT1/", client=(ip, 1001)
1090
+ )
1091
+ communicator2 = ClientWebsocketCommunicator(
1092
+ application, "/IPLIMIT2/", client=(ip, 1002)
1093
+ )
1094
+ communicator3 = ClientWebsocketCommunicator(
1095
+ application, "/IPLIMIT3/", client=(ip, 1003)
1096
+ )
1097
+ other = ClientWebsocketCommunicator(
1098
+ application, "/OTHERIP/", client=("198.51.100.5", 2001)
1099
+ )
1100
+ connected1 = connected2 = connected_other = False
1101
+ try:
1102
+ connected1, _ = await communicator1.connect()
1103
+ self.assertTrue(connected1)
1104
+ connected2, _ = await communicator2.connect()
1105
+ self.assertTrue(connected2)
1106
+ connected3, code = await communicator3.connect()
1107
+ self.assertFalse(connected3)
1108
+ self.assertEqual(code, 4003)
1109
+ connected_other, _ = await other.connect()
1110
+ self.assertTrue(connected_other)
1111
+ finally:
1112
+ if connected1:
1113
+ await communicator1.disconnect()
1114
+ if connected2:
1115
+ await communicator2.disconnect()
1116
+ if connected_other:
1117
+ await other.disconnect()
1118
+
1119
+ async def test_rate_limit_allows_reconnect_after_disconnect(self):
1120
+ store.ip_connections.clear()
1121
+ ip = "203.0.113.20"
1122
+ communicator1 = ClientWebsocketCommunicator(
1123
+ application, "/LIMITRESET1/", client=(ip, 3001)
1124
+ )
1125
+ communicator2 = ClientWebsocketCommunicator(
1126
+ application, "/LIMITRESET2/", client=(ip, 3002)
1127
+ )
1128
+ communicator3 = ClientWebsocketCommunicator(
1129
+ application, "/LIMITRESET3/", client=(ip, 3003)
1130
+ )
1131
+ communicator3_retry = None
1132
+ connected1 = connected2 = connected3_retry = False
1133
+ try:
1134
+ connected1, _ = await communicator1.connect()
1135
+ self.assertTrue(connected1)
1136
+ connected2, _ = await communicator2.connect()
1137
+ self.assertTrue(connected2)
1138
+ connected3, code = await communicator3.connect()
1139
+ self.assertFalse(connected3)
1140
+ self.assertEqual(code, 4003)
1141
+ await communicator1.disconnect()
1142
+ connected1 = False
1143
+ communicator3_retry = ClientWebsocketCommunicator(
1144
+ application, "/LIMITRESET4/", client=(ip, 3004)
1145
+ )
1146
+ connected3_retry, _ = await communicator3_retry.connect()
1147
+ self.assertTrue(connected3_retry)
1148
+ finally:
1149
+ if connected1:
1150
+ await communicator1.disconnect()
1151
+ if connected2:
1152
+ await communicator2.disconnect()
1153
+ if connected3_retry and communicator3_retry is not None:
1154
+ await communicator3_retry.disconnect()
1155
+
1156
+
917
1157
  class ChargerLandingTests(TestCase):
918
1158
  def setUp(self):
919
1159
  self.client = Client()
@@ -1281,6 +1521,42 @@ class SimulatorAdminTests(TransactionTestCase):
1281
1521
  resp = self.client.get(url)
1282
1522
  self.assertContains(resp, "ws://h:1111/SIMY/")
1283
1523
 
1524
+ def test_admin_ws_url_without_port(self):
1525
+ sim = Simulator.objects.create(
1526
+ name="SIMNP", cp_path="SIMNP", host="h", ws_port=None
1527
+ )
1528
+ url = reverse("admin:ocpp_simulator_changelist")
1529
+ resp = self.client.get(url)
1530
+ self.assertContains(resp, "ws://h/SIMNP/")
1531
+
1532
+ def test_send_open_door_action_requires_running_simulator(self):
1533
+ sim = Simulator.objects.create(name="SIMDO", cp_path="SIMDO")
1534
+ url = reverse("admin:ocpp_simulator_changelist")
1535
+ resp = self.client.post(
1536
+ url,
1537
+ {"action": "send_open_door", "_selected_action": [sim.pk]},
1538
+ follow=True,
1539
+ )
1540
+ self.assertEqual(resp.status_code, 200)
1541
+ self.assertContains(resp, "simulator is not running")
1542
+ self.assertFalse(Simulator.objects.get(pk=sim.pk).door_open)
1543
+
1544
+ def test_send_open_door_action_triggers_simulator(self):
1545
+ sim = Simulator.objects.create(name="SIMTRIG", cp_path="SIMTRIG")
1546
+ stub = SimpleNamespace(trigger_door_open=Mock())
1547
+ store.simulators[sim.pk] = stub
1548
+ url = reverse("admin:ocpp_simulator_changelist")
1549
+ resp = self.client.post(
1550
+ url,
1551
+ {"action": "send_open_door", "_selected_action": [sim.pk]},
1552
+ follow=True,
1553
+ )
1554
+ self.assertEqual(resp.status_code, 200)
1555
+ stub.trigger_door_open.assert_called_once()
1556
+ self.assertContains(resp, "DoorOpen status notification sent")
1557
+ self.assertFalse(Simulator.objects.get(pk=sim.pk).door_open)
1558
+ store.simulators.pop(sim.pk, None)
1559
+
1284
1560
  def test_as_config_includes_custom_fields(self):
1285
1561
  sim = Simulator.objects.create(
1286
1562
  name="SIM3",
@@ -1298,6 +1574,45 @@ class SimulatorAdminTests(TransactionTestCase):
1298
1574
  self.assertEqual(cfg.pre_charge_delay, 5)
1299
1575
  self.assertEqual(cfg.vin, "WP0ZZZ99999999999")
1300
1576
 
1577
+ def _post_simulator_change(self, sim: Simulator, **overrides):
1578
+ url = reverse("admin:ocpp_simulator_change", args=[sim.pk])
1579
+ data = {
1580
+ "name": sim.name,
1581
+ "cp_path": sim.cp_path,
1582
+ "host": sim.host,
1583
+ "ws_port": sim.ws_port or "",
1584
+ "rfid": sim.rfid,
1585
+ "duration": sim.duration,
1586
+ "interval": sim.interval,
1587
+ "pre_charge_delay": sim.pre_charge_delay,
1588
+ "kw_max": sim.kw_max,
1589
+ "repeat": "on" if sim.repeat else "",
1590
+ "username": sim.username,
1591
+ "password": sim.password,
1592
+ "door_open": "on" if overrides.get("door_open", False) else "",
1593
+ "_save": "Save",
1594
+ }
1595
+ data.update(overrides)
1596
+ return self.client.post(url, data, follow=True)
1597
+
1598
+ def test_save_model_triggers_door_open(self):
1599
+ sim = Simulator.objects.create(name="SIMSAVE", cp_path="SIMSAVE")
1600
+ stub = SimpleNamespace(trigger_door_open=Mock())
1601
+ store.simulators[sim.pk] = stub
1602
+ resp = self._post_simulator_change(sim, door_open="on")
1603
+ self.assertEqual(resp.status_code, 200)
1604
+ stub.trigger_door_open.assert_called_once()
1605
+ self.assertContains(resp, "DoorOpen status notification sent")
1606
+ self.assertFalse(Simulator.objects.get(pk=sim.pk).door_open)
1607
+ store.simulators.pop(sim.pk, None)
1608
+
1609
+ def test_save_model_reports_error_when_not_running(self):
1610
+ sim = Simulator.objects.create(name="SIMERR", cp_path="SIMERR")
1611
+ resp = self._post_simulator_change(sim, door_open="on")
1612
+ self.assertEqual(resp.status_code, 200)
1613
+ self.assertContains(resp, "simulator is not running")
1614
+ self.assertFalse(Simulator.objects.get(pk=sim.pk).door_open)
1615
+
1301
1616
  async def test_unknown_charger_auto_registered(self):
1302
1617
  communicator = WebsocketCommunicator(application, "/NEWCHG/")
1303
1618
  connected, _ = await communicator.connect()
@@ -1781,6 +2096,80 @@ class ChargePointSimulatorTests(TransactionTestCase):
1781
2096
  server.close()
1782
2097
  await server.wait_closed()
1783
2098
 
2099
+ async def test_door_open_event_sends_notifications(self):
2100
+ status_payloads = []
2101
+
2102
+ async def handler(ws):
2103
+ async for msg in ws:
2104
+ data = json.loads(msg)
2105
+ action = data[2]
2106
+ if action == "BootNotification":
2107
+ await ws.send(
2108
+ json.dumps(
2109
+ [
2110
+ 3,
2111
+ data[1],
2112
+ {"status": "Accepted", "currentTime": "2024-01-01T00:00:00Z"},
2113
+ ]
2114
+ )
2115
+ )
2116
+ elif action == "Authorize":
2117
+ await ws.send(json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}]))
2118
+ elif action == "StatusNotification":
2119
+ status_payloads.append(data[3])
2120
+ await ws.send(json.dumps([3, data[1], {}]))
2121
+ elif action == "StartTransaction":
2122
+ await ws.send(
2123
+ json.dumps(
2124
+ [
2125
+ 3,
2126
+ data[1],
2127
+ {"transactionId": 1, "idTagInfo": {"status": "Accepted"}},
2128
+ ]
2129
+ )
2130
+ )
2131
+ elif action == "MeterValues":
2132
+ await ws.send(json.dumps([3, data[1], {}]))
2133
+ elif action == "StopTransaction":
2134
+ await ws.send(json.dumps([3, data[1], {"idTagInfo": {"status": "Accepted"}}]))
2135
+ break
2136
+
2137
+ server = await websockets.serve(
2138
+ handler, "127.0.0.1", 0, subprotocols=["ocpp1.6"]
2139
+ )
2140
+ port = server.sockets[0].getsockname()[1]
2141
+
2142
+ cfg = SimulatorConfig(
2143
+ host="127.0.0.1",
2144
+ ws_port=port,
2145
+ cp_path="SIMDOOR/",
2146
+ duration=0.2,
2147
+ interval=0.05,
2148
+ pre_charge_delay=0.0,
2149
+ )
2150
+ sim = ChargePointSimulator(cfg)
2151
+ sim.trigger_door_open()
2152
+ try:
2153
+ await sim._run_session()
2154
+ finally:
2155
+ server.close()
2156
+ await server.wait_closed()
2157
+ store.clear_log(cfg.cp_path, log_type="simulator")
2158
+
2159
+ door_open_messages = [p for p in status_payloads if p.get("errorCode") == "DoorOpen"]
2160
+ door_closed_messages = [p for p in status_payloads if p.get("errorCode") == "NoError"]
2161
+ self.assertTrue(door_open_messages)
2162
+ self.assertTrue(door_closed_messages)
2163
+ first_open = next(
2164
+ idx for idx, payload in enumerate(status_payloads) if payload.get("errorCode") == "DoorOpen"
2165
+ )
2166
+ first_close = next(
2167
+ idx for idx, payload in enumerate(status_payloads) if payload.get("errorCode") == "NoError"
2168
+ )
2169
+ self.assertLess(first_open, first_close)
2170
+ self.assertEqual(door_open_messages[0].get("status"), "Faulted")
2171
+ self.assertEqual(door_closed_messages[0].get("status"), "Available")
2172
+
1784
2173
 
1785
2174
  class PurgeMeterReadingsTaskTests(TestCase):
1786
2175
  def test_purge_old_meter_readings(self):
@@ -2294,3 +2683,20 @@ class LiveUpdateViewTests(TestCase):
2294
2683
  resp = self.client.get(reverse("cp-simulator"))
2295
2684
  self.assertEqual(resp.context["request"].live_update_interval, 5)
2296
2685
  self.assertContains(resp, "setInterval(() => location.reload()")
2686
+
2687
+ def test_dashboard_hides_private_chargers(self):
2688
+ public = Charger.objects.create(charger_id="PUBLICCP")
2689
+ private = Charger.objects.create(
2690
+ charger_id="PRIVATECP", public_display=False
2691
+ )
2692
+
2693
+ resp = self.client.get(reverse("ocpp-dashboard"))
2694
+ chargers = [item["charger"] for item in resp.context["chargers"]]
2695
+ self.assertIn(public, chargers)
2696
+ self.assertNotIn(private, chargers)
2697
+
2698
+ list_response = self.client.get(reverse("charger-list"))
2699
+ payload = list_response.json()
2700
+ ids = [item["charger_id"] for item in payload["chargers"]]
2701
+ self.assertIn(public.charger_id, ids)
2702
+ self.assertNotIn(private.charger_id, ids)