Kaynağa Gözat

Add test scaffolding with pytest-homeassistant-custom-component

ZiB 4 ay önce
ebeveyn
işleme
21413f15a0

+ 17 - 0
pyproject.toml

@@ -0,0 +1,17 @@
+[project]
+name = "room-cal-ha"
+version = "0.1.0"
+requires-python = ">=3.12"
+
+[dependency-groups]
+dev = [
+  "caldav>=3.2.1",
+  "icalendar>=7.1.3",
+  "pytest-homeassistant-custom-component",
+]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+asyncio_mode = "auto"
+
+[tool.uv.sources]

+ 0 - 0
tests/__init__.py


BIN
tests/__pycache__/__init__.cpython-314.pyc


BIN
tests/__pycache__/conftest.cpython-314-pytest-9.0.3.pyc


BIN
tests/__pycache__/test_config_flow.cpython-314-pytest-9.0.3.pyc


BIN
tests/__pycache__/test_coordinator.cpython-314-pytest-9.0.3.pyc


+ 28 - 0
tests/conftest.py

@@ -0,0 +1,28 @@
+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,
+  DOMAIN,
+)
+
+
+@pytest.fixture(autouse=True)
+def auto_enable_custom_integrations(enable_custom_integrations):
+  yield
+
+
+@pytest.fixture
+def mock_entry():
+  return MockConfigEntry(
+    domain=DOMAIN,
+    data={
+      CONF_ROOM_STATE_NAME: "Room State",
+      CONF_CURRENT_NAME: "Current",
+      CONF_NEXT_NAME: "Next",
+    },
+    options={CONF_CALDAV_ENABLED: False},
+  )

+ 105 - 0
tests/test_config_flow.py

@@ -0,0 +1,105 @@
+from unittest.mock import patch
+
+import pytest
+from homeassistant.config_entries import SOURCE_USER
+from homeassistant.core import HomeAssistant
+
+from custom_components.room_cal_ha.const import (
+  CONF_CALDAV_ENABLED,
+  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,
+  DOMAIN,
+)
+
+
+async def test_user_step_shows_defaults(hass: HomeAssistant):
+  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
+
+
+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(
+    result["flow_id"],
+    user_input={
+      CONF_ROOM_STATE_NAME: "My Room",
+      CONF_CURRENT_NAME: "Staying Now",
+      CONF_NEXT_NAME: "Coming Up",
+    },
+  )
+  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"
+
+
+async def test_options_flow_caldav_disabled(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"] == "caldav"
+
+  result = await hass.config_entries.options.async_configure(
+    result["flow_id"],
+    user_input={CONF_CALDAV_ENABLED: False, CONF_CALDAV_URL: "", CONF_CALENDAR_NAME: ""},
+  )
+  assert result["type"] == "create_entry"
+  assert result["data"][CONF_CALDAV_ENABLED] is False
+
+
+async def test_options_flow_caldav_enabled_tests_connection(hass: HomeAssistant, mock_entry):
+  mock_entry.add_to_hass(hass)
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+
+  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_ENABLED: True,
+        CONF_CALDAV_URL: "https://cal.example.com",
+        CONF_CALENDAR_NAME: "Room",
+      },
+    )
+  assert result["type"] == "create_entry"
+  assert result["data"][CONF_CALDAV_ENABLED] is True
+
+
+async def test_options_flow_bad_credentials_shows_error(hass: HomeAssistant, mock_entry):
+  import caldav.lib.error as caldav_error
+
+  mock_entry.add_to_hass(hass)
+  result = await hass.config_entries.options.async_init(mock_entry.entry_id)
+
+  with patch(
+    "custom_components.room_cal_ha.config_flow._test_caldav_auth",
+    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",
+      },
+    )
+  assert result["type"] == "form"
+  assert result["errors"]["base"] == "invalid_auth"

+ 158 - 0
tests/test_coordinator.py

@@ -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"