"""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, UnifiAvailableSensor 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 # --------------------------------------------------------------------------- # UnifiAvailableSensor # --------------------------------------------------------------------------- def _entry(available: bool) -> MagicMock: hub = MagicMock() hub.available = available e = MagicMock() e.runtime_data = hub return e def _unifi_sensor(entries=None) -> UnifiAvailableSensor: s = object.__new__(UnifiAvailableSensor) s.hass = MagicMock() s.hass.config_entries.async_loaded_entries.return_value = entries if entries is not None else [] return s def test_unifi_sensor_no_entries_is_false(): assert _unifi_sensor([]).is_on is False def test_unifi_sensor_hub_unavailable_is_false(): assert _unifi_sensor([_entry(False)]).is_on is False def test_unifi_sensor_hub_available_is_true(): assert _unifi_sensor([_entry(True)]).is_on is True def test_unifi_sensor_first_unavailable_second_available_is_true(): assert _unifi_sensor([_entry(False), _entry(True)]).is_on is True def test_unifi_sensor_all_unavailable_is_false(): assert _unifi_sensor([_entry(False), _entry(False)]).is_on is False