|
|
@@ -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}"
|