test_buttons.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. """Unit tests for ForceRefreshButton and TestNextGuestButton.
  2. Uses object.__new__ to bypass HA entity setup — valid because async_press
  3. only calls coordinator methods, not HA lifecycle hooks.
  4. """
  5. from datetime import date, time, datetime, timezone
  6. from unittest.mock import AsyncMock, MagicMock, patch
  7. import pytest
  8. from custom_components.room_cal_ha.button import (
  9. ForceRefreshButton,
  10. OverrideCurrentButton,
  11. OverrideEmptyButton,
  12. TestNextGuestButton,
  13. )
  14. from custom_components.room_cal_ha.const import STORE_NEXT
  15. from custom_components.room_cal_ha.models import OverrideData, ReservationData
  16. def _button(cls):
  17. btn = object.__new__(cls)
  18. btn._coordinator = MagicMock()
  19. btn._coordinator.async_refresh = AsyncMock()
  20. btn._coordinator.async_promote_next_to_current = AsyncMock()
  21. btn._coordinator.async_clear_current = AsyncMock()
  22. return btn
  23. async def test_force_refresh_calls_coordinator_refresh():
  24. btn = _button(ForceRefreshButton)
  25. await btn.async_press()
  26. btn._coordinator.async_refresh.assert_awaited_once()
  27. async def test_force_refresh_does_not_call_promote():
  28. btn = _button(ForceRefreshButton)
  29. await btn.async_press()
  30. btn._coordinator.async_promote_next_to_current.assert_not_called()
  31. # ---------------------------------------------------------------------------
  32. # TestNextGuestButton
  33. # ---------------------------------------------------------------------------
  34. _NEXT = ReservationData(
  35. guest_name="Bob",
  36. check_in_date=date(2026, 7, 10),
  37. check_in_time=time(15, 0),
  38. check_out_date=date(2026, 7, 14),
  39. check_out_time=time(11, 0),
  40. )
  41. _FROZEN_NOW = datetime(2026, 6, 22, 14, 0, 0, tzinfo=timezone.utc)
  42. def _test_next_button(next_data=_NEXT):
  43. btn = object.__new__(TestNextGuestButton)
  44. btn.coordinator = MagicMock()
  45. btn.coordinator.store = MagicMock()
  46. btn.coordinator.store.get.return_value = next_data
  47. btn.coordinator.async_start_override = AsyncMock()
  48. return btn
  49. async def test_test_next_guest_calls_start_override():
  50. btn = _test_next_button()
  51. with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW):
  52. await btn.async_press()
  53. btn.coordinator.async_start_override.assert_awaited_once()
  54. async def test_test_next_guest_passes_override_data():
  55. btn = _test_next_button()
  56. with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW):
  57. await btn.async_press()
  58. arg = btn.coordinator.async_start_override.call_args[0][0]
  59. assert isinstance(arg, OverrideData)
  60. async def test_test_next_guest_sets_check_in_to_one_hour_ago():
  61. btn = _test_next_button()
  62. with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW):
  63. await btn.async_press()
  64. override: OverrideData = btn.coordinator.async_start_override.call_args[0][0]
  65. # now() naive = 2026-06-22 14:00 UTC; minus 1h = 13:00
  66. assert override.current.check_in_date == date(2026, 6, 22)
  67. assert override.current.check_in_time == time(13, 0)
  68. async def test_test_next_guest_preserves_other_fields():
  69. btn = _test_next_button()
  70. with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW):
  71. await btn.async_press()
  72. override: OverrideData = btn.coordinator.async_start_override.call_args[0][0]
  73. assert override.current.guest_name == "Bob"
  74. assert override.current.check_out_date == date(2026, 7, 14)
  75. assert override.current.check_out_time == time(11, 0)
  76. async def test_test_next_guest_override_next_is_none():
  77. btn = _test_next_button()
  78. with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW):
  79. await btn.async_press()
  80. override: OverrideData = btn.coordinator.async_start_override.call_args[0][0]
  81. assert override.next is None
  82. async def test_test_next_guest_reads_from_store():
  83. btn = _test_next_button()
  84. with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW):
  85. await btn.async_press()
  86. btn.coordinator.store.get.assert_called_with(STORE_NEXT)
  87. async def test_test_next_guest_no_next_data_does_nothing():
  88. btn = _test_next_button(next_data=None)
  89. with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW):
  90. await btn.async_press()
  91. btn.coordinator.async_start_override.assert_not_awaited()
  92. # ---------------------------------------------------------------------------
  93. # OverrideCurrentButton
  94. # ---------------------------------------------------------------------------
  95. _CURRENT = ReservationData(
  96. guest_name="Carol",
  97. check_in_date=date(2026, 7, 2),
  98. check_in_time=time(15, 0),
  99. check_out_date=date(2026, 7, 6),
  100. check_out_time=time(11, 0),
  101. )
  102. def _override_current_button(current=_CURRENT):
  103. btn = object.__new__(OverrideCurrentButton)
  104. btn.coordinator = MagicMock()
  105. btn.coordinator.data = MagicMock(current=current) if current is not None else None
  106. btn.coordinator.async_start_override = AsyncMock()
  107. return btn
  108. async def test_override_current_calls_start_override():
  109. btn = _override_current_button()
  110. await btn.async_press()
  111. btn.coordinator.async_start_override.assert_awaited_once()
  112. async def test_override_current_passes_unchanged_current_data():
  113. btn = _override_current_button()
  114. await btn.async_press()
  115. override: OverrideData = btn.coordinator.async_start_override.call_args[0][0]
  116. assert isinstance(override, OverrideData)
  117. assert override.current == _CURRENT
  118. assert override.next is None
  119. async def test_override_current_no_end_at():
  120. btn = _override_current_button()
  121. await btn.async_press()
  122. kwargs = btn.coordinator.async_start_override.call_args[1]
  123. assert "end_at" not in kwargs
  124. async def test_override_current_no_data_does_nothing():
  125. btn = _override_current_button(current=None)
  126. btn.coordinator.data = None
  127. await btn.async_press()
  128. btn.coordinator.async_start_override.assert_not_awaited()
  129. # ---------------------------------------------------------------------------
  130. # OverrideEmptyButton
  131. # ---------------------------------------------------------------------------
  132. _OUTGOING = ReservationData(
  133. guest_name="Alice",
  134. check_in_date=date(2026, 7, 1),
  135. check_in_time=time(14, 0),
  136. check_out_date=date(2026, 7, 5),
  137. check_out_time=time(12, 0),
  138. )
  139. def _override_empty_button(current=_OUTGOING):
  140. btn = object.__new__(OverrideEmptyButton)
  141. btn.coordinator = MagicMock()
  142. btn.coordinator.data = MagicMock(current=current) if current is not None else None
  143. btn.coordinator.async_start_override = AsyncMock()
  144. return btn
  145. async def test_override_empty_calls_start_override():
  146. btn = _override_empty_button()
  147. await btn.async_press()
  148. btn.coordinator.async_start_override.assert_awaited_once()
  149. async def test_override_empty_passes_empty_reservation_data():
  150. btn = _override_empty_button()
  151. await btn.async_press()
  152. override: OverrideData = btn.coordinator.async_start_override.call_args[0][0]
  153. assert isinstance(override, OverrideData)
  154. assert override.current == ReservationData()
  155. assert override.next is None
  156. async def test_override_empty_end_at_matches_outgoing_checkout():
  157. btn = _override_empty_button()
  158. await btn.async_press()
  159. kwargs = btn.coordinator.async_start_override.call_args[1]
  160. assert kwargs["end_at"] == datetime(2026, 7, 5, 12, 0, 0)
  161. async def test_override_empty_no_end_at_when_outgoing_has_no_checkout():
  162. btn = _override_empty_button(current=ReservationData(guest_name="Alice"))
  163. await btn.async_press()
  164. kwargs = btn.coordinator.async_start_override.call_args[1]
  165. assert kwargs["end_at"] is None
  166. async def test_override_empty_no_end_at_when_no_current_data():
  167. btn = _override_empty_button(current=None)
  168. btn.coordinator.data = None
  169. await btn.async_press()
  170. kwargs = btn.coordinator.async_start_override.call_args[1]
  171. assert kwargs["end_at"] is None