|
|
@@ -0,0 +1,108 @@
|
|
|
+"""Unit tests for StaleSensor.
|
|
|
+
|
|
|
+Uses object.__new__ to bypass HA entity setup — valid because is_on only
|
|
|
+reads entry.options, coordinator.data timestamps, and dt_util.utcnow().
|
|
|
+"""
|
|
|
+from datetime import datetime, timedelta, timezone
|
|
|
+from unittest.mock import MagicMock, patch
|
|
|
+
|
|
|
+import pytest
|
|
|
+
|
|
|
+from custom_components.room_cal_ha.binary_sensor import StaleSensor
|
|
|
+from custom_components.room_cal_ha.coordinator import CoordinatorData
|
|
|
+from custom_components.room_cal_ha.const import (
|
|
|
+ CONF_REFRESH_INTERVAL,
|
|
|
+ CONF_SYNC_MODE,
|
|
|
+ DEFAULT_REFRESH_INTERVAL,
|
|
|
+ SYNC_MODE_CALDAV,
|
|
|
+ SYNC_MODE_MANUAL,
|
|
|
+)
|
|
|
+from custom_components.room_cal_ha.models import ReservationData
|
|
|
+
|
|
|
+
|
|
|
+_NOW = datetime(2026, 6, 22, 12, 0, 0, tzinfo=timezone.utc)
|
|
|
+
|
|
|
+
|
|
|
+def _sensor(sync_mode=SYNC_MODE_CALDAV, interval=DEFAULT_REFRESH_INTERVAL, current_ts=None, next_ts=None):
|
|
|
+ s = object.__new__(StaleSensor)
|
|
|
+ s._entry = MagicMock()
|
|
|
+ s._entry.options = {CONF_SYNC_MODE: sync_mode, CONF_REFRESH_INTERVAL: interval}
|
|
|
+ coord = MagicMock()
|
|
|
+ coord.data = CoordinatorData(
|
|
|
+ current=ReservationData(),
|
|
|
+ next=ReservationData(),
|
|
|
+ current_last_refresh=current_ts,
|
|
|
+ next_last_refresh=next_ts,
|
|
|
+ )
|
|
|
+ s.coordinator = coord
|
|
|
+ return s
|
|
|
+
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# Manual mode — always None
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+def test_stale_sensor_manual_mode_returns_none():
|
|
|
+ s = _sensor(sync_mode=SYNC_MODE_MANUAL)
|
|
|
+ with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
|
|
|
+ assert s.is_on is None
|
|
|
+
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# CalDAV mode — fresh timestamps
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+def test_stale_sensor_fresh_timestamps_returns_false():
|
|
|
+ recent = _NOW - timedelta(minutes=5)
|
|
|
+ s = _sensor(current_ts=recent, next_ts=recent, interval=15)
|
|
|
+ with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
|
|
|
+ assert s.is_on is False
|
|
|
+
|
|
|
+
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+# CalDAV mode — stale / missing timestamps
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
+
|
|
|
+def test_stale_sensor_none_current_ts_returns_true():
|
|
|
+ recent = _NOW - timedelta(minutes=5)
|
|
|
+ s = _sensor(current_ts=None, next_ts=recent, interval=15)
|
|
|
+ with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
|
|
|
+ assert s.is_on is True
|
|
|
+
|
|
|
+
|
|
|
+def test_stale_sensor_none_next_ts_returns_true():
|
|
|
+ recent = _NOW - timedelta(minutes=5)
|
|
|
+ s = _sensor(current_ts=recent, next_ts=None, interval=15)
|
|
|
+ with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
|
|
|
+ assert s.is_on is True
|
|
|
+
|
|
|
+
|
|
|
+def test_stale_sensor_both_ts_none_returns_true():
|
|
|
+ s = _sensor(current_ts=None, next_ts=None, interval=15)
|
|
|
+ with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
|
|
|
+ assert s.is_on is True
|
|
|
+
|
|
|
+
|
|
|
+def test_stale_sensor_expired_current_ts_returns_true():
|
|
|
+ # interval=15 → threshold=30 min; set current 31 min ago
|
|
|
+ stale = _NOW - timedelta(minutes=31)
|
|
|
+ fresh = _NOW - timedelta(minutes=5)
|
|
|
+ s = _sensor(current_ts=stale, next_ts=fresh, interval=15)
|
|
|
+ with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
|
|
|
+ assert s.is_on is True
|
|
|
+
|
|
|
+
|
|
|
+def test_stale_sensor_exactly_at_threshold_is_not_stale():
|
|
|
+ # Exactly 2x interval is NOT stale (uses strict >)
|
|
|
+ threshold = _NOW - timedelta(minutes=30)
|
|
|
+ s = _sensor(current_ts=threshold, next_ts=threshold, interval=15)
|
|
|
+ with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
|
|
|
+ assert s.is_on is False
|
|
|
+
|
|
|
+
|
|
|
+def test_stale_sensor_custom_interval():
|
|
|
+ # interval=5 → threshold=10 min; set both 9 min ago → fresh
|
|
|
+ recent = _NOW - timedelta(minutes=9)
|
|
|
+ s = _sensor(current_ts=recent, next_ts=recent, interval=5)
|
|
|
+ with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
|
|
|
+ assert s.is_on is False
|