Selaa lähdekoodia

Isolate integration tests from HA test plugin restrictions

Disable pytest-homeassistant-custom-component and pytest-socket plugins
in tests_integration/pytest.ini so the integration environment never
needs to undo their DNS/socket blocks. Drop the _allow_real_network
fixture from conftest; add homeassistant.helpers.aiohttp_client to the
HA stub list. Fix stale import: _parse_event moved to backends/caldav.
ZiB 4 kuukautta sitten
vanhempi
sitoutus
100e6b9b03

+ 1 - 3
tests_integration/conftest.py

@@ -6,11 +6,8 @@ caldav, icalendar, and pytest).
 """
 """
 import sys
 import sys
 from pathlib import Path
 from pathlib import Path
-from types import ModuleType
 from unittest.mock import MagicMock
 from unittest.mock import MagicMock
 
 
-import pytest
-
 sys.path.insert(0, str(Path(__file__).parents[1]))
 sys.path.insert(0, str(Path(__file__).parents[1]))
 
 
 _HA_MODULES = [
 _HA_MODULES = [
@@ -20,6 +17,7 @@ _HA_MODULES = [
   "homeassistant.const",
   "homeassistant.const",
   "homeassistant.core",
   "homeassistant.core",
   "homeassistant.helpers",
   "homeassistant.helpers",
+  "homeassistant.helpers.aiohttp_client",
   "homeassistant.helpers.config_validation",
   "homeassistant.helpers.config_validation",
   "homeassistant.helpers.entity",
   "homeassistant.helpers.entity",
   "homeassistant.helpers.event",
   "homeassistant.helpers.event",

+ 1 - 0
tests_integration/pytest.ini

@@ -1,2 +1,3 @@
 [pytest]
 [pytest]
 asyncio_mode = auto
 asyncio_mode = auto
+addopts = -p no:homeassistant -p no:socket

+ 108 - 0
tests_integration/test_api_integration.py

@@ -0,0 +1,108 @@
+"""Integration tests against a live room-cal API server.
+
+Reads config from <project_root>/api.env (two levels above code/).
+Skipped automatically when the URL is not configured.
+
+Run with:
+    uv run --only-group integration pytest tests_integration/
+"""
+from __future__ import annotations
+
+from datetime import date, timedelta
+from pathlib import Path
+
+import requests
+import pytest
+
+from custom_components.room_cal_ha.backends.api import _parse_reservation
+
+_ENV_FILE = Path(__file__).parents[2] / "api.env"
+
+
+def _read_env() -> dict[str, str]:
+  result: dict[str, str] = {}
+  if _ENV_FILE.exists():
+    for line in _ENV_FILE.read_text().splitlines():
+      line = line.strip()
+      if "=" in line and not line.startswith("#"):
+        k, _, v = line.partition("=")
+        result[k.strip()] = v.strip()
+  return result
+
+
+@pytest.fixture
+def api_cfg():
+  env = _read_env()
+  url = env.get("url", "").rstrip("/")
+  if not url:
+    pytest.skip("api.env: url not set")
+  return {"url": url, "token": env.get("token", "")}
+
+
+@pytest.fixture
+def headers(api_cfg):
+  return {"Authorization": f"Bearer {api_cfg['token']}"}
+
+
+def _reservation_url(base: str, *, days_back: int = 0, days_forward: int = 90) -> str:
+  today = date.today()
+  start = today - timedelta(days=days_back)
+  end = today + timedelta(days=days_forward)
+  return f"{base}/reservation/{start}/{end}"
+
+
+# ── tests ─────────────────────────────────────────────────────────────────────
+
+def test_connection(api_cfg, headers):
+  """API responds with a non-error HTTP status."""
+  resp = requests.get(_reservation_url(api_cfg["url"]), headers=headers, timeout=10)
+  assert resp.status_code not in (401, 403), f"Auth rejected: HTTP {resp.status_code}"
+  assert resp.status_code < 500, f"Server error: HTTP {resp.status_code}"
+
+
+def test_reservation_response_is_json(api_cfg, headers):
+  """Reservation endpoint returns a valid JSON object."""
+  resp = requests.get(_reservation_url(api_cfg["url"]), headers=headers, timeout=10)
+  resp.raise_for_status()
+  data = resp.json()
+  assert isinstance(data, dict), "Expected a JSON object"
+
+
+def test_reservations_exist_in_window(api_cfg, headers):
+  """At least one reservation exists within ±90 days of today."""
+  resp = requests.get(
+    _reservation_url(api_cfg["url"], days_back=90, days_forward=90),
+    headers=headers,
+    timeout=10,
+  )
+  resp.raise_for_status()
+  data = resp.json()
+  non_null = [v for v in data.values() if v is not None]
+  assert len(non_null) > 0, "Expected at least one reservation in the ±90-day window"
+
+
+def test_parse_reservation_fields(api_cfg, headers):
+  """_parse_reservation correctly maps raw API dicts to ReservationData."""
+  resp = requests.get(
+    _reservation_url(api_cfg["url"], days_back=90, days_forward=90),
+    headers=headers,
+    timeout=10,
+  )
+  resp.raise_for_status()
+  data = resp.json()
+
+  parsed = [_parse_reservation(v) for v in data.values() if v is not None]
+  assert len(parsed) > 0, "No reservations to parse"
+
+  for res in parsed:
+    assert res is not None
+    assert res.check_in_date is not None
+    assert res.check_out_date is not None
+    assert res.check_in_date <= res.check_out_date
+
+
+def test_invalid_token_returns_401_or_403(api_cfg):
+  """A bad token is rejected with a 401 or 403."""
+  bad_headers = {"Authorization": "Bearer invalid-token-xyz"}
+  resp = requests.get(_reservation_url(api_cfg["url"]), headers=bad_headers, timeout=10)
+  assert resp.status_code in (401, 403), f"Expected auth error, got HTTP {resp.status_code}"

+ 1 - 1
tests_integration/test_caldav_integration.py

@@ -16,7 +16,7 @@ import pytest
 from icalendar import Calendar as iCal
 from icalendar import Calendar as iCal
 
 
 from custom_components.room_cal_ha.const import RESERVATION_TITLE_PREFIX, X_DATA_PROP
 from custom_components.room_cal_ha.const import RESERVATION_TITLE_PREFIX, X_DATA_PROP
-from custom_components.room_cal_ha.coordinator import _parse_event
+from custom_components.room_cal_ha.backends.caldav import _parse_event
 
 
 _ENV_FILE = Path(__file__).parents[2] / "caldav.env"
 _ENV_FILE = Path(__file__).parents[2] / "caldav.env"