Преглед изворни кода

Update tests to match current config flow and backend design

- conftest: replace CONF_CALDAV_ENABLED with CONF_SYNC_MODE/SYNC_MODE_MANUAL
- test_config_flow: rewrite for two-step options flow (sync_mode → caldav/api);
  add API mode tests; remove obsolete user-step form assertions
- test_coordinator: patch CalDavBackend/RoomCalApiBackend classes instead of
  removed _fetch_caldav method; add API mode and api_status assertions
ZiB пре 4 месеци
родитељ
комит
e80d1073cb
3 измењених фајлова са 279 додато и 70 уклоњено
  1. 3 2
      tests/conftest.py
  2. 172 41
      tests/test_config_flow.py
  3. 104 27
      tests/test_coordinator.py

+ 3 - 2
tests/conftest.py

@@ -2,11 +2,12 @@ import pytest
 from pytest_homeassistant_custom_component.common import MockConfigEntry
 
 from custom_components.room_cal_ha.const import (
-  CONF_CALDAV_ENABLED,
   CONF_CURRENT_NAME,
   CONF_NEXT_NAME,
   CONF_ROOM_STATE_NAME,
+  CONF_SYNC_MODE,
   DOMAIN,
+  SYNC_MODE_MANUAL,
 )
 
 
@@ -24,5 +25,5 @@ def mock_entry():
       CONF_CURRENT_NAME: "Current",
       CONF_NEXT_NAME: "Next",
     },
-    options={CONF_CALDAV_ENABLED: False},
+    options={CONF_SYNC_MODE: SYNC_MODE_MANUAL},
   )

+ 172 - 41
tests/test_config_flow.py

@@ -1,105 +1,236 @@
-from unittest.mock import patch
+from unittest.mock import AsyncMock, patch
 
 import pytest
+import caldav.lib.error as caldav_error
 from homeassistant.config_entries import SOURCE_USER
 from homeassistant.core import HomeAssistant
 
 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_CURRENT_NAME,
-  CONF_NEXT_NAME,
-  CONF_ROOM_STATE_NAME,
-  DEFAULT_CURRENT_NAME,
-  DEFAULT_NEXT_NAME,
-  DEFAULT_ROOM_STATE_NAME,
+  CONF_PASSWORD,
+  CONF_REFRESH_INTERVAL,
+  CONF_SYNC_MODE,
+  CONF_USERNAME,
+  DEFAULT_REFRESH_INTERVAL,
   DOMAIN,
+  SYNC_MODE_API,
+  SYNC_MODE_CALDAV,
+  SYNC_MODE_MANUAL,
 )
 
 
-async def test_user_step_shows_defaults(hass: HomeAssistant):
+# ---------------------------------------------------------------------------
+# Config flow — user step
+# ---------------------------------------------------------------------------
+
+async def test_user_step_creates_entry_immediately(hass: HomeAssistant):
+  """async_step_user creates an entry without showing a form."""
   result = await hass.config_entries.flow.async_init(
     DOMAIN, context={"source": SOURCE_USER}
   )
