test_api_backend.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. """Unit tests for RoomCalApiBackend."""
  2. from contextlib import asynccontextmanager
  3. from datetime import date, time, timedelta
  4. from unittest.mock import AsyncMock, MagicMock, patch
  5. import pytest
  6. from homeassistant.core import HomeAssistant
  7. from custom_components.room_cal_ha.backends import BackendAuthError, BackendConnectError
  8. from custom_components.room_cal_ha.backends.api import RoomCalApiBackend, _parse_reservation
  9. from custom_components.room_cal_ha.const import CONF_API_BASE_URL, CONF_API_TOKEN, CONF_SYNC_MODE, SYNC_MODE_API
  10. from custom_components.room_cal_ha.models import ReservationData
  11. # ---------------------------------------------------------------------------
  12. # Helpers
  13. # ---------------------------------------------------------------------------
  14. def _make_entry(base_url="https://api.example.com", token="test-token"):
  15. entry = MagicMock()
  16. entry.options = {CONF_API_BASE_URL: base_url, CONF_SYNC_MODE: SYNC_MODE_API}
  17. entry.data = {CONF_API_TOKEN: token}
  18. return entry
  19. def _mock_session(status: int, json_data=None):
  20. """Return a mock aiohttp session that yields one response."""
  21. resp = MagicMock()
  22. resp.status = status
  23. resp.json = AsyncMock(return_value=json_data)
  24. if status >= 400 and status not in (401, 403):
  25. resp.raise_for_status.side_effect = Exception(f"HTTP {status}")
  26. @asynccontextmanager
  27. async def _get(*args, **kwargs):
  28. yield resp
  29. session = MagicMock()
  30. session.get = _get
  31. return session
  32. def _mock_session_raises(exc):
  33. """Return a session whose .get() call raises immediately."""
  34. session = MagicMock()
  35. session.get = MagicMock(side_effect=exc)
  36. return session
  37. def _reservation_slot(guest, checkin, checkout):
  38. return {
  39. "guest_name": guest,
  40. "checkin_date": str(checkin),
  41. "checkin_time": "14:00",
  42. "checkout_date": str(checkout),
  43. "checkout_time": "11:00",
  44. "wifi_ssid": "GuestNet",
  45. "wifi_key": "wifikey",
  46. "door_code": "1234",
  47. }
  48. # ---------------------------------------------------------------------------
  49. # _parse_reservation
  50. # ---------------------------------------------------------------------------
  51. def test_parse_reservation_maps_fields():
  52. raw = {
  53. "guest_name": "Alice",
  54. "checkin_date": "2026-07-01",
  55. "checkin_time": "14:00",
  56. "checkout_date": "2026-07-05",
  57. "checkout_time": "11:00",
  58. "wifi_ssid": "GuestNet",
  59. "wifi_key": "secret",
  60. "door_code": "4567",
  61. }
  62. res = _parse_reservation(raw)
  63. assert isinstance(res, ReservationData)
  64. assert res.guest_name == "Alice"
  65. assert res.check_in_date == date(2026, 7, 1)
  66. assert res.check_in_time == time(14, 0)
  67. assert res.check_out_date == date(2026, 7, 5)
  68. assert res.check_out_time == time(11, 0)
  69. assert res.wifi_ssid == "GuestNet"
  70. assert res.wifi_key == "secret"
  71. assert res.door_code == "4567"
  72. def test_parse_reservation_none_returns_none():
  73. assert _parse_reservation(None) is None
  74. def test_parse_reservation_missing_dates_gives_none_fields():
  75. res = _parse_reservation({"guest_name": "Bob"})
  76. assert res.guest_name == "Bob"
  77. assert res.check_in_date is None
  78. assert res.check_in_time is None
  79. assert res.check_out_date is None
  80. assert res.check_out_time is None
  81. # ---------------------------------------------------------------------------
  82. # async_fetch — reservation classification
  83. # ---------------------------------------------------------------------------
  84. async def test_fetch_current_today_no_next(hass: HomeAssistant):
  85. today = date.today()
  86. tomorrow = today + timedelta(days=1)
  87. response = {str(today): _reservation_slot("Alice", today, tomorrow)}
  88. session = _mock_session(200, response)
  89. entry = _make_entry()
  90. backend = RoomCalApiBackend(hass, entry)
  91. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  92. current, nxt = await backend.async_fetch()
  93. assert current is not None
  94. assert current.guest_name == "Alice"
  95. assert nxt is None
  96. async def test_fetch_no_current_next_upcoming(hass: HomeAssistant):
  97. today = date.today()
  98. future = today + timedelta(days=5)
  99. future_end = today + timedelta(days=8)
  100. response = {
  101. str(today): None,
  102. str(future): _reservation_slot("Bob", future, future_end),
  103. }
  104. session = _mock_session(200, response)
  105. backend = RoomCalApiBackend(hass, _make_entry())
  106. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  107. current, nxt = await backend.async_fetch()
  108. assert current is None
  109. assert nxt is not None
  110. assert nxt.guest_name == "Bob"
  111. async def test_fetch_both_current_and_next(hass: HomeAssistant):
  112. today = date.today()
  113. tomorrow = today + timedelta(days=1)
  114. future = today + timedelta(days=5)
  115. future_end = today + timedelta(days=8)
  116. response = {
  117. str(today): _reservation_slot("Alice", today, tomorrow),
  118. str(future): _reservation_slot("Bob", future, future_end),
  119. }
  120. session = _mock_session(200, response)
  121. backend = RoomCalApiBackend(hass, _make_entry())
  122. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  123. current, nxt = await backend.async_fetch()
  124. assert current.guest_name == "Alice"
  125. assert nxt.guest_name == "Bob"
  126. async def test_fetch_multiday_current_not_duplicated_as_next(hass: HomeAssistant):
  127. """A reservation spanning multiple days should produce one current, not also be treated as next."""
  128. today = date.today()
  129. checkout = today + timedelta(days=3)
  130. slot = _reservation_slot("Alice", today, checkout)
  131. # Same reservation appears on each day it spans
  132. response = {
  133. str(today): slot,
  134. str(today + timedelta(days=1)): slot,
  135. str(today + timedelta(days=2)): slot,
  136. }
  137. session = _mock_session(200, response)
  138. backend = RoomCalApiBackend(hass, _make_entry())
  139. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  140. current, nxt = await backend.async_fetch()
  141. assert current.guest_name == "Alice"
  142. assert nxt is None
  143. async def test_fetch_empty_response(hass: HomeAssistant):
  144. session = _mock_session(200, {})
  145. backend = RoomCalApiBackend(hass, _make_entry())
  146. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  147. current, nxt = await backend.async_fetch()
  148. assert current is None
  149. assert nxt is None
  150. # ---------------------------------------------------------------------------
  151. # async_fetch — error handling
  152. # ---------------------------------------------------------------------------
  153. async def test_fetch_401_raises_backend_auth_error(hass: HomeAssistant):
  154. session = _mock_session(401, {})
  155. backend = RoomCalApiBackend(hass, _make_entry())
  156. with (
  157. patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
  158. pytest.raises(BackendAuthError),
  159. ):
  160. await backend.async_fetch()
  161. async def test_fetch_403_raises_backend_auth_error(hass: HomeAssistant):
  162. session = _mock_session(403, {})
  163. backend = RoomCalApiBackend(hass, _make_entry())
  164. with (
  165. patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
  166. pytest.raises(BackendAuthError),
  167. ):
  168. await backend.async_fetch()
  169. async def test_fetch_network_error_raises_backend_connect_error(hass: HomeAssistant):
  170. session = _mock_session_raises(Exception("connection refused"))
  171. backend = RoomCalApiBackend(hass, _make_entry())
  172. with (
  173. patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
  174. pytest.raises(BackendConnectError),
  175. ):
  176. await backend.async_fetch()
  177. async def test_fetch_500_raises_backend_connect_error(hass: HomeAssistant):
  178. session = _mock_session(500, {})
  179. backend = RoomCalApiBackend(hass, _make_entry())
  180. with (
  181. patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
  182. pytest.raises(BackendConnectError),
  183. ):
  184. await backend.async_fetch()