test_arrived_departed.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. """Tests for Arrived/Departed binary sensors, Guest Arrived/Departed buttons,
  2. and coordinator arrived/departed flag lifecycle.
  3. Uses object.__new__ for pure-unit tests and hass fixtures for coordinator integration.
  4. """
  5. from datetime import date, time
  6. from unittest.mock import AsyncMock, MagicMock, patch
  7. import pytest
  8. from homeassistant.core import HomeAssistant
  9. from pytest_homeassistant_custom_component.common import MockConfigEntry
  10. from custom_components.room_cal_ha.binary_sensor import ArrivedSensor, DepartedSensor
  11. from custom_components.room_cal_ha.button import (
  12. GuestArrivedButton,
  13. GuestDepartedButton,
  14. GuestEnteredButton,
  15. )
  16. from custom_components.room_cal_ha.const import (
  17. CONF_SYNC_MODE,
  18. DOMAIN,
  19. STORE_CURRENT,
  20. STORE_NEXT,
  21. SYNC_MODE_CALDAV,
  22. SYNC_MODE_MANUAL,
  23. )
  24. from custom_components.room_cal_ha.coordinator import CoordinatorData, RoomCalCoordinator
  25. from custom_components.room_cal_ha.models import ReservationData
  26. _ALICE = ReservationData(
  27. guest_name="Alice",
  28. check_in_date=date(2026, 7, 1),
  29. check_in_time=time(14, 0),
  30. check_out_date=date(2026, 7, 5),
  31. check_out_time=time(12, 0),
  32. )
  33. _BOB = ReservationData(
  34. guest_name="Bob",
  35. check_in_date=date(2026, 7, 6),
  36. check_in_time=time(15, 0),
  37. check_out_date=date(2026, 7, 10),
  38. check_out_time=time(11, 0),
  39. )
  40. _BASE_DATA = {"room_state_name": "Room State"}
  41. def _make_entry(hass, options: dict) -> MockConfigEntry:
  42. entry = MockConfigEntry(domain=DOMAIN, data=_BASE_DATA, options=options)
  43. entry.add_to_hass(hass)
  44. return entry
  45. # ---------------------------------------------------------------------------
  46. # ArrivedSensor / DepartedSensor unit tests
  47. # ---------------------------------------------------------------------------
  48. def _data(arrived=False, departed=False) -> CoordinatorData:
  49. return CoordinatorData(
  50. active=ReservationData(),
  51. current=ReservationData(),
  52. next=ReservationData(),
  53. current_last_refresh=None,
  54. next_last_refresh=None,
  55. active_arrived=arrived,
  56. active_departed=departed,
  57. )
  58. def _arrived_sensor(arrived=False) -> ArrivedSensor:
  59. s = object.__new__(ArrivedSensor)
  60. s.coordinator = MagicMock()
  61. s.coordinator.data = _data(arrived=arrived)
  62. return s
  63. def _departed_sensor(departed=False) -> DepartedSensor:
  64. s = object.__new__(DepartedSensor)
  65. s.coordinator = MagicMock()
  66. s.coordinator.data = _data(departed=departed)
  67. return s
  68. def test_arrived_sensor_default_false():
  69. assert _arrived_sensor(arrived=False).is_on is False
  70. def test_arrived_sensor_true_when_set():
  71. assert _arrived_sensor(arrived=True).is_on is True
  72. def test_departed_sensor_default_false():
  73. assert _departed_sensor(departed=False).is_on is False
  74. def test_departed_sensor_true_when_set():
  75. assert _departed_sensor(departed=True).is_on is True
  76. # ---------------------------------------------------------------------------
  77. # GuestArrivedButton / GuestDepartedButton unit tests
  78. # ---------------------------------------------------------------------------
  79. def _arrived_button() -> GuestArrivedButton:
  80. btn = object.__new__(GuestArrivedButton)
  81. btn._coordinator = MagicMock()
  82. btn._coordinator.async_set_arrived = AsyncMock()
  83. return btn
  84. def _departed_button() -> GuestDepartedButton:
  85. btn = object.__new__(GuestDepartedButton)
  86. btn._coordinator = MagicMock()
  87. btn._coordinator.async_set_departed = AsyncMock()
  88. return btn
  89. async def test_arrived_button_calls_set_arrived_true():
  90. btn = _arrived_button()
  91. await btn.async_press()
  92. btn._coordinator.async_set_arrived.assert_awaited_once_with(True)
  93. async def test_departed_button_calls_set_departed_true():
  94. btn = _departed_button()
  95. await btn.async_press()
  96. btn._coordinator.async_set_departed.assert_awaited_once_with(True)
  97. # ---------------------------------------------------------------------------
  98. # GuestEnteredButton unit tests
  99. # ---------------------------------------------------------------------------
  100. def _entered_button(arrived=False) -> GuestEnteredButton:
  101. btn = object.__new__(GuestEnteredButton)
  102. btn._coordinator = MagicMock()
  103. btn._coordinator.data = _data(arrived=arrived)
  104. btn._coordinator.async_set_arrived = AsyncMock()
  105. return btn
  106. async def test_entered_button_sets_arrived_when_not_yet_arrived():
  107. btn = _entered_button(arrived=False)
  108. await btn.async_press()
  109. btn._coordinator.async_set_arrived.assert_awaited_once_with(True)
  110. async def test_entered_button_does_not_re_set_arrived_when_already_arrived():
  111. btn = _entered_button(arrived=True)
  112. await btn.async_press()
  113. btn._coordinator.async_set_arrived.assert_not_awaited()
  114. # ---------------------------------------------------------------------------
  115. # Coordinator: async_set_arrived / async_set_departed
  116. # ---------------------------------------------------------------------------
  117. @pytest.fixture
  118. async def coord(hass: HomeAssistant):
  119. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_MANUAL})
  120. c = RoomCalCoordinator(hass, entry)
  121. await c.async_setup()
  122. await c.async_refresh()
  123. return c
  124. async def test_set_arrived_sets_flag(coord: RoomCalCoordinator):
  125. await coord.async_set_arrived(True)
  126. assert coord._active_arrived is True
  127. assert coord.data.active_arrived is True
  128. async def test_set_arrived_persists_to_store(coord: RoomCalCoordinator):
  129. await coord.async_set_arrived(True)
  130. assert coord.store.get_arrived() is True
  131. async def test_set_departed_sets_flag(coord: RoomCalCoordinator):
  132. await coord.async_set_departed(True)
  133. assert coord._active_departed is True
  134. assert coord.data.active_departed is True
  135. async def test_set_departed_persists_to_store(coord: RoomCalCoordinator):
  136. await coord.async_set_departed(True)
  137. assert coord.store.get_departed() is True
  138. async def test_flags_load_from_store_on_startup(hass: HomeAssistant):
  139. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_MANUAL})
  140. c = RoomCalCoordinator(hass, entry)
  141. await c.async_setup()
  142. await c.store.async_set_arrived(True)
  143. await c.store.async_set_departed(True)
  144. c2 = RoomCalCoordinator(hass, entry)
  145. await c2.async_setup()
  146. assert c2._active_arrived is True
  147. assert c2._active_departed is True
  148. # ---------------------------------------------------------------------------
  149. # Coordinator: flag reset on reservation identity change
  150. # ---------------------------------------------------------------------------
  151. async def test_flags_reset_when_reservation_changes(hass: HomeAssistant):
  152. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, "caldav_url": "https://x", "calendar_name": "R"})
  153. c = RoomCalCoordinator(hass, entry)
  154. await c.async_setup()
  155. # Load Alice as current
  156. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
  157. M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None))
  158. await c.async_refresh()
  159. await c.async_set_arrived(True)
  160. assert c._active_arrived is True
  161. # Bob becomes current — different check-in identity
  162. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
  163. M.return_value.async_fetch = AsyncMock(return_value=(_BOB, None))
  164. await c.async_refresh()
  165. assert c._active_arrived is False
  166. assert c.data.active_arrived is False
  167. assert c.store.get_arrived() is False
  168. async def test_flags_not_reset_when_same_reservation(hass: HomeAssistant):
  169. entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, "caldav_url": "https://x", "calendar_name": "R"})
  170. c = RoomCalCoordinator(hass, entry)
  171. await c.async_setup()
  172. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
  173. M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None))
  174. await c.async_refresh()
  175. await c.async_set_arrived(True)
  176. # Same Alice again — no identity change
  177. with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as M:
  178. M.return_value.async_fetch = AsyncMock(return_value=(_ALICE, None))
  179. await c.async_refresh()
  180. assert c._active_arrived is True
  181. assert c.data.active_arrived is True
  182. async def test_flags_reset_on_promote_from_next(coord: RoomCalCoordinator):
  183. await coord.store.async_set(STORE_CURRENT, _ALICE)
  184. await coord.store.async_set(STORE_NEXT, _BOB)
  185. await coord.async_refresh()
  186. await coord.async_set_arrived(True)
  187. await coord.async_promote_next_to_active()
  188. assert coord._active_arrived is False
  189. assert coord.data.active_arrived is False
  190. async def test_flags_reset_on_clear_current(coord: RoomCalCoordinator):
  191. await coord.store.async_set(STORE_CURRENT, _ALICE)
  192. await coord.async_refresh()
  193. await coord.async_set_arrived(True)
  194. await coord.async_clear_active()
  195. assert coord._active_arrived is False
  196. assert coord.data.active_arrived is False
  197. async def test_flags_reset_on_checkout(coord: RoomCalCoordinator):
  198. await coord.store.async_set(STORE_CURRENT, _ALICE)
  199. await coord.async_refresh()
  200. await coord.async_set_arrived(True)
  201. await coord._async_do_check_out()
  202. assert coord._active_arrived is False
  203. assert coord.data.active_arrived is False