test_coordinator.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. from datetime import date, time
  2. from unittest.mock import AsyncMock, patch
  3. import pytest
  4. from homeassistant.core import HomeAssistant
  5. from pytest_homeassistant_custom_component.common import MockConfigEntry
  6. from custom_components.room_cal_ha.backends import BackendAuthError, BackendConnectError
  7. from custom_components.room_cal_ha.const import (
  8. CONF_API_BASE_URL,
  9. CONF_API_TOKEN,
  10. CONF_CALDAV_URL,
  11. CONF_CALENDAR_NAME,
  12. CONF_SYNC_MODE,
  13. DOMAIN,
  14. STORE_CURRENT,
  15. STORE_NEXT,
  16. SYNC_MODE_API,
  17. SYNC_MODE_CALDAV,
  18. SYNC_MODE_MANUAL,
  19. )
  20. from custom_components.room_cal_ha.coordinator import RoomCalCoordinator
  21. from custom_components.room_cal_ha.models import ApiStatusData, ReservationData
  22. SAMPLE = ReservationData(
  23. guest_name="Alice",
  24. check_in_date=date(2026, 7, 1),
  25. check_in_time=time(14, 0),
  26. check_out_date=date(2026, 7, 5),
  27. check_out_time=time(12, 0),
  28. wifi_key="stone-angel-market",
  29. door_code="0472",
  30. )
  31. _BASE_DATA = {
  32. "room_state_name": "Room State",
  33. "current_name": "Current",
  34. "next_name": "Next",
  35. }
  36. def _make_entry(hass, options: dict) -> MockConfigEntry:
  37. entry = MockConfigEntry(domain=DOMAIN, data=_BASE_DATA, options=options)
  38. entry.add_to_hass(hass)
  39. return entry
  40. @pytest.fixture
  41. async def coordinator(hass: HomeAssistant, mock_entry):
  42. mock_entry.add_to_hass(hass)
  43. coord = RoomCalCoordinator(hass, mock_entry)
  44. await coord.async_setup()
  45. return coord
  46. # ---------------------------------------------------------------------------
  47. # Manual mode
  48. # ---------------------------------------------------------------------------
  49. async def test_manual_mode_serves_empty(coordinator: RoomCalCoordinator):
  50. await coordinator.async_refresh()
  51. assert coordinator.data.current == ReservationData()
  52. assert coordinator.data.next == ReservationData()
  53. assert coordinator.data.current_last_refresh is None
  54. assert coordinator.data.next_last_refresh is None
  55. assert coordinator.data.api_status is None
  56. # ---------------------------------------------------------------------------
  57. # CalDAV mode
  58. # ---------------------------------------------------------------------------
  59. async def test_caldav_fetch_writes_store_and_timestamps(hass: HomeAssistant):
  60. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"})
  61. coord = RoomCalCoordinator(hass, entry)
  62. await coord.async_setup()
  63. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend:
  64. MockBackend.return_value.async_fetch = AsyncMock(return_value=(SAMPLE, None))
  65. await coord.async_refresh()
  66. assert coord.data.current.guest_name == "Alice"
  67. assert coord.data.current.door_code == "0472"
  68. assert coord.data.next == ReservationData()
  69. assert coord.data.current_last_refresh is not None
  70. assert coord.data.next_last_refresh is not None
  71. async def test_caldav_empty_result_clears_store(hass: HomeAssistant):
  72. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"})
  73. coord = RoomCalCoordinator(hass, entry)
  74. await coord.async_setup()
  75. await coord.store.async_set(STORE_CURRENT, SAMPLE)
  76. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend:
  77. MockBackend.return_value.async_fetch = AsyncMock(return_value=(None, None))
  78. await coord.async_refresh()
  79. assert coord.data.current == ReservationData()
  80. assert coord.store.get(STORE_CURRENT) is None
  81. async def test_caldav_connect_error_preserves_store(hass: HomeAssistant):
  82. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"})
  83. coord = RoomCalCoordinator(hass, entry)
  84. await coord.async_setup()
  85. await coord.store.async_set(STORE_CURRENT, SAMPLE)
  86. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend:
  87. MockBackend.return_value.async_fetch = AsyncMock(side_effect=BackendConnectError("timeout"))
  88. await coord.async_refresh()
  89. assert coord.data.current.guest_name == "Alice"
  90. assert coord.data.current_last_refresh is None
  91. async def test_caldav_auth_error_triggers_reauth(hass: HomeAssistant):
  92. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"})
  93. coord = RoomCalCoordinator(hass, entry)
  94. await coord.async_setup()
  95. with (
  96. patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend,
  97. patch.object(entry, "async_start_reauth") as mock_reauth,
  98. ):
  99. MockBackend.return_value.async_fetch = AsyncMock(side_effect=BackendAuthError)
  100. await coord.async_refresh()
  101. mock_reauth.assert_called_once_with(hass)
  102. # ---------------------------------------------------------------------------
  103. # API mode
  104. # ---------------------------------------------------------------------------
  105. SAMPLE_STATUS = ApiStatusData(
  106. status="ok",
  107. checks={"caldav_connection": {"ok": True}, "calendar": {"ok": True}},
  108. )
  109. async def test_api_fetch_writes_store_and_status(hass: HomeAssistant):
  110. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_API, CONF_API_BASE_URL: "https://api.example.com"})
  111. coord = RoomCalCoordinator(hass, entry)
  112. await coord.async_setup()
  113. with patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend:
  114. instance = MockBackend.return_value
  115. instance.async_fetch = AsyncMock(return_value=(SAMPLE, None))
  116. instance.async_fetch_status = AsyncMock(return_value=SAMPLE_STATUS)
  117. instance.async_fetch_services = AsyncMock(return_value={})
  118. await coord.async_refresh()
  119. assert coord.data.current.guest_name == "Alice"
  120. assert coord.data.api_status.status == "ok"
  121. assert coord.data.current_last_refresh is not None
  122. async def test_api_connect_error_preserves_store_and_still_fetches_status(hass: HomeAssistant):
  123. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_API, CONF_API_BASE_URL: "https://api.example.com"})
  124. coord = RoomCalCoordinator(hass, entry)
  125. await coord.async_setup()
  126. await coord.store.async_set(STORE_CURRENT, SAMPLE)
  127. degraded = ApiStatusData(status="error")
  128. with patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend:
  129. instance = MockBackend.return_value
  130. instance.async_fetch = AsyncMock(side_effect=BackendConnectError("timeout"))
  131. instance.async_fetch_status = AsyncMock(return_value=degraded)
  132. instance.async_fetch_services = AsyncMock(return_value={})
  133. await coord.async_refresh()
  134. assert coord.data.current.guest_name == "Alice"
  135. assert coord.data.current_last_refresh is None
  136. assert coord.data.api_status.status == "error"
  137. async def test_api_auth_error_triggers_reauth(hass: HomeAssistant):
  138. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_API, CONF_API_BASE_URL: "https://api.example.com"})
  139. coord = RoomCalCoordinator(hass, entry)
  140. await coord.async_setup()
  141. with (
  142. patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend,
  143. patch.object(entry, "async_start_reauth") as mock_reauth,
  144. ):
  145. instance = MockBackend.return_value
  146. instance.async_fetch = AsyncMock(side_effect=BackendAuthError)
  147. instance.async_fetch_status = AsyncMock(return_value=ApiStatusData(status="auth_error"))
  148. instance.async_fetch_services = AsyncMock(return_value={})
  149. await coord.async_refresh()
  150. mock_reauth.assert_called_once_with(hass)
  151. # ---------------------------------------------------------------------------
  152. # Thin updates
  153. # ---------------------------------------------------------------------------
  154. async def test_thin_update_overlays_fields(coordinator: RoomCalCoordinator):
  155. await coordinator.store.async_set(STORE_CURRENT, SAMPLE)
  156. await coordinator.async_refresh()
  157. await coordinator.async_apply_update(STORE_CURRENT, {"wifi_ssid": "GuestNet", "door_code": "9999"})
  158. assert coordinator.data.current.wifi_ssid == "GuestNet"
  159. assert coordinator.data.current.door_code == "9999"
  160. assert coordinator.data.current.guest_name == "Alice"
  161. assert coordinator.data.current.wifi_key == "stone-angel-market"
  162. async def test_thin_update_updates_last_refresh(coordinator: RoomCalCoordinator):
  163. await coordinator.store.async_set(STORE_CURRENT, SAMPLE)
  164. await coordinator.async_refresh()
  165. assert coordinator.data.current_last_refresh is None
  166. await coordinator.async_apply_update(STORE_CURRENT, {"door_code": "1234"})
  167. assert coordinator.data.current_last_refresh is not None
  168. # ---------------------------------------------------------------------------
  169. # Store persistence
  170. # ---------------------------------------------------------------------------
  171. async def test_store_persists_across_reload(hass: HomeAssistant, mock_entry):
  172. mock_entry.add_to_hass(hass)
  173. coord = RoomCalCoordinator(hass, mock_entry)
  174. await coord.async_setup()
  175. await coord.store.async_set(STORE_CURRENT, SAMPLE)
  176. coord2 = RoomCalCoordinator(hass, mock_entry)
  177. await coord2.async_setup()
  178. loaded = coord2.store.get(STORE_CURRENT)
  179. assert loaded is not None
  180. assert loaded.guest_name == "Alice"
  181. assert loaded.door_code == "0472"