"""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