|
@@ -1,12 +1,13 @@
|
|
|
-"""Unit tests for _parse_event in the CalDAV backend."""
|
|
|
|
|
|
|
+"""Unit tests for the CalDAV backend: _parse_event and _fetch selection logic."""
|
|
|
import json
|
|
import json
|
|
|
-from datetime import date, time
|
|
|
|
|
-from unittest.mock import MagicMock
|
|
|
|
|
|
|
+from datetime import date, datetime, time, timedelta, timezone
|
|
|
|
|
+from unittest.mock import MagicMock, patch
|
|
|
|
|
|
|
|
import pytest
|
|
import pytest
|
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
|
|
|
|
-from custom_components.room_cal_ha.backends.caldav import _parse_event
|
|
|
|
|
|
|
+from custom_components.room_cal_ha.backends.caldav import CalDavBackend, _parse_event
|
|
|
|
|
+from custom_components.room_cal_ha.const import CONF_CALDAV_URL, CONF_CALENDAR_NAME, CONF_USERNAME, CONF_PASSWORD
|
|
|
from custom_components.room_cal_ha.models import ReservationData
|
|
from custom_components.room_cal_ha.models import ReservationData
|
|
|
|
|
|
|
|
|
|
|
|
@@ -157,3 +158,122 @@ def test_parse_event_x_data_missing_optional_fields(hass: HomeAssistant):
|
|
|
assert result.wifi_ssid is None
|
|
assert result.wifi_ssid is None
|
|
|
assert result.wifi_key is None
|
|
assert result.wifi_key is None
|
|
|
assert result.door_code is None
|
|
assert result.door_code is None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# _fetch — current/next classification with time pinning
|
|
|
|
|
+#
|
|
|
|
|
+# These guard the datetime-window selection logic against the midnight-boundary
|
|
|
|
|
+# bug where a date-only comparison would promote an upcoming guest to "current"
|
|
|
|
|
+# at midnight on their check-in day rather than at their actual check-in time.
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+def _make_caldav_entry():
|
|
|
|
|
+ entry = MagicMock()
|
|
|
|
|
+ entry.options = {CONF_CALDAV_URL: "https://cal.example.com", CONF_CALENDAR_NAME: "Room"}
|
|
|
|
|
+ entry.data = {CONF_USERNAME: "user", CONF_PASSWORD: "pass"}
|
|
|
|
|
+ return entry
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _mock_caldav_client(n_events: int, cal_name: str = "Room"):
|
|
|
|
|
+ """Return a mock DAVClient whose calendar yields n_events placeholder events."""
|
|
|
|
|
+ mock_cal = MagicMock()
|
|
|
|
|
+ mock_cal.get_display_name.return_value = cal_name
|
|
|
|
|
+ mock_cal.date_search.return_value = [MagicMock() for _ in range(n_events)]
|
|
|
|
|
+ mock_principal = MagicMock()
|
|
|
|
|
+ mock_principal.calendars.return_value = [mock_cal]
|
|
|
|
|
+ mock_client = MagicMock()
|
|
|
|
|
+ mock_client.principal.return_value = mock_principal
|
|
|
|
|
+ return mock_client
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _pin_now(dt: datetime):
|
|
|
|
|
+ """Return an aware datetime whose .replace(tzinfo=None) gives dt (naive local)."""
|
|
|
|
|
+ return dt.replace(tzinfo=timezone.utc)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _fetch_with_pinned_time(hass, reservations: list, now: datetime):
|
|
|
|
|
+ """Run CalDavBackend._fetch() with parsed events and pinned time."""
|
|
|
|
|
+ backend = CalDavBackend(hass, _make_caldav_entry())
|
|
|
|
|
+ with (
|
|
|
|
|
+ patch("custom_components.room_cal_ha.backends.caldav.caldav.DAVClient",
|
|
|
|
|
+ return_value=_mock_caldav_client(len(reservations))),
|
|
|
|
|
+ patch("custom_components.room_cal_ha.backends.caldav.dt_util.now",
|
|
|
|
|
+ return_value=_pin_now(now)),
|
|
|
|
|
+ patch("custom_components.room_cal_ha.backends.caldav._parse_event",
|
|
|
|
|
+ side_effect=reservations),
|
|
|
|
|
+ ):
|
|
|
|
|
+ return backend._fetch()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_caldav_fetch_not_promoted_before_checkin_time(hass: HomeAssistant):
|
|
|
|
|
+ """Guest checking in today at 14:00 should be 'next', not 'current', at 09:00 today.
|
|
|
|
|
+
|
|
|
|
|
+ This is the exact midnight-boundary scenario: at 00:00–13:59 on check-in day a
|
|
|
|
|
+ date-only comparison would have wrongly classified the incoming guest as current.
|
|
|
|
|
+ """
|
|
|
|
|
+ today = date(2026, 7, 1)
|
|
|
|
|
+ alice = ReservationData(
|
|
|
|
|
+ guest_name="Alice",
|
|
|
|
|
+ check_in_date=date(2026, 6, 30), check_in_time=time(14, 0),
|
|
|
|
|
+ check_out_date=today, check_out_time=time(23, 59),
|
|
|
|
|
+ )
|
|
|
|
|
+ bob = ReservationData(
|
|
|
|
|
+ guest_name="Bob",
|
|
|
|
|
+ check_in_date=today, check_in_time=time(14, 0),
|
|
|
|
|
+ check_out_date=date(2026, 7, 2), check_out_time=time(11, 0),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ current, nxt = _fetch_with_pinned_time(hass, [alice, bob], datetime(2026, 7, 1, 9, 0))
|
|
|
|
|
+
|
|
|
|
|
+ assert current is not None and current.guest_name == "Alice"
|
|
|
|
|
+ assert nxt is not None and nxt.guest_name == "Bob"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_caldav_fetch_ongoing_stay_remains_current_before_checkout_time(hass: HomeAssistant):
|
|
|
|
|
+ """A guest checking out today at 11:00 should still be current at 09:00 that morning."""
|
|
|
|
|
+ today = date(2026, 7, 1)
|
|
|
|
|
+ alice = ReservationData(
|
|
|
|
|
+ guest_name="Alice",
|
|
|
|
|
+ check_in_date=date(2026, 6, 29), check_in_time=time(14, 0),
|
|
|
|
|
+ check_out_date=today, check_out_time=time(11, 0),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ current, nxt = _fetch_with_pinned_time(hass, [alice], datetime(2026, 7, 1, 9, 0))
|
|
|
|
|
+
|
|
|
|
|
+ assert current is not None and current.guest_name == "Alice"
|
|
|
|
|
+ assert nxt is None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_caldav_fetch_stay_cleared_at_checkout_time(hass: HomeAssistant):
|
|
|
|
|
+ """Exactly at checkout time the departing guest should no longer be current."""
|
|
|
|
|
+ today = date(2026, 7, 1)
|
|
|
|
|
+ alice = ReservationData(
|
|
|
|
|
+ guest_name="Alice",
|
|
|
|
|
+ check_in_date=date(2026, 6, 29), check_in_time=time(14, 0),
|
|
|
|
|
+ check_out_date=today, check_out_time=time(11, 0),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ current, nxt = _fetch_with_pinned_time(hass, [alice], datetime(2026, 7, 1, 11, 0))
|
|
|
|
|
+
|
|
|
|
|
+ assert current is None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_caldav_fetch_no_current_when_between_stays(hass: HomeAssistant):
|
|
|
|
|
+ """Gap between two reservations: no current, next guest correctly identified."""
|
|
|
|
|
+ alice = ReservationData(
|
|
|
|
|
+ guest_name="Alice",
|
|
|
|
|
+ check_in_date=date(2026, 6, 28), check_in_time=time(14, 0),
|
|
|
|
|
+ check_out_date=date(2026, 7, 1), check_out_time=time(11, 0),
|
|
|
|
|
+ )
|
|
|
|
|
+ bob = ReservationData(
|
|
|
|
|
+ guest_name="Bob",
|
|
|
|
|
+ check_in_date=date(2026, 7, 3), check_in_time=time(14, 0),
|
|
|
|
|
+ check_out_date=date(2026, 7, 6), check_out_time=time(11, 0),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 14:00 on July 1 — Alice checked out at 11:00, Bob doesn't arrive until July 3
|
|
|
|
|
+ current, nxt = _fetch_with_pinned_time(hass, [alice, bob], datetime(2026, 7, 1, 14, 0))
|
|
|
|
|
+
|
|
|
|
|
+ assert current is None
|
|
|
|
|
+ assert nxt is not None and nxt.guest_name == "Bob"
|