test_satellite.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. # -*- coding: utf-8 -*-
  2. """
  3. Unit-Tests für trixy_core.satellite.
  4. """
  5. import json
  6. import pytest
  7. from datetime import datetime
  8. from unittest.mock import MagicMock, AsyncMock
  9. from pathlib import Path
  10. from trixy_core.satellite.satellite import Satellite, ConnectionState, SatelliteSocket
  11. from trixy_core.satellite.satellite_manager import SatelliteManager
  12. from trixy_core.satellite.registration_manager import RegistrationManager, PendingRegistration
  13. class TestConnectionState:
  14. """Tests für ConnectionState."""
  15. def test_states_exist(self):
  16. """Testet, dass alle Zustände existieren."""
  17. assert hasattr(ConnectionState, "DISCONNECTED")
  18. assert hasattr(ConnectionState, "CONNECTING")
  19. assert hasattr(ConnectionState, "CONNECTED")
  20. assert hasattr(ConnectionState, "AUTHENTICATED")
  21. assert hasattr(ConnectionState, "DISCONNECTING")
  22. assert hasattr(ConnectionState, "ERROR")
  23. class TestSatellite:
  24. """Tests für Satellite-Klasse."""
  25. @pytest.fixture
  26. def satellite(self):
  27. """Erstellt einen Test-Satellite."""
  28. return Satellite(
  29. satellite_id="sat-test-123",
  30. room_id="wohnzimmer",
  31. mac_address="AA:BB:CC:DD:EE:FF",
  32. alias="Test Echo",
  33. ip_address="192.168.1.100"
  34. )
  35. def test_satellite_creation(self, satellite):
  36. """Testet die Satellite-Erstellung."""
  37. assert satellite.id == "sat-test-123"
  38. assert satellite.room_id == "wohnzimmer"
  39. assert satellite.mac_address == "AA:BB:CC:DD:EE:FF"
  40. assert satellite.alias == "Test Echo"
  41. assert satellite.ip_address == "192.168.1.100"
  42. def test_satellite_auto_id(self):
  43. """Testet die automatische ID-Generierung."""
  44. sat = Satellite(room_id="test")
  45. assert sat.id is not None
  46. assert len(sat.id) > 0
  47. def test_satellite_initial_state(self, satellite):
  48. """Testet den initialen Zustand."""
  49. assert satellite.state == ConnectionState.DISCONNECTED
  50. assert satellite.is_connected is False
  51. def test_satellite_is_connected(self, satellite):
  52. """Testet is_connected für verschiedene Zustände."""
  53. satellite.state = ConnectionState.CONNECTED
  54. assert satellite.is_connected is True
  55. satellite.state = ConnectionState.AUTHENTICATED
  56. assert satellite.is_connected is True
  57. satellite.state = ConnectionState.DISCONNECTED
  58. assert satellite.is_connected is False
  59. def test_set_connected(self, satellite):
  60. """Testet set_connected."""
  61. satellite.set_connected()
  62. assert satellite.state == ConnectionState.CONNECTED
  63. assert satellite.connected_at is not None
  64. assert satellite.last_heartbeat is not None
  65. def test_update_heartbeat(self, satellite):
  66. """Testet update_heartbeat."""
  67. satellite.update_heartbeat()
  68. assert satellite.last_heartbeat is not None
  69. def test_conversation_id(self, satellite):
  70. """Testet conversation_id."""
  71. assert satellite.conversation_id is None
  72. assert satellite.has_active_conversation is False
  73. satellite.conversation_id = "conv-123"
  74. assert satellite.conversation_id == "conv-123"
  75. assert satellite.has_active_conversation is True
  76. def test_version(self, satellite):
  77. """Testet version."""
  78. satellite.version = "1.0.0"
  79. assert satellite.version == "1.0.0"
  80. def test_metadata(self, satellite):
  81. """Testet metadata."""
  82. satellite.metadata["custom"] = "value"
  83. assert satellite.metadata["custom"] == "value"
  84. def test_to_dict(self, satellite):
  85. """Testet to_dict."""
  86. satellite.set_connected()
  87. data = satellite.to_dict()
  88. assert data["id"] == "sat-test-123"
  89. assert data["room_id"] == "wohnzimmer"
  90. assert data["mac_address"] == "AA:BB:CC:DD:EE:FF"
  91. assert data["state"] == "CONNECTED"
  92. @pytest.mark.asyncio
  93. async def test_disconnect(self, satellite):
  94. """Testet disconnect."""
  95. satellite.set_connected()
  96. satellite.conversation_id = "conv-123"
  97. await satellite.disconnect("Test reason")
  98. assert satellite.state == ConnectionState.DISCONNECTED
  99. assert satellite.conversation_id is None
  100. def test_repr(self, satellite):
  101. """Testet __repr__."""
  102. repr_str = repr(satellite)
  103. assert "Satellite" in repr_str
  104. assert "sat-test-123" in repr_str
  105. class TestSatelliteSocket:
  106. """Tests für SatelliteSocket."""
  107. def test_socket_initialization(self):
  108. """Testet die Socket-Initialisierung."""
  109. sockets = SatelliteSocket()
  110. assert sockets.command is None
  111. assert sockets.audio_in is None
  112. assert sockets.audio_out is None
  113. assert sockets.music_out is None
  114. class TestSatelliteManager:
  115. """Tests für SatelliteManager."""
  116. @pytest.fixture
  117. def mock_application(self):
  118. """Erstellt eine Mock-Anwendung."""
  119. return MagicMock()
  120. @pytest.fixture
  121. def manager(self, mock_application):
  122. """Erstellt einen SatelliteManager."""
  123. return SatelliteManager(mock_application)
  124. @pytest.fixture
  125. def satellite(self):
  126. """Erstellt einen Test-Satellite."""
  127. return Satellite(
  128. satellite_id="sat-1",
  129. room_id="wohnzimmer",
  130. mac_address="AA:BB:CC:DD:EE:FF",
  131. alias="Echo 1"
  132. )
  133. def test_manager_initialization(self, manager):
  134. """Testet die Manager-Initialisierung."""
  135. assert manager.count == 0
  136. assert manager.connected_count == 0
  137. def test_add_satellite(self, manager, satellite):
  138. """Testet das Hinzufügen eines Satellites."""
  139. manager.add(satellite)
  140. assert manager.count == 1
  141. assert satellite.id in manager._satellites
  142. def test_remove_satellite(self, manager, satellite):
  143. """Testet das Entfernen eines Satellites."""
  144. manager.add(satellite)
  145. removed = manager.remove(satellite.id)
  146. assert removed == satellite
  147. assert manager.count == 0
  148. def test_remove_nonexistent(self, manager):
  149. """Testet das Entfernen eines nicht existierenden Satellites."""
  150. assert manager.remove("nonexistent") is None
  151. def test_get_satellite(self, manager, satellite):
  152. """Testet get."""
  153. manager.add(satellite)
  154. result = manager.get(satellite.id)
  155. assert result == satellite
  156. def test_get_by_mac(self, manager, satellite):
  157. """Testet get_by_mac."""
  158. manager.add(satellite)
  159. result = manager.get_by_mac("AA:BB:CC:DD:EE:FF")
  160. assert result == satellite
  161. def test_get_by_room(self, manager, satellite):
  162. """Testet get_by_room."""
  163. manager.add(satellite)
  164. result = manager.get_by_room("wohnzimmer")
  165. assert len(result) == 1
  166. assert result[0] == satellite
  167. def test_get_connected(self, manager, satellite):
  168. """Testet get_connected."""
  169. satellite.set_connected()
  170. manager.add(satellite)
  171. connected = manager.get_connected()
  172. assert len(connected) == 1
  173. assert manager.connected_count == 1
  174. def test_get_disconnected(self, manager, satellite):
  175. """Testet get_disconnected."""
  176. manager.add(satellite)
  177. disconnected = manager.get_disconnected()
  178. assert len(disconnected) == 1
  179. def test_getitem_by_index(self, manager, satellite):
  180. """Testet __getitem__ mit Index."""
  181. manager.add(satellite)
  182. result = manager[0]
  183. assert result == satellite
  184. def test_getitem_by_id(self, manager, satellite):
  185. """Testet __getitem__ mit ID."""
  186. manager.add(satellite)
  187. result = manager[satellite.id]
  188. assert result == satellite
  189. def test_getitem_by_room_selector(self, manager, satellite):
  190. """Testet __getitem__ mit Room-Selektor."""
  191. manager.add(satellite)
  192. result = manager["room:wohnzimmer"]
  193. assert len(result) == 1
  194. assert result[0] == satellite
  195. def test_getitem_by_mac_selector(self, manager, satellite):
  196. """Testet __getitem__ mit MAC-Selektor."""
  197. manager.add(satellite)
  198. result = manager["mac:AA:BB:CC:DD:EE:FF"]
  199. assert result == satellite
  200. def test_getitem_raises_keyerror(self, manager):
  201. """Testet, dass __getitem__ KeyError wirft."""
  202. with pytest.raises(KeyError):
  203. _ = manager["nonexistent"]
  204. def test_getitem_raises_indexerror(self, manager):
  205. """Testet, dass __getitem__ IndexError wirft."""
  206. with pytest.raises(IndexError):
  207. _ = manager[0]
  208. def test_contains(self, manager, satellite):
  209. """Testet __contains__."""
  210. manager.add(satellite)
  211. assert satellite.id in manager
  212. assert "AA:BB:CC:DD:EE:FF" in manager
  213. assert "nonexistent" not in manager
  214. def test_iter(self, manager, satellite):
  215. """Testet __iter__."""
  216. manager.add(satellite)
  217. satellites = list(manager)
  218. assert len(satellites) == 1
  219. assert satellites[0] == satellite
  220. def test_len(self, manager, satellite):
  221. """Testet __len__."""
  222. assert len(manager) == 0
  223. manager.add(satellite)
  224. assert len(manager) == 1
  225. @pytest.mark.asyncio
  226. async def test_disconnect_satellite(self, manager, satellite):
  227. """Testet disconnect."""
  228. satellite.set_connected()
  229. manager.add(satellite)
  230. result = await manager.disconnect(satellite.id, "Test")
  231. assert result is True
  232. assert satellite.state == ConnectionState.DISCONNECTED
  233. @pytest.mark.asyncio
  234. async def test_disconnect_all(self, manager, satellite):
  235. """Testet disconnect_all."""
  236. satellite.set_connected()
  237. manager.add(satellite)
  238. count = await manager.disconnect_all()
  239. assert count == 1
  240. def test_find(self, manager):
  241. """Testet find."""
  242. sat1 = Satellite(room_id="wohnzimmer", alias="Echo 1")
  243. sat2 = Satellite(room_id="küche", alias="Echo 2")
  244. sat1.set_connected()
  245. manager.add(sat1)
  246. manager.add(sat2)
  247. # Nach Raum suchen
  248. result = manager.find(room="wohnzimmer")
  249. assert len(result) == 1
  250. # Nach Alias suchen
  251. result = manager.find(alias="Echo")
  252. assert len(result) == 2
  253. # Nach Verbindungsstatus suchen
  254. result = manager.find(connected=True)
  255. assert len(result) == 1
  256. class TestPendingRegistration:
  257. """Tests für PendingRegistration."""
  258. def test_pending_creation(self):
  259. """Testet die Erstellung einer ausstehenden Registrierung."""
  260. pending = PendingRegistration(
  261. mac_address="AA:BB:CC:DD:EE:FF",
  262. room="wohnzimmer",
  263. alias="Echo 1"
  264. )
  265. assert pending.mac_address == "AA:BB:CC:DD:EE:FF"
  266. assert pending.is_expired() is False
  267. class TestRegistrationManager:
  268. """Tests für RegistrationManager."""
  269. @pytest.fixture
  270. def mock_application(self):
  271. """Erstellt eine Mock-Anwendung."""
  272. return MagicMock()
  273. @pytest.fixture
  274. def manager(self, mock_application, temp_dir):
  275. """Erstellt einen RegistrationManager."""
  276. return RegistrationManager(mock_application, directory=temp_dir)
  277. def test_manager_initialization(self, manager):
  278. """Testet die Manager-Initialisierung."""
  279. assert manager.registration_mode is False
  280. assert manager.pending_count == 0
  281. def test_enable_registration_mode(self, manager):
  282. """Testet das Aktivieren des Registrierungsmodus."""
  283. manager.enable_registration_mode(timeout_seconds=60)
  284. assert manager.registration_mode is True
  285. def test_disable_registration_mode(self, manager):
  286. """Testet das Deaktivieren des Registrierungsmodus."""
  287. manager.enable_registration_mode()
  288. manager.disable_registration_mode()
  289. assert manager.registration_mode is False
  290. def test_begin_registration_requires_mode(self, manager):
  291. """Testet, dass Registrierung Modus erfordert."""
  292. result = manager.begin_registration(
  293. mac_address="AA:BB:CC:DD:EE:FF",
  294. room="test",
  295. alias="Test"
  296. )
  297. assert result is False
  298. def test_begin_registration(self, manager):
  299. """Testet das Starten einer Registrierung."""
  300. manager.enable_registration_mode()
  301. result = manager.begin_registration(
  302. mac_address="AA:BB:CC:DD:EE:FF",
  303. room="wohnzimmer",
  304. alias="Echo 1"
  305. )
  306. assert result is True
  307. assert manager.pending_count == 1
  308. def test_complete_registration(self, manager):
  309. """Testet das Abschließen einer Registrierung."""
  310. manager.enable_registration_mode()
  311. manager.begin_registration(
  312. mac_address="AA:BB:CC:DD:EE:FF",
  313. room="wohnzimmer",
  314. alias="Echo 1"
  315. )
  316. satellite = manager.complete_registration("AA:BB:CC:DD:EE:FF")
  317. assert satellite is not None
  318. assert satellite.mac_address == "AA:BB:CC:DD:EE:FF"
  319. assert satellite.room_id == "wohnzimmer"
  320. assert manager.pending_count == 0
  321. def test_complete_nonexistent_registration(self, manager):
  322. """Testet Abschluss einer nicht existierenden Registrierung."""
  323. satellite = manager.complete_registration("XX:XX:XX:XX:XX:XX")
  324. assert satellite is None
  325. def test_cancel_registration(self, manager):
  326. """Testet das Abbrechen einer Registrierung."""
  327. manager.enable_registration_mode()
  328. manager.begin_registration(
  329. mac_address="AA:BB:CC:DD:EE:FF",
  330. room="test",
  331. alias="Test"
  332. )
  333. result = manager.cancel_registration("AA:BB:CC:DD:EE:FF")
  334. assert result is True
  335. assert manager.pending_count == 0
  336. def test_is_registered(self, manager):
  337. """Testet is_registered."""
  338. manager.enable_registration_mode()
  339. manager.begin_registration(
  340. mac_address="AA:BB:CC:DD:EE:FF",
  341. room="test",
  342. alias="Test"
  343. )
  344. manager.complete_registration("AA:BB:CC:DD:EE:FF")
  345. assert manager.is_registered("AA:BB:CC:DD:EE:FF") is True
  346. assert manager.is_registered("XX:XX:XX:XX:XX:XX") is False
  347. def test_load_registered(self, manager):
  348. """Testet load_registered."""
  349. # Registriere und speichere
  350. manager.enable_registration_mode()
  351. manager.begin_registration(
  352. mac_address="AA:BB:CC:DD:EE:FF",
  353. room="wohnzimmer",
  354. alias="Echo 1"
  355. )
  356. original = manager.complete_registration("AA:BB:CC:DD:EE:FF")
  357. # Lade erneut
  358. loaded = manager.load_registered("AA:BB:CC:DD:EE:FF")
  359. assert loaded is not None
  360. assert loaded.mac_address == original.mac_address
  361. assert loaded.room_id == original.room_id
  362. def test_load_all(self, manager):
  363. """Testet load_all."""
  364. # Registriere zwei Satellites
  365. manager.enable_registration_mode()
  366. manager.begin_registration("AA:AA:AA:AA:AA:AA", "raum1", "Echo 1")
  367. manager.complete_registration("AA:AA:AA:AA:AA:AA")
  368. manager.begin_registration("BB:BB:BB:BB:BB:BB", "raum2", "Echo 2")
  369. manager.complete_registration("BB:BB:BB:BB:BB:BB")
  370. satellites = manager.load_all()
  371. assert len(satellites) == 2
  372. def test_delete_registration(self, manager):
  373. """Testet delete_registration."""
  374. manager.enable_registration_mode()
  375. manager.begin_registration(
  376. mac_address="AA:BB:CC:DD:EE:FF",
  377. room="test",
  378. alias="Test"
  379. )
  380. manager.complete_registration("AA:BB:CC:DD:EE:FF")
  381. result = manager.delete_registration("AA:BB:CC:DD:EE:FF")
  382. assert result is True
  383. assert manager.is_registered("AA:BB:CC:DD:EE:FF") is False