| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- """Unit tests for RoomCalApiBackend."""
- from contextlib import asynccontextmanager
- from datetime import date, time, 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, checkin_time="14:00", checkout_time="11:00"):
- return {
- "guest_name": guest,
- "checkin_date": str(checkin),
- "checkin_time": checkin_time,
- "checkout_date": str(checkout),
- "checkout_time": checkout_time,
- "wifi_ssid": "GuestNet",
- "wifi_key": "wifikey",
- "door_code": "1234",
- }
- # ---------------------------------------------------------------------------
- # _parse_reservation
- # ---------------------------------------------------------------------------
- def test_parse_reservation_maps_fields():
- raw = {
- "guest_name": "Alice",
- "checkin_date": "2026-07-01",
- "checkin_time": "14:00",
- "checkout_date": "2026-07-05",
- "checkout_time": "11:00",
- "wifi_ssid": "GuestNet",
- "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_in_time == time(14, 0)
- assert res.check_out_date == date(2026, 7, 5)
- assert res.check_out_time == time(11, 0)
- assert res.wifi_ssid == "GuestNet"
- 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_in_time is None
- assert res.check_out_date is None
- assert res.check_out_time is None
- # ---------------------------------------------------------------------------
- # async_fetch — reservation classification
- # ---------------------------------------------------------------------------
- async def test_fetch_current_today_no_next(hass: HomeAssistant):
- # checkin yesterday at 14:00, checkout tomorrow at 11:00 — window always active
- today = date.today()
- yesterday = today - timedelta(days=1)
- tomorrow = today + timedelta(days=1)
- response = {str(today): _reservation_slot("Alice", yesterday, 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):
- # Alice: window always active (checked in yesterday, out tomorrow)
- # Bob: future (5 days from now)
- today = date.today()
- yesterday = today - timedelta(days=1)
- tomorrow = today + timedelta(days=1)
- future = today + timedelta(days=5)
- future_end = today + timedelta(days=8)
- response = {
- str(today): _reservation_slot("Alice", yesterday, 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 multi-day stay appearing on multiple date keys should produce one current, not also next."""
- # checkin yesterday, checkout in 3 days — window always active regardless of test time
- today = date.today()
- yesterday = today - timedelta(days=1)
- checkout = today + timedelta(days=3)
- slot = _reservation_slot("Alice", yesterday, 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_current_not_promoted_before_checkin_time(hass: HomeAssistant):
- """Next guest's slot should not become current just because their check-in DATE is today."""
- # Guest A: checked in 2 days ago, checks out today at 23:59 — window active
- # Guest B: checks in today at 14:00 — should be next, not current
- # This covers the midnight-transition bug where a date-key lookup would wrongly
- # promote Guest B to current at midnight of their check-in date.
- from datetime import datetime as _dt, time as _time
- today = date.today()
- two_days_ago = today - timedelta(days=2)
- tomorrow = today + timedelta(days=1)
- response = {
- str(today): _reservation_slot("Alice", two_days_ago, today, checkout_time="23:59"),
- str(tomorrow): _reservation_slot("Bob", today, tomorrow, checkin_time="14:00"),
- }
- # Pin now to 09:00 — before Alice's checkout AND before Bob's check-in
- fake_now = _dt.combine(today, _time(9, 0))
- 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),
- patch("custom_components.room_cal_ha.backends.api.datetime") as mock_dt,
- ):
- mock_dt.now.return_value = fake_now
- mock_dt.combine = _dt.combine
- current, nxt = await backend.async_fetch()
- assert current is not None
- assert current.guest_name == "Alice"
- assert nxt is not None
- assert nxt.guest_name == "Bob"
- 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()
|