|
@@ -0,0 +1,158 @@
|
|
|
|
|
+from datetime import date, time, datetime, timezone
|
|
|
|
|
+from unittest.mock import AsyncMock, patch
|
|
|
|
|
+
|
|
|
|
|
+import pytest
|
|
|
|
|
+from homeassistant.core import HomeAssistant
|
|
|
|
|
+
|
|
|
|
|
+from custom_components.room_cal_ha.const import (
|
|
|
|
|
+ CONF_CALDAV_ENABLED,
|
|
|
|
|
+ CONF_CALDAV_URL,
|
|
|
|
|
+ CONF_CALENDAR_NAME,
|
|
|
|
|
+ DOMAIN,
|
|
|
|
|
+ STORE_CURRENT,
|
|
|
|
|
+ STORE_NEXT,
|
|
|
|
|
+)
|
|
|
|
|
+from custom_components.room_cal_ha.coordinator import RoomCalCoordinator
|
|
|
|
|
+from custom_components.room_cal_ha.models import ReservationData
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
|
|
|
+
|
|
|
|
|
+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",
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+CALDAV_OPTIONS = {
|
|
|
|
|
+ CONF_CALDAV_ENABLED: True,
|
|
|
|
|
+ CONF_CALDAV_URL: "https://cal.example.com",
|
|
|
|
|
+ CONF_CALENDAR_NAME: "Room",
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+_ENTRY_DATA = {
|
|
|
|
|
+ "room_state_name": "Room State",
|
|
|
|
|
+ "current_name": "Current",
|
|
|
|
|
+ "next_name": "Next",
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _caldav_entry(hass):
|
|
|
|
|
+ entry = MockConfigEntry(domain=DOMAIN, data=_ENTRY_DATA, options=CALDAV_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
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def test_caldav_disabled_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
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+async def test_caldav_fetch_writes_store_and_timestamps(hass: HomeAssistant):
|
|
|
|
|
+ entry = _caldav_entry(hass)
|
|
|
|
|
+ coord = RoomCalCoordinator(hass, entry)
|
|
|
|
|
+ await coord.async_setup()
|
|
|
|
|
+
|
|
|
|
|
+ with patch.object(coord, "_fetch_caldav", 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 = _caldav_entry(hass)
|
|
|
|
|
+ coord = RoomCalCoordinator(hass, entry)
|
|
|
|
|
+ await coord.async_setup()
|
|
|
|
|
+ await coord.store.async_set(STORE_CURRENT, SAMPLE)
|
|
|
|
|
+
|
|
|
|
|
+ with patch.object(coord, "_fetch_caldav", 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_failure_preserves_store(hass: HomeAssistant):
|
|
|
|
|
+ entry = _caldav_entry(hass)
|
|
|
|
|
+ coord = RoomCalCoordinator(hass, entry)
|
|
|
|
|
+ await coord.async_setup()
|
|
|
|
|
+ await coord.store.async_set(STORE_CURRENT, SAMPLE)
|
|
|
|
|
+
|
|
|
|
|
+ with patch.object(coord, "_fetch_caldav", side_effect=Exception("network error")):
|
|
|
|
|
+ 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):
|
|
|
|
|
+ import caldav.lib.error as caldav_error
|
|
|
|
|
+
|
|
|
|
|
+ entry = _caldav_entry(hass)
|
|
|
|
|
+ coord = RoomCalCoordinator(hass, entry)
|
|
|
|
|
+ await coord.async_setup()
|
|
|
|
|
+
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch.object(coord, "_fetch_caldav", side_effect=caldav_error.AuthorizationError),
|
|
|
|
|
+ patch.object(entry, "async_start_reauth") as mock_reauth,
|
|
|
|
|
+ ):
|
|
|
|
|
+ await coord.async_refresh()
|
|
|
|
|
+
|
|
|
|
|
+ mock_reauth.assert_called_once_with(hass)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+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"
|
|
|
|
|
+ # Untouched fields preserved
|
|
|
|
|
+ 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
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+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)
|
|
|
|
|
+
|
|
|
|
|
+ # New coordinator instance simulates HA restart
|
|
|
|
|
+ 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"
|