| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- """Integration tests against a live CalDAV server.
- Reads credentials from <project_root>/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"
|