"""Integration tests against a live room-cal API server. Reads config from /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}"