"""Integration tests against a live CalDAV server. Reads credentials from /caldav.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, datetime, timedelta, timezone from pathlib import Path import caldav import pytest 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.backends.caldav import _parse_event _ENV_FILE = Path(__file__).parents[2] / "caldav.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 caldav_cfg(): env = _read_env() url = env.get("url", "") if not url: pytest.skip("caldav.env: url not set") return { "url": url, "username": env.get("user", ""), "password": env.get("pass", ""), "calendar_name": env.get("cal", "Room"), } @pytest.fixture def calendar(caldav_cfg): client = caldav.DAVClient( url=caldav_cfg["url"], username=caldav_cfg["username"], password=caldav_cfg["password"], ) principal = client.principal() for cal in principal.calendars(): if cal.get_display_name() == caldav_cfg["calendar_name"]: yield cal client.close() return client.close() pytest.skip(f"Calendar '{caldav_cfg['calendar_name']}' not found on server") # ── tests ───────────────────────────────────────────────────────────────────── def test_connection(caldav_cfg): """Verify credentials are accepted and principal resolves.""" client = caldav.DAVClient( url=caldav_cfg["url"], username=caldav_cfg["username"], password=caldav_cfg["password"], ) try: assert client.principal() is not None finally: client.close() def test_calendar_found(calendar): """Verify the target calendar exists and has a name.""" assert calendar is not None assert calendar.get_display_name() def test_parse_existing_reservation(calendar): """Fetch real Guest: events from the server and verify _parse_event parses them.""" now = datetime.now(timezone.utc) events = calendar.search( start=now - timedelta(days=30), end=now + timedelta(days=90), event=True, expand=True, ) reservations = [r for r in (_parse_event(e) for e in events) if r is not None] assert len(reservations) > 0, "Expected at least one parseable Guest: event on the calendar" res = reservations[0] assert res.guest_name is not None assert res.check_in_date is not None assert res.check_in_time is not None assert res.check_out_date is not None assert res.check_out_time is not None def test_x_data_fields_parsed_correctly(calendar): """X-ROOMCAL-DATA JSON fields must match what _parse_event returns.""" import json as _json now = datetime.now(timezone.utc) events = calendar.search( start=now - timedelta(days=30), end=now + timedelta(days=90), event=True, expand=True, ) checked = 0 for e in events: cal = iCal.from_ical(e.data) for comp in cal.walk(): if comp.name != "VEVENT": continue if not str(comp.get("SUMMARY", "")).startswith(RESERVATION_TITLE_PREFIX): continue raw = comp.get(X_DATA_PROP) if not raw: continue x_data = _json.loads(str(raw)) res = _parse_event(e) assert res is not None assert res.guest_name == x_data.get("guest_name") assert res.wifi_key == x_data.get("wifi_key") assert res.door_code == x_data.get("door_code") checked += 1 assert checked > 0, "No Guest: events with X-ROOMCAL-DATA found to verify" def test_event_without_prefix_is_ignored(calendar): """Existing non-Guest events on the server must not be parsed as reservations.""" now = datetime.now(timezone.utc) events = calendar.search( start=now - timedelta(days=30), end=now + timedelta(days=90), event=True, expand=True, ) non_guest = [] for e in events: cal = iCal.from_ical(e.data) for comp in cal.walk(): if comp.name == "VEVENT": summary = str(comp.get("SUMMARY", "")) if not summary.startswith(RESERVATION_TITLE_PREFIX): non_guest.append(e) break if not non_guest: pytest.skip("No non-Guest events found in the search window to test against") for e in non_guest: assert _parse_event(e) is None, "Expected None for non-Guest event" def test_all_day_event_is_ignored(calendar): """All-day events (date DTSTART) on the server must return None from _parse_event.""" now = datetime.now(timezone.utc) events = calendar.search( start=now - timedelta(days=30), end=now + timedelta(days=90), event=True, expand=False, ) all_day = [] for e in events: cal = iCal.from_ical(e.data) for comp in cal.walk(): if comp.name == "VEVENT": dtstart = comp.get("DTSTART") if dtstart is not None and isinstance(dtstart.dt, date) and not isinstance(dtstart.dt, datetime): all_day.append(e) break if not all_day: pytest.skip("No all-day events found in the search window to test against") for e in all_day: assert _parse_event(e) is None, "Expected None for all-day event"