-  assert result["type"] == "form"
-  assert result["step_id"] == "user"
-  schema = result["data_schema"].schema
-  keys = {str(k): k for k in schema}
-  assert keys[CONF_ROOM_STATE_NAME].default() == DEFAULT_ROOM_STATE_NAME
-  assert keys[CONF_CURRENT_NAME].default() == DEFAULT_CURRENT_NAME
-  assert keys[CONF_NEXT_NAME].default() == DEFAULT_NEXT_NAME
+  assert result["type"] == "create_entry"
+  assert result["title"] == "Room State"
 
 
-async def test_user_step_creates_entry(hass: HomeAssistant):
-  result = await hass.config_entries.flow.async_init(
-    DOMAIN, context={"source": SOURCE_USER}
-  )
-  result = await hass.config_entries.flow.async_configure(
+# ---------------------------------------------------------------------------
+# Options flow — sync_mode step
+# ---------------------------------------------------------------------------
+
+async def test_options_sync_mode_manual(hass: HomeAssistant, mock_entry):
+  mock_entry.add_to_hass(hass)
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  assert result["type"] == "form"
+  assert result["step_id"] == "sync_mode"
+
+  result = await hass.config_entries.options.async_configure(
     result["flow_id"],
-    user_input={
-      CONF_ROOM_STATE_NAME: "My Room",
-      CONF_CURRENT_NAME: "Staying Now",
-      CONF_NEXT_NAME: "Coming Up",
-    },
+    user_input={CONF_SYNC_MODE: SYNC_MODE_MANUAL},
   )
   assert result["type"] == "create_entry"
-  assert result["data"][CONF_ROOM_STATE_NAME] == "My Room"
-  assert result["data"][CONF_CURRENT_NAME] == "Staying Now"
-  assert result["data"][CONF_NEXT_NAME] == "Coming Up"
+  assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_MANUAL
 
 
-async def test_options_flow_caldav_disabled(hass: HomeAssistant, mock_entry):
+async def test_options_sync_mode_caldav_proceeds_to_caldav_step(hass: HomeAssistant, mock_entry):
   mock_entry.add_to_hass(hass)
   result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  result = await hass.config_entries.options.async_configure(
+    result["flow_id"],
+    user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV},
+  )
   assert result["type"] == "form"
   assert result["step_id"] == "caldav"
 
+
+async def test_options_sync_mode_api_proceeds_to_api_step(hass: HomeAssistant, mock_entry):
+  mock_entry.add_to_hass(hass)
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
   result = await hass.config_entries.options.async_configure(
     result["flow_id"],
-    user_input={CONF_CALDAV_ENABLED: False, CONF_CALDAV_URL: "", CONF_CALENDAR_NAME: ""},
+    user_input={CONF_SYNC_MODE: SYNC_MODE_API},
   )
+  assert result["type"] == "form"
+  assert result["step_id"] == "api"
+
+
+# ---------------------------------------------------------------------------
+# Options flow — CalDAV step
+# ---------------------------------------------------------------------------
+
+async def test_options_caldav_saves_on_success(hass: HomeAssistant, mock_entry):
+  mock_entry.add_to_hass(hass)
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  await hass.config_entries.options.async_configure(
+    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV}
+  )
+
+  with patch("custom_components.room_cal_ha.config_flow._test_caldav_auth", return_value=None):
+    result = await hass.config_entries.options.async_configure(
+      result["flow_id"],
+      user_input={
+        CONF_CALDAV_URL: "https://cal.example.com",
+        CONF_CALENDAR_NAME: "Room",
+        CONF_USERNAME: "user",
+        CONF_PASSWORD: "pass",
+        CONF_REFRESH_INTERVAL: 30,
+      },
+    )
+
   assert result["type"] == "create_entry"
-  assert result["data"][CONF_CALDAV_ENABLED] is False
+  assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_CALDAV
+  assert result["data"][CONF_CALDAV_URL] == "https://cal.example.com"
+  assert result["data"][CONF_REFRESH_INTERVAL] == 30
 
 
-async def test_options_flow_caldav_enabled_tests_connection(hass: HomeAssistant, mock_entry):
+async def test_options_caldav_bad_credentials_shows_error(hass: HomeAssistant, mock_entry):
   mock_entry.add_to_hass(hass)
   result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  await hass.config_entries.options.async_configure(
+    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV}
+  )
 
   with patch(
     "custom_components.room_cal_ha.config_flow._test_caldav_auth",
-    return_value=None,
+    side_effect=caldav_error.AuthorizationError,
   ):
     result = await hass.config_entries.options.async_configure(
       result["flow_id"],
       user_input={
-        CONF_CALDAV_ENABLED: True,
         CONF_CALDAV_URL: "https://cal.example.com",
         CONF_CALENDAR_NAME: "Room",
+        CONF_USERNAME: "user",
+        CONF_PASSWORD: "bad",
+        CONF_REFRESH_INTERVAL: 15,
       },
     )
