"""Unit tests for RoomCalApiBackend.""" from contextlib import asynccontextmanager from datetime import date, timedelta 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()