test_buttons.py 6.4 KB

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