From ba2e80f741100ee1b3e74589d46793e75cefa665 Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Mon, 20 Feb 2023 03:51:01 -0500 Subject: [PATCH] Add more tests for APC UPS Daemon integration (#85967) * Add tests for init. * Add more test init. * Fix test init side_effect. * Add test sensor. * Fix sensor test file name. * Fix sensor test. * Add binary sensor test. * Fix comments and styling. * Remove apcupsd from omissions in coveragerc. * Add a test case for binary sensor when STATFLAG is not available. * Complete type annotations for test files. * Revert "Remove apcupsd from omissions in coveragerc." This reverts commit 66b05fcb8829619a771a650a3d70174089e15d91. --- tests/components/apcupsd/__init__.py | 33 ++++++ .../components/apcupsd/test_binary_sensor.py | 28 +++++ tests/components/apcupsd/test_init.py | 101 ++++++++++++++++++ tests/components/apcupsd/test_sensor.py | 98 +++++++++++++++++ 4 files changed, 260 insertions(+) create mode 100644 tests/components/apcupsd/test_binary_sensor.py create mode 100644 tests/components/apcupsd/test_init.py create mode 100644 tests/components/apcupsd/test_sensor.py diff --git a/tests/components/apcupsd/__init__.py b/tests/components/apcupsd/__init__.py index f6df277b37e..64547822b42 100644 --- a/tests/components/apcupsd/__init__.py +++ b/tests/components/apcupsd/__init__.py @@ -1,8 +1,14 @@ """Tests for the APCUPSd component.""" from collections import OrderedDict from typing import Final +from unittest.mock import patch +from homeassistant.components.apcupsd import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry CONF_DATA: Final = {CONF_HOST: "test", CONF_PORT: 1234} @@ -64,3 +70,30 @@ MOCK_MINIMAL_STATUS: Final = OrderedDict( ("END APC", "1970-01-01 00:00:00 0000"), ] ) + + +async def init_integration( + hass: HomeAssistant, host: str = "test", status=None +) -> MockConfigEntry: + """Set up the APC UPS Daemon integration in HomeAssistant.""" + if status is None: + status = MOCK_STATUS + + entry = MockConfigEntry( + version=1, + domain=DOMAIN, + title="APCUPSd", + data=CONF_DATA | {CONF_HOST: host}, + unique_id=status.get("SERIALNO", None), + source=SOURCE_USER, + ) + + entry.add_to_hass(hass) + + with patch("apcaccess.status.parse", return_value=status), patch( + "apcaccess.status.get", return_value=b"" + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py new file mode 100644 index 00000000000..c00707b7ff1 --- /dev/null +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -0,0 +1,28 @@ +"""Test binary sensors of APCUPSd integration.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MOCK_STATUS, init_integration + + +async def test_binary_sensor(hass: HomeAssistant) -> None: + """Test states of binary sensor.""" + await init_integration(hass, status=MOCK_STATUS) + registry = er.async_get(hass) + + state = hass.states.get("binary_sensor.ups_online_status") + assert state + assert state.state == "on" + entry = registry.async_get("binary_sensor.ups_online_status") + assert entry + assert entry.unique_id == "XXXXXXXXXXXX_statflag" + + +async def test_no_binary_sensor(hass: HomeAssistant) -> None: + """Test binary sensor when STATFLAG is not available.""" + status = MOCK_STATUS.copy() + status.pop("STATFLAG") + await init_integration(hass, status=status) + + state = hass.states.get("binary_sensor.ups_online_status") + assert state is None diff --git a/tests/components/apcupsd/test_init.py b/tests/components/apcupsd/test_init.py new file mode 100644 index 00000000000..8f2e7901c52 --- /dev/null +++ b/tests/components/apcupsd/test_init.py @@ -0,0 +1,101 @@ +"""Test init of APCUPSd integration.""" +from collections import OrderedDict +from unittest.mock import patch + +import pytest + +from homeassistant.components.apcupsd import DOMAIN, APCUPSdData +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, init_integration + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize("status", (MOCK_STATUS, MOCK_MINIMAL_STATUS)) +async def test_async_setup_entry(hass: HomeAssistant, status: OrderedDict) -> None: + """Test a successful setup entry.""" + # Minimal status does not contain "SERIALNO" field, which is used to determine the + # unique ID of this integration. But, the integration should work fine without it. + await init_integration(hass, status=status) + + # Verify successful setup by querying the status sensor. + state = hass.states.get("binary_sensor.ups_online_status") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "on" + + +async def test_multiple_integrations(hass: HomeAssistant) -> None: + """Test successful setup for multiple entries.""" + # Load two integrations from two mock hosts. + entries = ( + await init_integration(hass, host="test1", status=MOCK_STATUS), + await init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS), + ) + + # Data dict should contain different API objects. + assert len(hass.data[DOMAIN]) == len(entries) + for entry in entries: + assert entry.entry_id in hass.data[DOMAIN] + assert isinstance(hass.data[DOMAIN][entry.entry_id], APCUPSdData) + + assert ( + hass.data[DOMAIN][entries[0].entry_id] != hass.data[DOMAIN][entries[1].entry_id] + ) + + +async def test_connection_error(hass: HomeAssistant) -> None: + """Test connection error during integration setup.""" + entry = MockConfigEntry( + version=1, + domain=DOMAIN, + title="APCUPSd", + data=CONF_DATA, + source=SOURCE_USER, + ) + + entry.add_to_hass(hass) + + with patch("apcaccess.status.parse", side_effect=OSError()), patch( + "apcaccess.status.get" + ): + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_unload_remove(hass): + """Test successful unload of entry.""" + # Load two integrations from two mock hosts. + entries = ( + await init_integration(hass, host="test1", status=MOCK_STATUS), + await init_integration(hass, host="test2", status=MOCK_MINIMAL_STATUS), + ) + + # Assert they are loaded. + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + assert all(entry.state is ConfigEntryState.LOADED for entry in entries) + + # Unload the first entry. + assert await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + assert entries[0].state is ConfigEntryState.NOT_LOADED + assert entries[1].state is ConfigEntryState.LOADED + assert len(hass.data[DOMAIN]) == 1 + + # Unload the second entry. + assert await hass.config_entries.async_unload(entries[1].entry_id) + await hass.async_block_till_done() + assert all(entry.state is ConfigEntryState.NOT_LOADED for entry in entries) + + # We should never leave any garbage in the data dict. + assert len(hass.data[DOMAIN]) == 0 + + # Remove both entries. + for entry in entries: + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get(entry.entry_id) + assert state is None diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py new file mode 100644 index 00000000000..bf797db7dbe --- /dev/null +++ b/tests/components/apcupsd/test_sensor.py @@ -0,0 +1,98 @@ +"""Test sensors of APCUPSd integration.""" +import pytest + +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, + UnitOfElectricPotential, + UnitOfPower, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MOCK_STATUS, init_integration + + +async def test_sensor(hass: HomeAssistant) -> None: + """Test states of sensor.""" + await init_integration(hass, status=MOCK_STATUS) + registry = er.async_get(hass) + + # Test a representative string sensor. + state = hass.states.get("sensor.ups_mode") + assert state + assert state.state == "Stand Alone" + entry = registry.async_get("sensor.ups_mode") + assert entry + assert entry.unique_id == "XXXXXXXXXXXX_upsmode" + + # Test two representative voltage sensors. + state = hass.states.get("sensor.ups_input_voltage") + assert state + assert pytest.approx(float(state.state)) == 124.0 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE + entry = registry.async_get("sensor.ups_input_voltage") + assert entry + assert entry.unique_id == "XXXXXXXXXXXX_linev" + + state = hass.states.get("sensor.ups_battery_voltage") + assert state + assert pytest.approx(float(state.state)) == 13.7 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE + entry = registry.async_get("sensor.ups_battery_voltage") + assert entry + assert entry.unique_id == "XXXXXXXXXXXX_battv" + + # Test a representative percentage sensor. + state = hass.states.get("sensor.ups_load") + assert state + assert pytest.approx(float(state.state)) == 14.0 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + entry = registry.async_get("sensor.ups_load") + assert entry + assert entry.unique_id == "XXXXXXXXXXXX_loadpct" + + # Test a representative wattage sensor. + state = hass.states.get("sensor.ups_nominal_output_power") + assert state + assert pytest.approx(float(state.state)) == 330.0 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + entry = registry.async_get("sensor.ups_nominal_output_power") + assert entry + assert entry.unique_id == "XXXXXXXXXXXX_nompower" + + +async def test_sensor_disabled(hass: HomeAssistant) -> None: + """Test sensor disabled by default.""" + await init_integration(hass) + registry = er.async_get(hass) + + # Test a representative integration-disabled sensor. + entry = registry.async_get("sensor.ups_model") + assert entry.disabled + assert entry.unique_id == "XXXXXXXXXXXX_model" + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Test enabling entity. + updated_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + + assert updated_entry != entry + assert updated_entry.disabled is False