"""Unit tests for sensor property logic. Bypasses HA entity setup by using object.__new__ and injecting a mock coordinator — valid because every tested property only reads self.coordinator.data. """ from datetime import date, datetime, time, timezone from unittest.mock import MagicMock import pytest from custom_components.room_cal_ha.coordinator import CoordinatorData from custom_components.room_cal_ha.models import ApiStatusData, ReservationData from custom_components.room_cal_ha.sensor import ( ApiHealthSensor, GuestNameSensor, LastUpdatedSensor, ReservationDateTimeSensor, ReservationStrSensor, ReservationTimeSensor, WiFiKeySensor, ) from custom_components.room_cal_ha.const import SLOT_ACTIVE, STORE_CURRENT, STORE_NEXT # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _coordinator( active=None, current=None, nxt=None, current_ts=None, next_ts=None, current_updated=None, next_updated=None, active_ts=None, active_updated=None, api_status=None, ): coord = MagicMock() coord.data = CoordinatorData( active=active if active is not None else (current or ReservationData()), current=current or ReservationData(), next=nxt or ReservationData(), current_last_refresh=current_ts, next_last_refresh=next_ts, current_last_updated=current_updated, next_last_updated=next_updated, active_last_refresh=active_ts, active_last_updated=active_updated, api_status=api_status, ) return coord def _sensor(cls, coordinator, slot=STORE_CURRENT, field="guest_name", **extra): """Create a sensor instance without running __init__.""" s = object.__new__(cls) s.coordinator = coordinator s._slot = slot s._field = field for k, v in extra.items(): setattr(s, k, v) return s SAMPLE = ReservationData( guest_name="Alice", check_in_date=date(2026, 7, 1), check_in_time=time(14, 0), check_out_date=date(2026, 7, 5), check_out_time=time(12, 0), wifi_ssid="MyNet", wifi_key="hunter2", door_code="0472", ) # --------------------------------------------------------------------------- # ReservationStrSensor # --------------------------------------------------------------------------- def test_str_sensor_returns_field_value(): coord = _coordinator(current=SAMPLE) s = _sensor(ReservationStrSensor, coord, field="guest_name") assert s.native_value == "Alice" def test_str_sensor_reads_next_slot(): nxt = ReservationData(guest_name="Bob") coord = _coordinator(nxt=nxt) s = _sensor(ReservationStrSensor, coord, slot=STORE_NEXT, field="guest_name") assert s.native_value == "Bob" def test_str_sensor_none_when_field_is_none(): res = ReservationData(guest_name=None) coord = _coordinator(current=res) s = _sensor(ReservationStrSensor, coord, field="guest_name") assert s.native_value is None def test_str_sensor_reads_active_slot(): """Active sensors read the live/effective record — distinct from Current, which never reflects an in-progress override.""" active = ReservationData(guest_name="Override Guest") coord = _coordinator(active=active, current=ReservationData(guest_name="Backend Guest")) s = _sensor(ReservationStrSensor, coord, slot=SLOT_ACTIVE, field="guest_name") assert s.native_value == "Override Guest" # --------------------------------------------------------------------------- # GuestNameSensor # --------------------------------------------------------------------------- def test_guest_name_single_word(): coord = _coordinator(current=ReservationData(guest_name="Alice")) s = _sensor(GuestNameSensor, coord, field="guest_name") attrs = s.extra_state_attributes assert attrs["first_name"] == "Alice" assert attrs["first_abbr"] == "A" assert attrs["last_name"] is None assert attrs["last_abbr"] is None assert attrs["public"] == "Alice" def test_guest_name_two_words(): coord = _coordinator(current=ReservationData(guest_name="ZiB Guest")) s = _sensor(GuestNameSensor, coord, field="guest_name") attrs = s.extra_state_attributes assert attrs["first_name"] == "ZiB" assert attrs["first_abbr"] == "Z" assert attrs["last_name"] == "Guest" assert attrs["last_abbr"] == "G" assert attrs["public"] == "ZiB G." def test_guest_name_three_words_last_is_final(): coord = _coordinator(current=ReservationData(guest_name="ZiB Middle Guest")) s = _sensor(GuestNameSensor, coord, field="guest_name") attrs = s.extra_state_attributes assert attrs["first_name"] == "ZiB" assert attrs["last_name"] == "Guest" assert attrs["last_abbr"] == "G" assert attrs["public"] == "ZiB G." def test_guest_name_none_returns_no_attrs(): coord = _coordinator(current=ReservationData(guest_name=None)) s = _sensor(GuestNameSensor, coord, field="guest_name") assert s.extra_state_attributes is None # --------------------------------------------------------------------------- # ReservationTimeSensor # --------------------------------------------------------------------------- def test_time_sensor_returns_iso_string(): coord = _coordinator(current=SAMPLE) s = _sensor(ReservationTimeSensor, coord, field="check_in_time") assert s.native_value == "14:00:00" def test_time_sensor_none_when_time_is_none(): res = ReservationData(check_in_time=None) coord = _coordinator(current=res) s = _sensor(ReservationTimeSensor, coord, field="check_in_time") assert s.native_value is None # --------------------------------------------------------------------------- # ReservationDateTimeSensor # --------------------------------------------------------------------------- def test_datetime_sensor_native_value_is_date(): coord = _coordinator(current=SAMPLE) s = _sensor(ReservationDateTimeSensor, coord, field="check_in_date") s._time_field = "check_in_time" assert s.native_value == date(2026, 7, 1) def test_datetime_sensor_attrs_contain_expected_keys(): coord = _coordinator(current=SAMPLE) s = _sensor(ReservationDateTimeSensor, coord, field="check_in_date") s._time_field = "check_in_time" attrs = s.extra_state_attributes assert attrs is not None for key in ("datetime", "date", "weekday", "month", "time_abbr", "hour"): assert key in attrs, f"missing key: {key}" def test_datetime_sensor_attrs_none_when_date_missing(): res = ReservationData(check_in_date=None, check_in_time=time(14, 0)) coord = _coordinator(current=res) s = _sensor(ReservationDateTimeSensor, coord, field="check_in_date") s._time_field = "check_in_time" assert s.extra_state_attributes is None def test_datetime_sensor_attrs_none_when_time_missing(): res = ReservationData(check_in_date=date(2026, 7, 1), check_in_time=None) coord = _coordinator(current=res) s = _sensor(ReservationDateTimeSensor, coord, field="check_in_date") s._time_field = "check_in_time" assert s.extra_state_attributes is None # --------------------------------------------------------------------------- # WiFiKeySensor # --------------------------------------------------------------------------- def test_wifi_key_sensor_native_value(): coord = _coordinator(current=SAMPLE) s = _sensor(WiFiKeySensor, coord, field="wifi_key") assert s.native_value == "hunter2" def test_wifi_key_sensor_qr_attribute(): coord = _coordinator(current=SAMPLE) s = _sensor(WiFiKeySensor, coord, field="wifi_key") attrs = s.extra_state_attributes assert attrs is not None assert attrs["qr"] == "WIFI:S:MyNet;T:WPA;P:hunter2;;" def test_wifi_key_sensor_qr_attribute_open_network(): """An empty-string wifi_key (open network default) should produce a nopass QR string.""" res = ReservationData(wifi_ssid="MyNet", wifi_key="") coord = _coordinator(current=res) s = _sensor(WiFiKeySensor, coord, field="wifi_key") attrs = s.extra_state_attributes assert attrs is not None assert attrs["qr"] == "WIFI:S:MyNet;T:nopass;;" def test_wifi_key_sensor_qr_attribute_escapes_special_chars(): res = ReservationData(wifi_ssid="Guest;Net,\"A\"", wifi_key="p:a\\ss;w,ord") coord = _coordinator(current=res) s = _sensor(WiFiKeySensor, coord, field="wifi_key") attrs = s.extra_state_attributes assert attrs is not None assert attrs["qr"] == 'WIFI:S:Guest\\;Net\\,\\"A\\";T:WPA;P:p\\:a\\\\ss\\;w\\,ord;;' def test_wifi_key_sensor_no_attrs_when_ssid_missing(): res = ReservationData(wifi_ssid=None, wifi_key="hunter2") coord = _coordinator(current=res) s = _sensor(WiFiKeySensor, coord, field="wifi_key") assert s.extra_state_attributes is None def test_wifi_key_sensor_no_attrs_when_key_missing(): res = ReservationData(wifi_ssid="MyNet", wifi_key=None) coord = _coordinator(current=res) s = _sensor(WiFiKeySensor, coord, field="wifi_key") assert s.extra_state_attributes is None # --------------------------------------------------------------------------- # LastUpdatedSensor # --------------------------------------------------------------------------- def test_last_updated_sensor_current(): ts = datetime(2026, 6, 22, 10, 0, tzinfo=timezone.utc) coord = _coordinator(current_updated=ts) s = _sensor(LastUpdatedSensor, coord) s._slot = STORE_CURRENT assert s.native_value == ts def test_last_updated_sensor_next(): ts = datetime(2026, 6, 22, 12, 0, tzinfo=timezone.utc) coord = _coordinator(next_updated=ts) s = _sensor(LastUpdatedSensor, coord) s._slot = STORE_NEXT assert s.native_value == ts def test_last_updated_sensor_none_before_any_change(): coord = _coordinator() s = _sensor(LastUpdatedSensor, coord) s._slot = STORE_CURRENT assert s.native_value is None def test_last_updated_sensor_exposes_last_refresh_as_attribute(): refresh_ts = datetime(2026, 6, 22, 9, 0, tzinfo=timezone.utc) updated_ts = datetime(2026, 6, 22, 10, 0, tzinfo=timezone.utc) coord = _coordinator(current_ts=refresh_ts, current_updated=updated_ts) s = _sensor(LastUpdatedSensor, coord) s._slot = STORE_CURRENT assert s.extra_state_attributes["last_refresh"] == refresh_ts def test_last_updated_sensor_last_refresh_attribute_none_when_not_yet_fetched(): coord = _coordinator() s = _sensor(LastUpdatedSensor, coord) s._slot = STORE_CURRENT assert s.extra_state_attributes["last_refresh"] is None def test_last_updated_sensor_active(): ts = datetime(2026, 6, 22, 11, 0, tzinfo=timezone.utc) coord = _coordinator(active_updated=ts) s = _sensor(LastUpdatedSensor, coord) s._slot = SLOT_ACTIVE assert s.native_value == ts def test_last_updated_sensor_active_exposes_active_last_refresh(): refresh_ts = datetime(2026, 6, 22, 9, 30, tzinfo=timezone.utc) coord = _coordinator(active_ts=refresh_ts) s = _sensor(LastUpdatedSensor, coord) s._slot = SLOT_ACTIVE assert s.extra_state_attributes["last_refresh"] == refresh_ts # --------------------------------------------------------------------------- # ApiHealthSensor # --------------------------------------------------------------------------- def _api_sensor(coordinator): s = object.__new__(ApiHealthSensor) s.coordinator = coordinator return s _REAL_CHECKS = { "caldav_connection": {"ok": True}, "calendar": {"ok": True}, "credentials": { "airbnb": {"ok": True}, "booking": {"ok": False}, }, } def test_api_health_ok(): status = ApiStatusData(status="ok", checks=_REAL_CHECKS) assert _api_sensor(_coordinator(api_status=status)).native_value == "OK" def test_api_health_degraded_on_non_ok_status(): status = ApiStatusData(status="error", checks=_REAL_CHECKS) assert _api_sensor(_coordinator(api_status=status)).native_value == "Degraded" def test_api_health_none_when_no_status(): assert _api_sensor(_coordinator(api_status=None)).native_value is None def test_api_health_attrs_flatten_checks(): from datetime import datetime ts = datetime(2026, 6, 23, 8, 0, 0) status = ApiStatusData(status="ok", checks=_REAL_CHECKS, stale=False, checked_at=ts) attrs = _api_sensor(_coordinator(api_status=status)).extra_state_attributes assert attrs["caldav_connection"] is True assert attrs["calendar"] is True assert attrs["airbnb"] is True assert attrs["booking"] is False assert attrs["stale"] is False assert attrs["checked_at"] == ts.isoformat() def test_api_health_attrs_present_with_empty_checks(): status = ApiStatusData(status="ok", checks={}, stale=True) attrs = _api_sensor(_coordinator(api_status=status)).extra_state_attributes assert attrs == {"stale": True, "checked_at": None} def test_api_health_no_attrs_when_no_status(): assert _api_sensor(_coordinator(api_status=None)).extra_state_attributes is None