| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- """Unit tests for RoomCalApiBackend."""
- from contextlib import asynccontextmanager
- from datetime import date, datetime, timedelta, timezone
- from unittest.mock import AsyncMock, MagicMock, patch
- import pytest
- from homeassistant.core import HomeAssistant
- from custom_components.room_cal_ha.backends import BackendAuthError, BackendConnectError
- from custom_components.room_cal_ha.backends.api import RoomCalApiBackend, _parse_reservation
- from custom_components.room_cal_ha.const import CONF_API_BASE_URL, CONF_API_TOKEN, CONF_SYNC_MODE, SYNC_MODE_API
- from custom_components.room_cal_ha.models import ReservationData
- # ---------------------------------------------------------------------------
- # Helpers
- # ---------------------------------------------------------------------------
- def _make_entry(base_url="https://api.example.com", token="test-token"):
- entry = MagicMock()
- entry.options = {CONF_API_BASE_URL: base_url, CONF_SYNC_MODE: SYNC_MODE_API}
- entry.data = {CONF_API_TOKEN: token}
- return entry
- def _mock_session(status: int, json_data=None):
- """Return a mock aiohttp session that yields one response."""
- resp = MagicMock()
- resp.status = status
- resp.json = AsyncMock(return_value=json_data)
- if status >= 400 and status not in (401, 403):
- resp.raise_for_status.side_effect = Exception(f"HTTP {status}")
- @asynccontextmanager
- async def _get(*args, **kwargs):
- yield resp
- session = MagicMock()
- session.get = _get
- return session
- def _mock_session_raises(exc):
- """Return a session whose .get() call raises immediately."""
- session = MagicMock()
- session.get = MagicMock(side_effect=exc)
- return session
- def _reservation_slot(guest, checkin, checkout):
- return {
- "guest_name": guest,
- "checkin_date": str(checkin),
- "checkout_date": str(checkout),
- "wifi_key": "wifikey",
- "door_code": "1234",
- }
- # ---------------------------------------------------------------------------
- # _parse_reservation
- # ---------------------------------------------------------------------------
- def test_parse_reservation_maps_fields():
- raw = {
- "guest_name": "Alice",
- "checkin_date": "2026-07-01",
- "checkout_date": "2026-07-05",
- "wifi_key": "secret",
- "door_code": "4567",
- }
- res = _parse_reservation(raw)
- assert isinstance(res, ReservationData)
- assert res.guest_name == "Alice"
- assert res.check_in_date == date(2026, 7, 1)
- assert res.check_out_date == date(2026, 7, 5)
- assert res.wifi_key == "secret"
- assert res.door_code == "4567"
- def test_parse_reservation_none_returns_none():
- assert _parse_reservation(None) is None
- def test_parse_reservation_missing_dates_gives_none_fields():
- res = _parse_reservation({"guest_name": "Bob"})
- assert res.guest_name == "Bob"
- assert res.check_in_date is None
- assert res.check_out_date is None
- # ---------------------------------------------------------------------------
- # async_fetch — reservation classification
- # ---------------------------------------------------------------------------
- async def test_fetch_current_today_no_next(hass: HomeAssistant):
- today = date.today()
- tomorrow = today + timedelta(days=1)
- response = {str(today): _reservation_slot("Alice", today, tomorrow)}
- session = _mock_session(200, response)
- entry = _make_entry()
- backend = RoomCalApiBackend(hass, entry)
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- current, nxt = await backend.async_fetch()
- assert current is not None
- assert current.guest_name == "Alice"
- assert nxt is None
- async def test_fetch_no_current_next_upcoming(hass: HomeAssistant):
- today = date.today()
- future = today + timedelta(days=5)
- future_end = today + timedelta(days=8)
- response = {
- str(today): None,
- str(future): _reservation_slot("Bob", future, future_end),
- }
- session = _mock_session(200, response)
- backend = RoomCalApiBackend(hass, _make_entry())
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- current, nxt = await backend.async_fetch()
- assert current is None
- assert nxt is not None
- assert nxt.guest_name == "Bob"
- async def test_fetch_both_current_and_next(hass: HomeAssistant):
- today = date.today()
- tomorrow = today + timedelta(days=1)
- future = today + timedelta(days=5)
- future_end = today + timedelta(days=8)
- response = {
- str(today): _reservation_slot("Alice", today, tomorrow),
- str(future): _reservation_slot("Bob", future, future_end),
- }
- session = _mock_session(200, response)
- backend = RoomCalApiBackend(hass, _make_entry())
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- current, nxt = await backend.async_fetch()
- assert current.guest_name == "Alice"
- assert nxt.guest_name == "Bob"
- async def test_fetch_multiday_current_not_duplicated_as_next(hass: HomeAssistant):
- """A reservation spanning multiple days should produce one current, not also be treated as next."""
- today = date.today()
- checkout = today + timedelta(days=3)
- slot = _reservation_slot("Alice", today, checkout)
- # Same reservation appears on each day it spans
- response = {
- str(today): slot,
- str(today + timedelta(days=1)): slot,
- str(today + timedelta(days=2)): slot,
- }
- session = _mock_session(200, response)
- backend = RoomCalApiBackend(hass, _make_entry())
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- current, nxt = await backend.async_fetch()
- assert current.guest_name == "Alice"
- assert nxt is None
- async def test_fetch_empty_response(hass: HomeAssistant):
- session = _mock_session(200, {})
- backend = RoomCalApiBackend(hass, _make_entry())
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- current, nxt = await backend.async_fetch()
- assert current is None
- assert nxt is None
- # ---------------------------------------------------------------------------
- # async_fetch — error handling
- # ---------------------------------------------------------------------------
- async def test_fetch_401_raises_backend_auth_error(hass: HomeAssistant):
- session = _mock_session(401, {})
- backend = RoomCalApiBackend(hass, _make_entry())
- with (
- patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
- pytest.raises(BackendAuthError),
- ):
- await backend.async_fetch()
- async def test_fetch_403_raises_backend_auth_error(hass: HomeAssistant):
- session = _mock_session(403, {})
- backend = RoomCalApiBackend(hass, _make_entry())
- with (
- patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
- pytest.raises(BackendAuthError),
- ):
- await backend.async_fetch()
- async def test_fetch_network_error_raises_backend_connect_error(hass: HomeAssistant):
- session = _mock_session_raises(Exception("connection refused"))
- backend = RoomCalApiBackend(hass, _make_entry())
- with (
- patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
- pytest.raises(BackendConnectError),
- ):
- await backend.async_fetch()
- async def test_fetch_500_raises_backend_connect_error(hass: HomeAssistant):
- session = _mock_session(500, {})
- backend = RoomCalApiBackend(hass, _make_entry())
- with (
- patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
- pytest.raises(BackendConnectError),
- ):
- await backend.async_fetch()
- # ---------------------------------------------------------------------------
- # async_fetch_status
- # ---------------------------------------------------------------------------
- async def test_fetch_status_ok(hass: HomeAssistant):
- ts = "2026-06-22T10:00:00+00:00"
- body = {
- "status": "ok",
- "checks": {
- "caldav_connection": {"ok": True},
- "calendar": {"ok": True},
- "credentials": {},
- },
- "checked_at": ts,
- }
- session = _mock_session(200, body)
- backend = RoomCalApiBackend(hass, _make_entry())
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- status = await backend.async_fetch_status()
- assert status.status == "ok"
- assert status.checks["caldav_connection"]["ok"] is True
- assert status.checked_at == datetime.fromisoformat(ts)
- assert status.stale is False
- async def test_fetch_status_stale_on_non_200(hass: HomeAssistant):
- body = {"status": "error", "checks": {}, "checked_at": "2026-06-22T10:00:00+00:00"}
- session = _mock_session(503, body)
- # 503 is not in 401/403 so it won't raise BackendAuthError; raise_for_status is NOT called in async_fetch_status
- backend = RoomCalApiBackend(hass, _make_entry())
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- status = await backend.async_fetch_status()
- assert status.status == "error"
- assert status.stale is True
- async def test_fetch_status_401_returns_auth_error_status(hass: HomeAssistant):
- session = _mock_session(401, {})
- backend = RoomCalApiBackend(hass, _make_entry())
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- status = await backend.async_fetch_status()
- assert status.status == "auth_error"
- async def test_fetch_status_network_error_returns_unknown(hass: HomeAssistant):
- session = _mock_session_raises(Exception("timeout"))
- backend = RoomCalApiBackend(hass, _make_entry())
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- status = await backend.async_fetch_status()
- assert status.status == "unknown"
- async def test_fetch_status_bad_checked_at_doesnt_crash(hass: HomeAssistant):
- body = {"status": "ok", "checks": {}, "checked_at": "not-a-date"}
- session = _mock_session(200, body)
- backend = RoomCalApiBackend(hass, _make_entry())
- with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
- status = await backend.async_fetch_status()
- assert status.status == "ok"
- assert status.checked_at is None
|