test_buttons.py 6.9 KB

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