|
|
@@ -40,6 +40,27 @@ def _mock_session(status: int, json_data=None):
|
|
|
return session
|
|
|
|
|
|
|
|
|
+def _mock_multi_session(responses: dict):
|
|
|
+ """Return a mock session that dispatches by URL suffix to (status, json_data) pairs."""
|
|
|
+
|
|
|
+ @asynccontextmanager
|
|
|
+ async def _get(url, *args, **kwargs):
|
|
|
+ for suffix, (status, json_data) in responses.items():
|
|
|
+ if url.endswith(suffix):
|
|
|
+ resp = MagicMock()
|
|
|
+ resp.status = status
|
|
|
+ resp.json = AsyncMock(return_value=json_data)
|
|
|
+ if status >= 400 and status not in (401, 403, 503):
|
|
|
+ resp.raise_for_status.side_effect = Exception(f"HTTP {status}")
|
|
|
+ yield resp
|
|
|
+ return
|
|
|
+ raise AssertionError(f"Unexpected URL: {url}")
|
|
|
+
|
|
|
+ session = MagicMock()
|
|
|
+ session.get = _get
|
|
|
+ return session
|
|
|
+
|
|
|
+
|
|
|
def _mock_session_raises(exc):
|
|
|
"""Return a session whose .get() call raises immediately."""
|
|
|
session = MagicMock()
|
|
|
@@ -281,3 +302,85 @@ async def test_fetch_500_raises_backend_connect_error(hass: HomeAssistant):
|
|
|
pytest.raises(BackendConnectError),
|
|
|
):
|
|
|
await backend.async_fetch()
|
|
|
+
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# async_fetch_status
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+async def test_fetch_status_ok(hass: HomeAssistant):
|
|
|
+ status_body = {
|
|
|
+ "status": "ok",
|
|
|
+ "checks": {"store_connected": {"ok": True}},
|
|
|
+ "checked_at": "2026-07-28T18:52:39.702615+00:00",
|
|
|
+ }
|
|
|
+ session = _mock_multi_session({
|
|
|
+ "/status": (200, status_body),
|
|
|
+ "/services": (200, []),
|
|
|
+ })
|
|
|
+ backend = RoomCalApiBackend(hass, _make_entry())
|
|
|
+
|
|
|
+ with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
|
|
|
+ result = await backend.async_fetch_status()
|
|
|
+
|
|
|
+ assert result.status == "ok"
|
|
|
+ assert result.stale is False
|
|
|
+
|
|
|
+
|
|
|
+async def test_fetch_status_503_returns_degraded(hass: HomeAssistant):
|
|
|
+ """A 503 with a degraded body must be parsed, not swallowed into a connect error.
|
|
|
+
|
|
|
+ This is the main signal the API Health sensor exists for: the backend service
|
|
|
+ reports a specific reason (e.g. stale Booking.com credentials) while still
|
|
|
+ returning HTTP 503, since some checks failing is considered a degraded-but-
|
|
|
+ reachable state rather than a connectivity failure.
|
|
|
+ """
|
|
|
+ status_body = {
|
|
|
+ "status": "degraded",
|
|
|
+ "checks": {
|
|
|
+ "store_connected": {"ok": True},
|
|
|
+ "credentials": {
|
|
|
+ "airbnb": {"ok": True},
|
|
|
+ "booking": {"ok": False, "reason": "Booking.com credentials are stale — run: refresh-creds"},
|
|
|
+ },
|
|
|
+ },
|
|
|
+ "checked_at": "2026-07-28T18:52:39.702615+00:00",
|
|
|
+ }
|
|
|
+ session = _mock_multi_session({
|
|
|
+ "/status": (503, status_body),
|
|
|
+ "/services": (200, []),
|
|
|
+ })
|
|
|
+ backend = RoomCalApiBackend(hass, _make_entry())
|
|
|
+
|
|
|
+ with patch("custom_components.room_cal_ha.backends.api.async_get_clientsession", return_value=session):
|
|
|
+ result = await backend.async_fetch_status()
|
|
|
+
|
|
|
+ assert result.status == "degraded"
|
|
|
+ assert result.checks["store_connected"]["ok"] is True
|
|
|
+ assert result.checks["credentials"]["booking"]["ok"] is False
|
|
|
+ assert result.checks["credentials"]["booking"]["reason"] == "Booking.com credentials are stale — run: refresh-creds"
|
|
|
+ from datetime import datetime, timezone
|
|
|
+ assert result.checked_at == datetime(2026, 7, 28, 18, 52, 39, 702615, tzinfo=timezone.utc)
|
|
|
+
|
|
|
+
|
|
|
+async def test_fetch_status_401_raises_backend_auth_error(hass: HomeAssistant):
|
|
|
+ session = _mock_multi_session({"/status": (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_status()
|
|
|
+
|
|
|
+
|
|
|
+async def test_fetch_status_500_raises_backend_connect_error(hass: HomeAssistant):
|
|
|
+ """A genuine server error (not a degraded-status body) still surfaces as a connect error."""
|
|
|
+ session = _mock_multi_session({"/status": (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_status()
|