|
|
@@ -1,22 +1,27 @@
|
|
|
-from datetime import date, time, datetime, timezone
|
|
|
+from datetime import date, time
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
|
|
import pytest
|
|
|
from homeassistant.core import HomeAssistant
|
|
|
+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_CALDAV_ENABLED,
|
|
|
+ CONF_API_BASE_URL,
|
|
|
+ CONF_API_TOKEN,
|
|
|
CONF_CALDAV_URL,
|
|
|
CONF_CALENDAR_NAME,
|
|
|
+ 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 ReservationData
|
|
|
-
|
|
|
+from custom_components.room_cal_ha.models import ApiStatusData, ReservationData
|
|
|
|
|
|
-from pytest_homeassistant_custom_component.common import MockConfigEntry
|
|
|
|
|
|
SAMPLE = ReservationData(
|
|
|
guest_name="Alice",
|
|
|
@@ -28,21 +33,15 @@ SAMPLE = ReservationData(
|
|
|
door_code="0472",
|
|
|
)
|
|
|
|
|
|
-CALDAV_OPTIONS = {
|
|
|
- CONF_CALDAV_ENABLED: True,
|
|
|
- CONF_CALDAV_URL: "https://cal.example.com",
|
|
|
- CONF_CALENDAR_NAME: "Room",
|
|
|
-}
|
|
|
-
|
|
|
-_ENTRY_DATA = {
|
|
|
+_BASE_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)
|
|
|
+def _make_entry(hass, options: dict) -> MockConfigEntry:
|
|
|
+ entry = MockConfigEntry(domain=DOMAIN, data=_BASE_DATA, options=options)
|
|
|
entry.add_to_hass(hass)
|
|
|
return entry
|
|
|
|
|
|
@@ -55,20 +54,30 @@ async def coordinator(hass: HomeAssistant, mock_entry):
|
|
|
return coord
|
|
|
|
|
|
|
|
|
-async def test_caldav_disabled_serves_empty(coordinator: RoomCalCoordinator):
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# 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
|
|
|
+ assert coordinator.data.api_status is None
|
|
|
|
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# CalDAV mode
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
async def test_caldav_fetch_writes_store_and_timestamps(hass: HomeAssistant):
|
|
|
- entry = _caldav_entry(hass)
|
|
|
+ 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.object(coord, "_fetch_caldav", return_value=(SAMPLE, None)):
|
|
|
+ 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"
|
|
|
@@ -79,25 +88,27 @@ async def test_caldav_fetch_writes_store_and_timestamps(hass: HomeAssistant):
|
|
|
|
|
|
|
|
|
async def test_caldav_empty_result_clears_store(hass: HomeAssistant):
|
|
|
- entry = _caldav_entry(hass)
|
|
|
+ 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.object(coord, "_fetch_caldav", return_value=(None, None)):
|
|
|
+ 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_failure_preserves_store(hass: HomeAssistant):
|
|
|
- entry = _caldav_entry(hass)
|
|
|
+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.object(coord, "_fetch_caldav", side_effect=Exception("network error")):
|
|
|
+ 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"
|
|
|
@@ -105,21 +116,85 @@ async def test_caldav_failure_preserves_store(hass: HomeAssistant):
|
|
|
|
|
|
|
|
|
async def test_caldav_auth_error_triggers_reauth(hass: HomeAssistant):
|
|
|
- import caldav.lib.error as caldav_error
|
|
|
+ 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
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+SAMPLE_STATUS = ApiStatusData(
|
|
|
+ status="ok",
|
|
|
+ checks={"caldav_connection": {"ok": True}, "calendar": {"ok": True}},
|
|
|
+)
|
|
|
+
|
|
|
|
|
|
- entry = _caldav_entry(hass)
|
|
|
+async def test_api_fetch_writes_store_and_status(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))
|
|
|
+ instance.async_fetch_status = AsyncMock(return_value=SAMPLE_STATUS)
|
|
|
+ await coord.async_refresh()
|
|
|
+
|
|
|
+ assert coord.data.current.guest_name == "Alice"
|
|
|
+ assert coord.data.api_status.status == "ok"
|
|
|
+ assert coord.data.current_last_refresh is not None
|
|
|
+
|
|
|
+
|
|
|
+async def test_api_connect_error_preserves_store_and_still_fetches_status(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)
|
|
|
+
|
|
|
+ degraded = ApiStatusData(status="error")
|
|
|
+ with patch("custom_components.room_cal_ha.coordinator.RoomCalApiBackend") as MockBackend:
|
|
|
+ instance = MockBackend.return_value
|
|
|
+ instance.async_fetch = AsyncMock(side_effect=BackendConnectError("timeout"))
|
|
|
+ instance.async_fetch_status = AsyncMock(return_value=degraded)
|
|
|
+ await coord.async_refresh()
|
|
|
+
|
|
|
+ assert coord.data.current.guest_name == "Alice"
|
|
|
+ assert coord.data.current_last_refresh is None
|
|
|
+ assert coord.data.api_status.status == "error"
|
|
|
+
|
|
|
+
|
|
|
+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.object(coord, "_fetch_caldav", side_effect=caldav_error.AuthorizationError),
|
|
|
+ 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)
|
|
|
+ instance.async_fetch_status = AsyncMock(return_value=ApiStatusData(status="auth_error"))
|
|
|
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()
|
|
|
@@ -128,7 +203,6 @@ async def test_thin_update_overlays_fields(coordinator: RoomCalCoordinator):
|
|
|
|
|
|
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"
|
|
|
|
|
|
@@ -142,13 +216,16 @@ async def test_thin_update_updates_last_refresh(coordinator: RoomCalCoordinator)
|
|
|
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)
|
|
|
|
|
|
- # New coordinator instance simulates HA restart
|
|
|
coord2 = RoomCalCoordinator(hass, mock_entry)
|
|
|
await coord2.async_setup()
|
|
|
|