diff --git a/.coveragerc b/.coveragerc index 70eae1eff5a5..d2bc1f2d2b41 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1307,8 +1307,6 @@ omit = homeassistant/components/waze_travel_time/__init__.py homeassistant/components/waze_travel_time/helpers.py homeassistant/components/waze_travel_time/sensor.py - homeassistant/components/whois/diagnostics.py - homeassistant/components/whois/sensor.py homeassistant/components/wiffi/* homeassistant/components/wirelesstag/* homeassistant/components/wolflink/__init__.py diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index e1199f35f2d6..2cbae147a780 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -72,7 +72,12 @@ def _ensure_timezone(timestamp: datetime | None) -> datetime | None: """Calculate days left until domain expires.""" if timestamp is None: return None - return timestamp.astimezone(tz=timezone.utc) + + # If timezone info isn't provided by the Whois, assume UTC. + if timestamp.tzinfo is None: + return timestamp.replace(tzinfo=timezone.utc) + + return timestamp SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( @@ -81,7 +86,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( name="Admin", icon="mdi:account-star", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=True, + entity_registry_enabled_default=False, value_fn=lambda domain: domain.admin if domain.admin else None, ), WhoisSensorEntityDescription( @@ -117,7 +122,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( name="Owner", icon="mdi:account", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=True, + entity_registry_enabled_default=False, value_fn=lambda domain: domain.owner if domain.owner else None, ), WhoisSensorEntityDescription( @@ -125,7 +130,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( name="Registrant", icon="mdi:account-edit", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=True, + entity_registry_enabled_default=False, value_fn=lambda domain: domain.registrant if domain.registrant else None, ), WhoisSensorEntityDescription( @@ -133,7 +138,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( name="Registrar", icon="mdi:store", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=True, + entity_registry_enabled_default=False, value_fn=lambda domain: domain.registrar if domain.registrar else None, ), WhoisSensorEntityDescription( @@ -141,7 +146,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( name="Reseller", icon="mdi:store", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=True, + entity_registry_enabled_default=False, value_fn=lambda domain: domain.reseller if domain.reseller else None, ), ) diff --git a/tests/components/whois/conftest.py b/tests/components/whois/conftest.py index 9a96e3436271..bbda3b101f52 100644 --- a/tests/components/whois/conftest.py +++ b/tests/components/whois/conftest.py @@ -10,6 +10,7 @@ import pytest from homeassistant.components.whois.const import DOMAIN from homeassistant.const import CONF_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry @@ -21,7 +22,7 @@ def mock_config_entry() -> MockConfigEntry: title="Home Assistant", domain=DOMAIN, data={ - CONF_DOMAIN: "Home-Assistant.io", + CONF_DOMAIN: "home-assistant.io", }, unique_id="home-assistant.io", ) @@ -53,10 +54,12 @@ def mock_whois() -> Generator[MagicMock, None, None]: domain = whois_mock.return_value domain.abuse_contact = "abuse@example.com" domain.admin = "admin@example.com" - domain.creation_date = datetime(2019, 1, 1, 0, 0, 0, 0) + domain.creation_date = datetime(2019, 1, 1, 0, 0, 0) domain.dnssec = True - domain.expiration_date = datetime(2023, 1, 1, 0, 0, 0, 0) - domain.last_updated = datetime(2022, 1, 1, 0, 0, 0, 0) + domain.expiration_date = datetime(2023, 1, 1, 0, 0, 0) + domain.last_updated = datetime( + 2022, 1, 1, 0, 0, 0, tzinfo=dt_util.get_time_zone("Europe/Amsterdam") + ) domain.name = "home-assistant.io" domain.name_servers = ["ns1.example.com", "ns2.example.com"] domain.owner = "owner@example.com" @@ -79,3 +82,13 @@ async def init_integration( await hass.async_block_till_done() return mock_config_entry + + +@pytest.fixture +def enable_all_entities() -> Generator[AsyncMock, None, None]: + """Test fixture that ensures all entities are enabled in the registry.""" + with patch( + "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", + return_value=True, + ) as mock_entity_registry_enabled_by_default: + yield mock_entity_registry_enabled_by_default diff --git a/tests/components/whois/test_diagnostics.py b/tests/components/whois/test_diagnostics.py new file mode 100644 index 000000000000..3bf6d82f6ef7 --- /dev/null +++ b/tests/components/whois/test_diagnostics.py @@ -0,0 +1,25 @@ +"""Tests for the diagnostics data provided by the Whois integration.""" +from aiohttp import ClientSession + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +): + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "creation_date": "2019-01-01T00:00:00", + "expiration_date": "2023-01-01T00:00:00", + "last_updated": "2022-01-01T00:00:00+01:00", + "status": "OK", + "statuses": ["OK"], + "dnssec": True, + } diff --git a/tests/components/whois/test_sensor.py b/tests/components/whois/test_sensor.py new file mode 100644 index 000000000000..b0e9862eb64c --- /dev/null +++ b/tests/components/whois/test_sensor.py @@ -0,0 +1,202 @@ +"""Tests for the sensors provided by the Whois integration.""" +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.whois.const import DOMAIN, SCAN_INTERVAL +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.mark.freeze_time("2022-01-01 12:00:00", tz_offset=0) +async def test_whois_sensors( + hass: HomeAssistant, + enable_all_entities: AsyncMock, + init_integration: MockConfigEntry, +) -> None: + """Test the Whois sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.home_assistant_io_admin") + entry = entity_registry.async_get("sensor.home_assistant_io_admin") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_admin" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "admin@example.com" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Admin" + assert state.attributes.get(ATTR_ICON) == "mdi:account-star" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.home_assistant_io_created") + entry = entity_registry.async_get("sensor.home_assistant_io_created") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_creation_date" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "2019-01-01T00:00:00+00:00" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Created" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.home_assistant_io_days_until_expiration") + entry = entity_registry.async_get("sensor.home_assistant_io_days_until_expiration") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_days_until_expiration" + assert entry.entity_category is None + assert state.state == "364" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "home-assistant.io Days Until Expiration" + ) + assert state.attributes.get(ATTR_ICON) == "mdi:calendar-clock" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.home_assistant_io_expires") + entry = entity_registry.async_get("sensor.home_assistant_io_expires") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_expiration_date" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "2023-01-01T00:00:00+00:00" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Expires" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.home_assistant_io_last_updated") + entry = entity_registry.async_get("sensor.home_assistant_io_last_updated") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_last_updated" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "2021-12-31T23:00:00+00:00" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last Updated" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + assert ATTR_ICON not in state.attributes + + state = hass.states.get("sensor.home_assistant_io_owner") + entry = entity_registry.async_get("sensor.home_assistant_io_owner") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_owner" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "owner@example.com" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Owner" + assert state.attributes.get(ATTR_ICON) == "mdi:account" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.home_assistant_io_registrant") + entry = entity_registry.async_get("sensor.home_assistant_io_registrant") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_registrant" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "registrant@example.com" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Registrant" + assert state.attributes.get(ATTR_ICON) == "mdi:account-edit" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.home_assistant_io_registrar") + entry = entity_registry.async_get("sensor.home_assistant_io_registrar") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_registrar" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "My Registrar" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Registrar" + assert state.attributes.get(ATTR_ICON) == "mdi:store" + assert ATTR_DEVICE_CLASS not in state.attributes + + state = hass.states.get("sensor.home_assistant_io_reseller") + entry = entity_registry.async_get("sensor.home_assistant_io_reseller") + assert entry + assert state + assert entry.unique_id == "home-assistant.io_reseller" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + assert state.state == "Top Domains, Low Prices" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Reseller" + assert state.attributes.get(ATTR_ICON) == "mdi:store" + assert ATTR_DEVICE_CLASS not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url is None + assert device_entry.entry_type == dr.DeviceEntryType.SERVICE + assert device_entry.identifiers == {(DOMAIN, "home-assistant.io")} + assert device_entry.manufacturer is None + assert device_entry.model is None + assert device_entry.name is None + assert device_entry.sw_version is None + + +@pytest.mark.parametrize( + "entity_id", + ( + "sensor.home_assistant_io_admin", + "sensor.home_assistant_io_owner", + "sensor.home_assistant_io_registrant", + "sensor.home_assistant_io_registrar", + "sensor.home_assistant_io_reseller", + ), +) +async def test_disabled_by_default_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, + entity_id: str, +) -> None: + """Test the disabled by default Whois sensors.""" + registry = er.async_get(hass) + + state = hass.states.get(entity_id) + assert state is None + + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + +@pytest.mark.parametrize( + "entity_id", + ( + "sensor.home_assistant_io_admin", + "sensor.home_assistant_io_created", + "sensor.home_assistant_io_days_until_expiration", + "sensor.home_assistant_io_expires", + "sensor.home_assistant_io_last_updated", + "sensor.home_assistant_io_owner", + "sensor.home_assistant_io_registrant", + "sensor.home_assistant_io_registrar", + "sensor.home_assistant_io_reseller", + ), +) +async def test_no_data( + hass: HomeAssistant, + mock_whois: MagicMock, + enable_all_entities: AsyncMock, + init_integration: MockConfigEntry, + entity_id: str, +) -> None: + """Test whois sensors become unknown when there is no data provided.""" + mock_whois.return_value = None + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN