from datetime import date, datetime, time from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.room_cal_ha.backends import BackendAuthError, BackendConnectError from custom_components.room_cal_ha.const import ( CONF_API_BASE_URL, CONF_CALDAV_URL, CONF_CALENDAR_NAME, CONF_RESERVATION_DEFAULTS, CONF_SYNC_MODE, DOMAIN, STORE_CURRENT, STORE_NEXT, SYNC_MODE_API, SYNC_MODE_CALDAV, SYNC_MODE_MANUAL, ) from custom_components.room_cal_ha.coordinator import RoomCalCoordinator from custom_components.room_cal_ha.models import OverrideData, ReservationData SAMPLE = 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), wifi_key="stone-angel-market", door_code="0472", ) _BASE_DATA = { "room_state_name": "Room State", "current_name": "Current", "next_name": "Next", } def _make_entry(hass, options: dict) -> MockConfigEntry: entry = MockConfigEntry(domain=DOMAIN, data=_BASE_DATA, options=options) entry.add_to_hass(hass) return entry @pytest.fixture async def coordinator(hass: HomeAssistant, mock_entry): mock_entry.add_to_hass(hass) coord = RoomCalCoordinator(hass, mock_entry) await coord.async_setup() return coord # --------------------------------------------------------------------------- # Manual mode # --------------------------------------------------------------------------- async def test_manual_mode_serves_empty(coordinator: RoomCalCoordinator): await coordinator.async_refresh() assert coordinator.data.current == ReservationData() assert coordinator.data.next == ReservationData() assert coordinator.data.current_last_refresh is None assert coordinator.data.next_last_refresh is None # --------------------------------------------------------------------------- # CalDAV mode # --------------------------------------------------------------------------- async def test_caldav_fetch_writes_store_and_timestamps(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"}) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend: MockBackend.return_value.async_fetch = AsyncMock(return_value=(SAMPLE, None)) await coord.async_refresh() assert coord.data.current.guest_name == "Alice" assert coord.data.current.door_code == "0472" assert coord.data.next == ReservationData() assert coord.data.current_last_refresh is not None assert coord.data.next_last_refresh is not None async def test_caldav_empty_result_clears_store(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"}) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() await coord.store.async_set(STORE_CURRENT, SAMPLE) with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend: MockBackend.return_value.async_fetch = AsyncMock(return_value=(None, None)) await coord.async_refresh() assert coord.data.current == ReservationData() assert coord.store.get(STORE_CURRENT) is None async def test_caldav_connect_error_preserves_store(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"}) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() await coord.store.async_set(STORE_CURRENT, SAMPLE) with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend: MockBackend.return_value.async_fetch = AsyncMock(side_effect=BackendConnectError("timeout")) await coord.async_refresh() assert coord.data.current.guest_name == "Alice" assert coord.data.current_last_refresh is None async def test_caldav_auth_error_triggers_reauth(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"}) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() with ( patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend, patch.object(entry, "async_start_reauth") as mock_reauth, ): MockBackend.return_value.async_fetch = AsyncMock(side_effect=BackendAuthError) await coord.async_refresh() mock_reauth.assert_called_once_with(hass) # --------------------------------------------------------------------------- # API mode # --------------------------------------------------------------------------- async def test_api_fetch_writes_store(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_API, CONF_API_BASE_URL: "https://api.example.com"}) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() with patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend: instance = MockBackend.return_value instance.async_fetch = AsyncMock(return_value=(SAMPLE, None)) await coord.async_refresh() assert coord.data.current.guest_name == "Alice" assert coord.data.current_last_refresh is not None async def test_api_connect_error_preserves_store(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_API, CONF_API_BASE_URL: "https://api.example.com"}) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() await coord.store.async_set(STORE_CURRENT, SAMPLE) with patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend: instance = MockBackend.return_value instance.async_fetch = AsyncMock(side_effect=BackendConnectError("timeout")) await coord.async_refresh() assert coord.data.current.guest_name == "Alice" assert coord.data.current_last_refresh is None async def test_api_auth_error_triggers_reauth(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_API, CONF_API_BASE_URL: "https://api.example.com"}) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() with ( patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend, patch.object(entry, "async_start_reauth") as mock_reauth, ): instance = MockBackend.return_value instance.async_fetch = AsyncMock(side_effect=BackendAuthError) await coord.async_refresh() mock_reauth.assert_called_once_with(hass) # --------------------------------------------------------------------------- # Thin updates # --------------------------------------------------------------------------- async def test_thin_update_overlays_fields(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, SAMPLE) await coordinator.async_refresh() await coordinator.async_apply_update(STORE_CURRENT, {"wifi_ssid": "GuestNet", "door_code": "9999"}) assert coordinator.data.current.wifi_ssid == "GuestNet" assert coordinator.data.current.door_code == "9999" assert coordinator.data.current.guest_name == "Alice" assert coordinator.data.current.wifi_key == "stone-angel-market" async def test_thin_update_updates_last_refresh(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, SAMPLE) await coordinator.async_refresh() assert coordinator.data.current_last_refresh is None await coordinator.async_apply_update(STORE_CURRENT, {"door_code": "1234"}) assert coordinator.data.current_last_refresh is not None # --------------------------------------------------------------------------- # Store persistence # --------------------------------------------------------------------------- async def test_store_persists_across_reload(hass: HomeAssistant, mock_entry): mock_entry.add_to_hass(hass) coord = RoomCalCoordinator(hass, mock_entry) await coord.async_setup() await coord.store.async_set(STORE_CURRENT, SAMPLE) coord2 = RoomCalCoordinator(hass, mock_entry) await coord2.async_setup() loaded = coord2.store.get(STORE_CURRENT) assert loaded is not None assert loaded.guest_name == "Alice" assert loaded.door_code == "0472" # --------------------------------------------------------------------------- # _reservation_window_state # --------------------------------------------------------------------------- DATED = ReservationData( 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 _state_at(coordinator, res, dt_str): """Freeze dt_util.now() to a naive local datetime and call _reservation_window_state.""" frozen = MagicMock() frozen.replace.return_value = datetime.fromisoformat(dt_str) # naive, no TZ with patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=frozen): return coordinator._reservation_window_state(res) async def test_window_state_active(coordinator): assert _state_at(coordinator, DATED, "2026-07-03T10:00:00") == "active" async def test_window_state_pending(coordinator): assert _state_at(coordinator, DATED, "2026-06-30T10:00:00") == "pending" async def test_window_state_elapsed(coordinator): assert _state_at(coordinator, DATED, "2026-07-10T12:00:00") == "elapsed" async def test_window_state_at_check_in_is_active(coordinator): assert _state_at(coordinator, DATED, "2026-07-01T14:00:00") == "active" async def test_window_state_at_check_out_is_elapsed(coordinator): assert _state_at(coordinator, DATED, "2026-07-05T12:00:00") == "elapsed" async def test_window_state_no_dates_returns_none(coordinator): assert coordinator._reservation_window_state(ReservationData()) is None # --------------------------------------------------------------------------- # Check-in WiFi key behavior # --------------------------------------------------------------------------- _WIFI_NEXT = ReservationData( guest_name="Bob", 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), wifi_ssid="GuestNet", wifi_key="securepass", ) async def test_check_in_calls_wifi_key(hass: HomeAssistant, coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_NEXT, _WIFI_NEXT) mock_switch = MagicMock() mock_switch.async_turn_on = AsyncMock() coordinator._guest_mode_switch = mock_switch with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi, patch("custom_components.room_cal_ha.wifi.async_set_ssid_enabled", new_callable=AsyncMock) as mock_ssid, ): await coordinator._async_do_check_in() coordinator.cancel_mode_sync() mock_ssid.assert_awaited_once_with(hass, "GuestNet", True) mock_wifi.assert_awaited_once_with(hass, "GuestNet", "securepass") mock_switch.async_turn_on.assert_awaited_once() async def test_check_in_skips_wifi_when_ssid_none(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_NEXT, ReservationData(wifi_ssid=None, wifi_key="securepass")) coordinator._guest_mode_switch = None with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi: await coordinator._async_do_check_in() mock_wifi.assert_not_awaited() async def test_check_in_skips_wifi_when_key_none(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_NEXT, ReservationData(wifi_ssid="GuestNet", wifi_key=None)) coordinator._guest_mode_switch = None with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi: await coordinator._async_do_check_in() mock_wifi.assert_not_awaited() async def test_check_in_skips_wifi_when_use_unifi_off(hass: HomeAssistant, coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_NEXT, _WIFI_NEXT) mock_switch = MagicMock() mock_switch.async_turn_on = AsyncMock() coordinator._guest_mode_switch = mock_switch with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch.object(coordinator, "_is_use_unifi_on", return_value=False), patch("custom_components.room_cal_ha.wifi.async_set_ssid_enabled", new_callable=AsyncMock) as mock_ssid, patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi, ): await coordinator._async_do_check_in() coordinator.cancel_mode_sync() mock_ssid.assert_not_awaited() mock_wifi.assert_not_awaited() mock_switch.async_turn_on.assert_awaited_once() async def test_check_in_wifi_error_still_activates_guest_mode(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_NEXT, _WIFI_NEXT) mock_switch = MagicMock() mock_switch.async_turn_on = AsyncMock() coordinator._guest_mode_switch = mock_switch with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch("custom_components.room_cal_ha.wifi.async_set_ssid_enabled", new_callable=AsyncMock), patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", side_effect=HomeAssistantError("hub down")), ): await coordinator._async_do_check_in() coordinator.cancel_mode_sync() mock_switch.async_turn_on.assert_awaited_once() # --------------------------------------------------------------------------- # Check-out SSID disable behavior # --------------------------------------------------------------------------- async def test_check_out_disables_ssid(hass: HomeAssistant, coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, _WIFI_NEXT) mock_switch = MagicMock() mock_switch.async_turn_off = AsyncMock() coordinator._guest_mode_switch = mock_switch with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch("custom_components.room_cal_ha.wifi.async_set_ssid_enabled", new_callable=AsyncMock) as mock_ssid, ): await coordinator._async_do_check_out() mock_ssid.assert_awaited_once_with(hass, "GuestNet", False) mock_switch.async_turn_off.assert_awaited_once() async def test_check_out_skips_ssid_when_ssid_none(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, ReservationData(wifi_ssid=None)) coordinator._guest_mode_switch = None with patch("custom_components.room_cal_ha.wifi.async_set_ssid_enabled", new_callable=AsyncMock) as mock_ssid: await coordinator._async_do_check_out() mock_ssid.assert_not_awaited() async def test_check_out_skips_ssid_when_use_unifi_off(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, _WIFI_NEXT) coordinator._guest_mode_switch = None with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch.object(coordinator, "_is_use_unifi_on", return_value=False), patch("custom_components.room_cal_ha.wifi.async_set_ssid_enabled", new_callable=AsyncMock) as mock_ssid, ): await coordinator._async_do_check_out() mock_ssid.assert_not_awaited() async def test_check_out_ssid_error_still_deactivates_guest_mode(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, _WIFI_NEXT) mock_switch = MagicMock() mock_switch.async_turn_off = AsyncMock() coordinator._guest_mode_switch = mock_switch with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch("custom_components.room_cal_ha.wifi.async_set_ssid_enabled", side_effect=HomeAssistantError("hub down")), ): await coordinator._async_do_check_out() mock_switch.async_turn_off.assert_awaited_once() # --------------------------------------------------------------------------- # async_start_override / async_end_override # --------------------------------------------------------------------------- _OVERRIDE_RES = ReservationData(guest_name="Override Guest", door_code="1111") _WIFI_OVERRIDE = ReservationData(guest_name="WiFi Guest", wifi_ssid="GuestNet", wifi_key="secret") async def test_start_override_sets_current_in_data(coordinator: RoomCalCoordinator): coordinator._guest_mode_switch = None await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES)) assert coordinator.data.current.guest_name == "Override Guest" async def test_start_override_sets_override_active_flag(coordinator: RoomCalCoordinator): coordinator._guest_mode_switch = None await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES)) assert coordinator.data.override_active is True async def test_start_override_cancels_mode_sync(coordinator: RoomCalCoordinator): cancel1, cancel2 = MagicMock(), MagicMock() coordinator._mode_sync_handles = [cancel1, cancel2] coordinator._guest_mode_switch = None await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES)) cancel1.assert_called_once() cancel2.assert_called_once() assert coordinator._mode_sync_handles == [] async def test_start_override_turns_on_guest_mode(coordinator: RoomCalCoordinator): mock_switch = MagicMock() mock_switch.async_turn_on = AsyncMock() coordinator._guest_mode_switch = mock_switch with patch.object(coordinator, "_is_mode_sync_on", return_value=True): await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES)) mock_switch.async_turn_on.assert_awaited_once() async def test_start_override_does_not_turn_on_guest_mode_when_mode_sync_off(coordinator: RoomCalCoordinator): mock_switch = MagicMock() mock_switch.async_turn_on = AsyncMock() coordinator._guest_mode_switch = mock_switch with patch.object(coordinator, "_is_mode_sync_on", return_value=False): await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES)) mock_switch.async_turn_on.assert_not_awaited() async def test_start_override_calls_wifi_when_credentials_present(hass: HomeAssistant, coordinator: RoomCalCoordinator): coordinator._guest_mode_switch = MagicMock() coordinator._guest_mode_switch.async_turn_on = AsyncMock() with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi, ): await coordinator.async_start_override(OverrideData(current=_WIFI_OVERRIDE)) mock_wifi.assert_awaited_once_with(hass, "GuestNet", "secret") async def test_start_override_skips_wifi_when_mode_sync_off(coordinator: RoomCalCoordinator): coordinator._guest_mode_switch = None with ( patch.object(coordinator, "_is_mode_sync_on", return_value=False), patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi, ): await coordinator.async_start_override(OverrideData(current=_WIFI_OVERRIDE)) mock_wifi.assert_not_awaited() async def test_start_override_skips_wifi_when_ssid_missing(coordinator: RoomCalCoordinator): coordinator._guest_mode_switch = None no_ssid = ReservationData(wifi_ssid=None, wifi_key="secret") with patch("custom_components.room_cal_ha.wifi.async_set_wifi_key", new_callable=AsyncMock) as mock_wifi: await coordinator.async_start_override(OverrideData(current=no_ssid)) mock_wifi.assert_not_awaited() async def test_start_override_uses_store_next_when_override_next_is_none(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_NEXT, SAMPLE) coordinator._guest_mode_switch = None await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES)) assert coordinator.data.next.guest_name == "Alice" async def test_start_override_uses_override_next_when_provided(coordinator: RoomCalCoordinator): override_next = ReservationData(guest_name="Override Next") coordinator._guest_mode_switch = None await coordinator.async_start_override(OverrideData(current=_OVERRIDE_RES, next=override_next)) assert coordinator.data.next.guest_name == "Override Next" async def test_start_override_applies_defaults_to_current(hass: HomeAssistant): entry = MockConfigEntry( domain=DOMAIN, data={}, options={ CONF_SYNC_MODE: SYNC_MODE_MANUAL, CONF_RESERVATION_DEFAULTS: {"wifi_ssid": "DefaultSSID", "check_out_time": "11:00:00"}, }, ) entry.add_to_hass(hass) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() coord._guest_mode_switch = None partial_override = ReservationData(guest_name="Override Guest") await coord.async_start_override(OverrideData(current=partial_override)) assert coord.data.current.guest_name == "Override Guest" assert coord.data.current.wifi_ssid == "DefaultSSID" assert coord.data.current.check_out_time == time(11, 0) async def test_start_override_defaults_do_not_overwrite_explicit_fields(hass: HomeAssistant): entry = MockConfigEntry( domain=DOMAIN, data={}, options={ CONF_SYNC_MODE: SYNC_MODE_MANUAL, CONF_RESERVATION_DEFAULTS: {"wifi_ssid": "DefaultSSID"}, }, ) entry.add_to_hass(hass) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() coord._guest_mode_switch = None explicit_override = ReservationData(guest_name="Override Guest", wifi_ssid="ExplicitSSID") await coord.async_start_override(OverrideData(current=explicit_override)) assert coord.data.current.wifi_ssid == "ExplicitSSID" async def test_refresh_during_override_applies_defaults_to_current(hass: HomeAssistant): entry = MockConfigEntry( domain=DOMAIN, data={}, options={ CONF_SYNC_MODE: SYNC_MODE_MANUAL, CONF_RESERVATION_DEFAULTS: {"door_code": "9999"}, }, ) entry.add_to_hass(hass) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() coord._guest_mode_switch = None partial_override = ReservationData(guest_name="Override Guest") await coord.async_start_override(OverrideData(current=partial_override)) await coord.async_refresh() assert coord.data.current.guest_name == "Override Guest" assert coord.data.current.door_code == "9999" async def test_start_override_applies_defaults_to_store_next(hass: HomeAssistant): entry = MockConfigEntry( domain=DOMAIN, data={}, options={ CONF_SYNC_MODE: SYNC_MODE_MANUAL, CONF_RESERVATION_DEFAULTS: {"wifi_ssid": "DefaultSSID"}, }, ) entry.add_to_hass(hass) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() coord._guest_mode_switch = None await coord.store.async_set(STORE_NEXT, ReservationData(guest_name="Next Guest")) await coord.async_start_override(OverrideData(current=_OVERRIDE_RES)) assert coord.data.next.guest_name == "Next Guest" assert coord.data.next.wifi_ssid == "DefaultSSID" async def test_end_override_clears_override_active(coordinator: RoomCalCoordinator): coordinator._guest_mode_switch = None coordinator._override = OverrideData(current=_OVERRIDE_RES) await coordinator.async_end_override() assert coordinator.data.override_active is False async def test_end_override_restores_store_current(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, SAMPLE) coordinator._guest_mode_switch = None coordinator._override = OverrideData(current=_OVERRIDE_RES) await coordinator.async_end_override() assert coordinator.data.current.guest_name == "Alice" # --------------------------------------------------------------------------- # Thin updates during override # --------------------------------------------------------------------------- async def test_thin_update_current_during_override_updates_override_not_store(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, SAMPLE) await coordinator.async_refresh() coordinator._override = OverrideData(current=_OVERRIDE_RES) await coordinator.async_apply_update(STORE_CURRENT, {"door_code": "9999"}) assert coordinator.data.current.door_code == "9999" # Store is unchanged assert coordinator.store.get(STORE_CURRENT).door_code == "0472" async def test_thin_update_current_during_override_data_still_shows_override_active(coordinator: RoomCalCoordinator): await coordinator.async_refresh() coordinator._override = OverrideData(current=_OVERRIDE_RES) await coordinator.async_apply_update(STORE_CURRENT, {"door_code": "9999"}) assert coordinator.data.override_active is True async def test_thin_update_next_during_override_falls_through_to_store(coordinator: RoomCalCoordinator): coordinator._override = OverrideData(current=_OVERRIDE_RES) # override.next is None await coordinator.async_refresh() await coordinator.async_apply_update(STORE_NEXT, {"guest_name": "Next Guest"}) assert coordinator.store.get(STORE_NEXT).guest_name == "Next Guest" assert coordinator.data.override_active is True assert coordinator.data.current.guest_name == "Override Guest" # --------------------------------------------------------------------------- # Backend fetch during override preserves override display # --------------------------------------------------------------------------- async def test_backend_fetch_during_override_updates_store_but_preserves_override(hass: HomeAssistant): entry = _make_entry(hass, {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"}) coord = RoomCalCoordinator(hass, entry) await coord.async_setup() coord._override = OverrideData(current=_OVERRIDE_RES) backend_data = ReservationData(guest_name="Backend Alice") with patch("custom_components.room_cal_ha.coordinator.CalDavBackend") as MockBackend: MockBackend.return_value.async_fetch = AsyncMock(return_value=(backend_data, None)) await coord.async_refresh() # Store was written by the backend assert coord.store.get(STORE_CURRENT).guest_name == "Backend Alice" # But displayed data still shows the override assert coord.data.current.guest_name == "Override Guest" assert coord.data.override_active is True # --------------------------------------------------------------------------- # async_reconcile_from_store (shared helper) # --------------------------------------------------------------------------- _ACTIVE = ReservationData( guest_name="Active Guest", check_in_date=date(2026, 6, 20), check_in_time=time(14, 0), check_out_date=date(2026, 6, 25), check_out_time=time(12, 0), ) _ELAPSED = ReservationData( guest_name="Past Guest", check_in_date=date(2026, 6, 1), check_in_time=time(14, 0), check_out_date=date(2026, 6, 5), check_out_time=time(12, 0), ) _FUTURE = ReservationData( guest_name="Future Guest", 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), ) # Freeze time at 2026-06-22 12:00 local (naive) _RECONCILE_NOW = MagicMock() _RECONCILE_NOW.replace.return_value = datetime(2026, 6, 22, 12, 0, 0) async def test_reconcile_active_current_turns_on_guest_mode(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, _ACTIVE) mock_switch = MagicMock() mock_switch.async_turn_on = AsyncMock() mock_switch.async_turn_off = AsyncMock() coordinator._guest_mode_switch = mock_switch with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_RECONCILE_NOW), ): await coordinator.async_reconcile_from_store() coordinator.cancel_mode_sync() mock_switch.async_turn_on.assert_awaited_once() mock_switch.async_turn_off.assert_not_awaited() async def test_reconcile_elapsed_current_clears_store_and_turns_off_guest_mode(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, _ELAPSED) mock_switch = MagicMock() mock_switch.async_turn_on = AsyncMock() mock_switch.async_turn_off = AsyncMock() coordinator._guest_mode_switch = mock_switch with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_RECONCILE_NOW), ): await coordinator.async_reconcile_from_store() assert coordinator.store.get(STORE_CURRENT) is None mock_switch.async_turn_off.assert_awaited_once() async def test_reconcile_promotes_next_when_check_in_elapsed(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_NEXT, _ACTIVE) with ( patch.object(coordinator, "_is_mode_sync_on", return_value=True), patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_RECONCILE_NOW), ): await coordinator.async_reconcile_from_store() coordinator.cancel_mode_sync() assert coordinator.store.get(STORE_CURRENT).guest_name == "Active Guest" async def test_reconcile_does_not_touch_guest_mode_when_mode_sync_off(coordinator: RoomCalCoordinator): await coordinator.store.async_set(STORE_CURRENT, _ACTIVE) mock_switch = MagicMock() mock_switch.async_turn_on = AsyncMock() mock_switch.async_turn_off = AsyncMock() coordinator._guest_mode_switch = mock_switch with ( patch.object(coordinator, "_is_mode_sync_on", return_value=False), patch("custom_components.room_cal_ha.coordinator.dt_util.now", return_value=_RECONCILE_NOW), ): await coordinator.async_reconcile_from_store() mock_switch.async_turn_on.assert_not_awaited() mock_switch.async_turn_off.assert_not_awaited()