-  assert result["type"] == "create_entry"
-  assert result["data"][CONF_CALDAV_ENABLED] is True
 
+  assert result["type"] == "form"
+  assert result["errors"]["base"] == "invalid_auth"
 
-async def test_options_flow_bad_credentials_shows_error(hass: HomeAssistant, mock_entry):
-  import caldav.lib.error as caldav_error
 
+async def test_options_caldav_connect_error_shows_error(hass: HomeAssistant, mock_entry):
   mock_entry.add_to_hass(hass)
   result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  await hass.config_entries.options.async_configure(
+    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV}
+  )
 
   with patch(
     "custom_components.room_cal_ha.config_flow._test_caldav_auth",
-    side_effect=caldav_error.AuthorizationError,
+    side_effect=Exception("timeout"),
   ):
     result = await hass.config_entries.options.async_configure(
       result["flow_id"],
       user_input={
-        CONF_CALDAV_ENABLED: True,
         CONF_CALDAV_URL: "https://cal.example.com",
         CONF_CALENDAR_NAME: "Room",
+        CONF_USERNAME: "user",
+        CONF_PASSWORD: "pass",
+        CONF_REFRESH_INTERVAL: 15,
       },
     )
+
+  assert result["type"] == "form"
+  assert result["errors"]["base"] == "cannot_connect"
+
+
+# ---------------------------------------------------------------------------
+# Options flow — API step
+# ---------------------------------------------------------------------------
+
+async def test_options_api_saves_on_success(hass: HomeAssistant, mock_entry):
+  mock_entry.add_to_hass(hass)
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  await hass.config_entries.options.async_configure(
+    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API}
+  )
+
+  with patch(
+    "custom_components.room_cal_ha.config_flow._test_api_connection",
+    new=AsyncMock(return_value=None),
+  ):
+    result = await hass.config_entries.options.async_configure(
+      result["flow_id"],
+      user_input={
+        CONF_API_BASE_URL: "https://api.example.com",
+        CONF_API_TOKEN: "tok-secret",
+        CONF_REFRESH_INTERVAL: 10,
+      },
+    )
+
+  assert result["type"] == "create_entry"
+  assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_API
+  assert result["data"][CONF_API_BASE_URL] == "https://api.example.com"
+  assert result["data"][CONF_REFRESH_INTERVAL] == 10
+
+
+async def test_options_api_bad_token_shows_error(hass: HomeAssistant, mock_entry):
+  from custom_components.room_cal_ha.config_flow import _ApiAuthError
+
+  mock_entry.add_to_hass(hass)
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  await hass.config_entries.options.async_configure(
+    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API}
+  )
+
+  with patch(
+    "custom_components.room_cal_ha.config_flow._test_api_connection",
+    new=AsyncMock(side_effect=_ApiAuthError),
+  ):
+    result = await hass.config_entries.options.async_configure(
+      result["flow_id"],
+      user_input={
+        CONF_API_BASE_URL: "https://api.example.com",
+        CONF_API_TOKEN: "bad-token",
+        CONF_REFRESH_INTERVAL: 15,
+      },
+    )
+
   assert result["type"] == "form"
   assert result["errors"]["base"] == "invalid_auth"
+
+
+async def test_options_api_connect_error_shows_error(hass: HomeAssistant, mock_entry):
+  mock_entry.add_to_hass(hass)
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+  await hass.config_entries.options.async_configure(
+    result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API}
+  )
+
+  with patch(
+    "custom_components.room_cal_ha.config_flow._test_api_connection",
+    new=AsyncMock(side_effect=Exception("timeout")),
+  ):
+    result = await hass.config_entries.options.async_configure(
+      result["flow_id"],
+      user_input={
+        CONF_API_BASE_URL: "https://api.example.com",
+        CONF_API_TOKEN: "tok",
+        CONF_REFRESH_INTERVAL: 15,
+      },
+    )
+
+  assert result["type"] == "form"
+  assert result["errors"]["base"] == "cannot_connect"

+ 104 - 27
tests/test_coordinator.py

@@ -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()