test_binary_sensor.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. """Unit tests for StaleSensor.
  2. Uses object.__new__ to bypass HA entity setup — valid because is_on only
  3. reads entry.options, coordinator.data timestamps, and dt_util.utcnow().
  4. """
  5. from datetime import datetime, timedelta, timezone
  6. from unittest.mock import MagicMock, patch
  7. import pytest
  8. from custom_components.room_cal_ha.binary_sensor import StaleSensor
  9. from custom_components.room_cal_ha.coordinator import CoordinatorData
  10. from custom_components.room_cal_ha.const import (
  11. CONF_REFRESH_INTERVAL,
  12. CONF_SYNC_MODE,
  13. DEFAULT_REFRESH_INTERVAL,
  14. SYNC_MODE_CALDAV,
  15. SYNC_MODE_MANUAL,
  16. )
  17. from custom_components.room_cal_ha.models import ReservationData
  18. _NOW = datetime(2026, 6, 22, 12, 0, 0, tzinfo=timezone.utc)
  19. def _sensor(sync_mode=SYNC_MODE_CALDAV, interval=DEFAULT_REFRESH_INTERVAL, current_ts=None, next_ts=None):
  20. s = object.__new__(StaleSensor)
  21. s._entry = MagicMock()
  22. s._entry.options = {CONF_SYNC_MODE: sync_mode, CONF_REFRESH_INTERVAL: interval}
  23. coord = MagicMock()
  24. coord.data = CoordinatorData(
  25. current=ReservationData(),
  26. next=ReservationData(),
  27. current_last_refresh=current_ts,
  28. next_last_refresh=next_ts,
  29. )
  30. s.coordinator = coord
  31. return s
  32. # ---------------------------------------------------------------------------
  33. # Manual mode — always None
  34. # ---------------------------------------------------------------------------
  35. def test_stale_sensor_manual_mode_returns_none():
  36. s = _sensor(sync_mode=SYNC_MODE_MANUAL)
  37. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  38. assert s.is_on is None
  39. # ---------------------------------------------------------------------------
  40. # CalDAV mode — fresh timestamps
  41. # ---------------------------------------------------------------------------
  42. def test_stale_sensor_fresh_timestamps_returns_false():
  43. recent = _NOW - timedelta(minutes=5)
  44. s = _sensor(current_ts=recent, next_ts=recent, interval=15)
  45. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  46. assert s.is_on is False
  47. # ---------------------------------------------------------------------------
  48. # CalDAV mode — stale / missing timestamps
  49. # ---------------------------------------------------------------------------
  50. def test_stale_sensor_none_current_ts_returns_true():
  51. recent = _NOW - timedelta(minutes=5)
  52. s = _sensor(current_ts=None, next_ts=recent, interval=15)
  53. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  54. assert s.is_on is True
  55. def test_stale_sensor_none_next_ts_returns_true():
  56. recent = _NOW - timedelta(minutes=5)
  57. s = _sensor(current_ts=recent, next_ts=None, interval=15)
  58. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  59. assert s.is_on is True
  60. def test_stale_sensor_both_ts_none_returns_true():
  61. s = _sensor(current_ts=None, next_ts=None, interval=15)
  62. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  63. assert s.is_on is True
  64. def test_stale_sensor_expired_current_ts_returns_true():
  65. # interval=15 → threshold=30 min; set current 31 min ago
  66. stale = _NOW - timedelta(minutes=31)
  67. fresh = _NOW - timedelta(minutes=5)
  68. s = _sensor(current_ts=stale, next_ts=fresh, interval=15)
  69. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  70. assert s.is_on is True
  71. def test_stale_sensor_exactly_at_threshold_is_not_stale():
  72. # Exactly 2x interval is NOT stale (uses strict >)
  73. threshold = _NOW - timedelta(minutes=30)
  74. s = _sensor(current_ts=threshold, next_ts=threshold, interval=15)
  75. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  76. assert s.is_on is False
  77. def test_stale_sensor_custom_interval():
  78. # interval=5 → threshold=10 min; set both 9 min ago → fresh
  79. recent = _NOW - timedelta(minutes=9)
  80. s = _sensor(current_ts=recent, next_ts=recent, interval=5)
  81. with patch("custom_components.room_cal_ha.binary_sensor.dt_util.utcnow", return_value=_NOW):
  82. assert s.is_on is False