"""Unit tests for ForceRefreshButton, OverrideNextButton, and OverrideEmptyButton. Uses object.__new__ to bypass HA entity setup — valid because async_press only calls coordinator methods, not HA lifecycle hooks. """ from datetime import date, time, datetime, timezone from unittest.mock import AsyncMock, MagicMock, patch import pytest from custom_components.room_cal_ha.button import ( ForceRefreshButton, OverrideEmptyButton, OverrideNextButton, ) from custom_components.room_cal_ha.const import STORE_NEXT from custom_components.room_cal_ha.models import OverrideData, ReservationData def _button(cls): btn = object.__new__(cls) btn._coordinator = MagicMock() btn._coordinator.async_refresh = AsyncMock() btn._coordinator.async_promote_next_to_active = AsyncMock() btn._coordinator.async_clear_active = AsyncMock() return btn async def test_force_refresh_calls_coordinator_refresh(): btn = _button(ForceRefreshButton) await btn.async_press() btn._coordinator.async_refresh.assert_awaited_once() async def test_force_refresh_does_not_call_promote(): btn = _button(ForceRefreshButton) await btn.async_press() btn._coordinator.async_promote_next_to_active.assert_not_called() # --------------------------------------------------------------------------- # OverrideNextButton # --------------------------------------------------------------------------- _NEXT = ReservationData( guest_name="Bob", check_in_date=date(2026, 7, 10), check_in_time=time(15, 0), check_out_date=date(2026, 7, 14), check_out_time=time(11, 0), ) _FROZEN_NOW = datetime(2026, 6, 22, 14, 0, 0, tzinfo=timezone.utc) def _override_next_button(next_data=_NEXT): btn = object.__new__(OverrideNextButton) btn.coordinator = MagicMock() btn.coordinator.store = MagicMock() btn.coordinator.store.get.return_value = next_data btn.coordinator.async_start_override = AsyncMock() return btn async def test_override_next_calls_start_override(): btn = _override_next_button() with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW): await btn.async_press() btn.coordinator.async_start_override.assert_awaited_once() async def test_override_next_passes_override_data(): btn = _override_next_button() with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW): await btn.async_press() arg = btn.coordinator.async_start_override.call_args[0][0] assert isinstance(arg, OverrideData) async def test_override_next_sets_check_in_to_one_hour_ago(): btn = _override_next_button() with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW): await btn.async_press() override: OverrideData = btn.coordinator.async_start_override.call_args[0][0] # now() naive = 2026-06-22 14:00 UTC; minus 1h = 13:00 assert override.active.check_in_date == date(2026, 6, 22) assert override.active.check_in_time == time(13, 0) async def test_override_next_preserves_other_fields(): btn = _override_next_button() with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW): await btn.async_press() override: OverrideData = btn.coordinator.async_start_override.call_args[0][0] assert override.active.guest_name == "Bob" assert override.active.check_out_date == date(2026, 7, 14) assert override.active.check_out_time == time(11, 0) async def test_override_next_end_at_matches_its_own_checkout(): btn = _override_next_button() with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW): await btn.async_press() kwargs = btn.coordinator.async_start_override.call_args[1] assert kwargs["end_at"] == datetime(2026, 7, 14, 11, 0, 0) async def test_override_next_no_end_at_when_no_checkout(): btn = _override_next_button(next_data=ReservationData(guest_name="Bob", check_in_date=date(2026, 7, 10), check_in_time=time(15, 0))) with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW): await btn.async_press() kwargs = btn.coordinator.async_start_override.call_args[1] assert kwargs["end_at"] is None async def test_override_next_reads_from_store(): btn = _override_next_button() with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW): await btn.async_press() btn.coordinator.store.get.assert_called_with(STORE_NEXT) async def test_override_next_no_next_data_does_nothing(): btn = _override_next_button(next_data=None) with patch("custom_components.room_cal_ha.button.dt_util.now", return_value=_FROZEN_NOW): await btn.async_press() btn.coordinator.async_start_override.assert_not_awaited() # --------------------------------------------------------------------------- # OverrideEmptyButton # --------------------------------------------------------------------------- _OUTGOING = ReservationData( guest_name="Alice", check_in_date=date(2026, 7, 1), check_in_time=time(14, 0), check_out_date=date(2026, 7, 5), check_out_time=time(12, 0), ) def _override_empty_button(active=_OUTGOING): btn = object.__new__(OverrideEmptyButton) btn.coordinator = MagicMock() btn.coordinator.data = MagicMock(active=active) if active is not None else None btn.coordinator.async_start_override = AsyncMock() return btn async def test_override_empty_calls_start_override(): btn = _override_empty_button() await btn.async_press() btn.coordinator.async_start_override.assert_awaited_once() async def test_override_empty_passes_empty_reservation_data(): btn = _override_empty_button() await btn.async_press() override: OverrideData = btn.coordinator.async_start_override.call_args[0][0] assert isinstance(override, OverrideData) assert override.active == ReservationData() async def test_override_empty_end_at_matches_outgoing_checkout(): btn = _override_empty_button() await btn.async_press() kwargs = btn.coordinator.async_start_override.call_args[1] assert kwargs["end_at"] == datetime(2026, 7, 5, 12, 0, 0) async def test_override_empty_no_end_at_when_outgoing_has_no_checkout(): btn = _override_empty_button(active=ReservationData(guest_name="Alice")) await btn.async_press() kwargs = btn.coordinator.async_start_override.call_args[1] assert kwargs["end_at"] is None async def test_override_empty_no_end_at_when_no_active_data(): btn = _override_empty_button(active=None) btn.coordinator.data = None await btn.async_press() kwargs = btn.coordinator.async_start_override.call_args[1] assert kwargs["end_at"] is None