test_api_integration.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. """Integration tests against a live room-cal API server.
  2. Reads config from <project_root>/api.env (two levels above code/).
  3. Skipped automatically when the URL is not configured.
  4. Run with:
  5. uv run --only-group integration pytest tests_integration/
  6. """
  7. from __future__ import annotations
  8. from datetime import date, timedelta
  9. from pathlib import Path
  10. import requests
  11. import pytest
  12. from custom_components.room_cal_ha.backends.api import _parse_reservation
  13. _ENV_FILE = Path(__file__).parents[2] / "api.env"
  14. def _read_env() -> dict[str, str]:
  15. result: dict[str, str] = {}
  16. if _ENV_FILE.exists():
  17. for line in _ENV_FILE.read_text().splitlines():
  18. line = line.strip()
  19. if "=" in line and not line.startswith("#"):
  20. k, _, v = line.partition("=")
  21. result[k.strip()] = v.strip()
  22. return result
  23. @pytest.fixture
  24. def api_cfg():
  25. env = _read_env()
  26. url = env.get("url", "").rstrip("/")
  27. if not url:
  28. pytest.skip("api.env: url not set")
  29. return {"url": url, "token": env.get("token", "")}
  30. @pytest.fixture
  31. def headers(api_cfg):
  32. return {"Authorization": f"Bearer {api_cfg['token']}"}
  33. def _reservation_url(base: str, *, days_back: int = 0, days_forward: int = 90) -> str:
  34. today = date.today()
  35. start = today - timedelta(days=days_back)
  36. end = today + timedelta(days=days_forward)
  37. return f"{base}/reservation/{start}/{end}"
  38. # ── tests ─────────────────────────────────────────────────────────────────────
  39. def test_connection(api_cfg, headers):
  40. """API responds with a non-error HTTP status."""
  41. resp = requests.get(_reservation_url(api_cfg["url"]), headers=headers, timeout=10)
  42. assert resp.status_code not in (401, 403), f"Auth rejected: HTTP {resp.status_code}"
  43. assert resp.status_code < 500, f"Server error: HTTP {resp.status_code}"
  44. def test_reservation_response_is_json(api_cfg, headers):
  45. """Reservation endpoint returns a valid JSON object."""
  46. resp = requests.get(_reservation_url(api_cfg["url"]), headers=headers, timeout=10)
  47. resp.raise_for_status()
  48. data = resp.json()
  49. assert isinstance(data, dict), "Expected a JSON object"
  50. def test_reservations_exist_in_window(api_cfg, headers):
  51. """At least one reservation exists within ±90 days of today."""
  52. resp = requests.get(
  53. _reservation_url(api_cfg["url"], days_back=90, days_forward=90),
  54. headers=headers,
  55. timeout=10,
  56. )
  57. resp.raise_for_status()
  58. data = resp.json()
  59. non_null = [v for v in data.values() if v is not None]
  60. assert len(non_null) > 0, "Expected at least one reservation in the ±90-day window"
  61. def test_parse_reservation_fields(api_cfg, headers):
  62. """_parse_reservation correctly maps raw API dicts to ReservationData."""
  63. resp = requests.get(
  64. _reservation_url(api_cfg["url"], days_back=90, days_forward=90),
  65. headers=headers,
  66. timeout=10,
  67. )
  68. resp.raise_for_status()
  69. data = resp.json()
  70. parsed = [_parse_reservation(v) for v in data.values() if v is not None]
  71. assert len(parsed) > 0, "No reservations to parse"
  72. for res in parsed:
  73. assert res is not None
  74. assert res.check_in_date is not None
  75. assert res.check_out_date is not None
  76. assert res.check_in_date <= res.check_out_date
  77. def test_invalid_token_returns_401_or_403(api_cfg):
  78. """A bad token is rejected with a 401 or 403."""
  79. bad_headers = {"Authorization": "Bearer invalid-token-xyz"}
  80. resp = requests.get(_reservation_url(api_cfg["url"]), headers=bad_headers, timeout=10)
  81. assert resp.status_code in (401, 403), f"Expected auth error, got HTTP {resp.status_code}"