test_api_backend.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. """Unit tests for RoomCalApiBackend."""
  2. from contextlib import asynccontextmanager
  3. from datetime import date, 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. "checkout_date": str(checkout),
  42. "wifi_key": "wifikey",
  43. "door_code": "1234",
  44. }
  45. # ---------------------------------------------------------------------------
  46. # _parse_reservation
  47. # ---------------------------------------------------------------------------
  48. def test_parse_reservation_maps_fields():
  49. raw = {
  50. "guest_name": "Alice",
  51. "checkin_date": "2026-07-01",
  52. "checkout_date": "2026-07-05",
  53. "wifi_key": "secret",
  54. "door_code": "4567",
  55. }
  56. res = _parse_reservation(raw)
  57. assert isinstance(res, ReservationData)
  58. assert res.guest_name == "Alice"
  59. assert res.check_in_date == date(2026, 7, 1)
  60. assert res.check_out_date == date(2026, 7, 5)
  61. assert res.wifi_key == "secret"
  62. assert res.door_code == "4567"
  63. def test_parse_reservation_none_returns_none():
  64. assert _parse_reservation(None) is None
  65. def test_parse_reservation_missing_dates_gives_none_fields():
  66. res = _parse_reservation({"guest_name": "Bob"})
  67. assert res.guest_name == "Bob"
  68. assert res.check_in_date is None
  69. assert res.check_out_date is None
  70. # ---------------------------------------------------------------------------
  71. # async_fetch — reservation classification
  72. # ---------------------------------------------------------------------------
  73. async def test_fetch_current_today_no_next(hass: HomeAssistant):
  74. today = date.today()
  75. tomorrow = today + timedelta(days=1)
  76. response = {str(today): _reservation_slot("Alice", today, tomorrow)}
  77. session = _mock_session(200, response)
  78. entry = _make_entry()
  79. backend = RoomCalApiBackend(hass, entry)
  80. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  81. current, nxt = await backend.async_fetch()
  82. assert current is not None
  83. assert current.guest_name == "Alice"
  84. assert nxt is None
  85. async def test_fetch_no_current_next_upcoming(hass: HomeAssistant):
  86. today = date.today()
  87. future = today + timedelta(days=5)
  88. future_end = today + timedelta(days=8)
  89. response = {
  90. str(today): None,
  91. str(future): _reservation_slot("Bob", future, future_end),
  92. }
  93. session = _mock_session(200, response)
  94. backend = RoomCalApiBackend(hass, _make_entry())
  95. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  96. current, nxt = await backend.async_fetch()
  97. assert current is None
  98. assert nxt is not None
  99. assert nxt.guest_name == "Bob"
  100. async def test_fetch_both_current_and_next(hass: HomeAssistant):
  101. today = date.today()
  102. tomorrow = today + timedelta(days=1)
  103. future = today + timedelta(days=5)
  104. future_end = today + timedelta(days=8)
  105. response = {
  106. str(today): _reservation_slot("Alice", today, tomorrow),
  107. str(future): _reservation_slot("Bob", future, future_end),
  108. }
  109. session = _mock_session(200, response)
  110. backend = RoomCalApiBackend(hass, _make_entry())
  111. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  112. current, nxt = await backend.async_fetch()
  113. assert current.guest_name == "Alice"
  114. assert nxt.guest_name == "Bob"
  115. async def test_fetch_multiday_current_not_duplicated_as_next(hass: HomeAssistant):
  116. """A reservation spanning multiple days should produce one current, not also be treated as next."""
  117. today = date.today()
  118. checkout = today + timedelta(days=3)
  119. slot = _reservation_slot("Alice", today, checkout)
  120. # Same reservation appears on each day it spans
  121. response = {
  122. str(today): slot,
  123. str(today + timedelta(days=1)): slot,
  124. str(today + timedelta(days=2)): slot,
  125. }
  126. session = _mock_session(200, response)
  127. backend = RoomCalApiBackend(hass, _make_entry())
  128. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  129. current, nxt = await backend.async_fetch()
  130. assert current.guest_name == "Alice"
  131. assert nxt is None
  132. async def test_fetch_empty_response(hass: HomeAssistant):
  133. session = _mock_session(200, {})
  134. backend = RoomCalApiBackend(hass, _make_entry())
  135. with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
  136. current, nxt = await backend.async_fetch()
  137. assert current is None
  138. assert nxt is None
  139. # ---------------------------------------------------------------------------
  140. # async_fetch — error handling
  141. # ---------------------------------------------------------------------------
  142. async def test_fetch_401_raises_backend_auth_error(hass: HomeAssistant):
  143. session = _mock_session(401, {})
  144. backend = RoomCalApiBackend(hass, _make_entry())
  145. with (
  146. patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
  147. pytest.raises(BackendAuthError),
  148. ):
  149. await backend.async_fetch()
  150. async def test_fetch_403_raises_backend_auth_error(hass: HomeAssistant):
  151. session = _mock_session(403, {})
  152. backend = RoomCalApiBackend(hass, _make_entry())
  153. with (
  154. patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
  155. pytest.raises(BackendAuthError),
  156. ):
  157. await backend.async_fetch()
  158. async def test_fetch_network_error_raises_backend_connect_error(hass: HomeAssistant):
  159. session = _mock_session_raises(Exception("connection refused"))
  160. backend = RoomCalApiBackend(hass, _make_entry())
  161. with (
  162. patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
  163. pytest.raises(BackendConnectError),
  164. ):
  165. await backend.async_fetch()
  166. async def test_fetch_500_raises_backend_connect_error(hass: HomeAssistant):
  167. session = _mock_session(500, {})
  168. backend = RoomCalApiBackend(hass, _make_entry())
  169. with (
  170. patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session),
  171. pytest.raises(BackendConnectError),
  172. ):
  173. await backend.async_fetch()