From ffcd5167b50fe3cac3ad855973b14d372929bc2b Mon Sep 17 00:00:00 2001 From: Yuxin Wang Date: Thu, 28 Aug 2025 10:11:31 -0400 Subject: [PATCH] Use fixtures instead of helper functions for APCUPSD tests (#151172) Co-authored-by: Franck Nijhof --- homeassistant/components/apcupsd/__init__.py | 2 +- tests/components/apcupsd/__init__.py | 37 -- tests/components/apcupsd/conftest.py | 68 +++- .../apcupsd/snapshots/test_init.ambr | 8 +- .../components/apcupsd/test_binary_sensor.py | 63 ++-- tests/components/apcupsd/test_config_flow.py | 326 ++++++++---------- tests/components/apcupsd/test_diagnostics.py | 6 +- tests/components/apcupsd/test_init.py | 108 +++--- tests/components/apcupsd/test_sensor.py | 145 ++++---- 9 files changed, 381 insertions(+), 382 deletions(-) diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index e444f1cd735..7526d605c59 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator -PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR) +PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry( diff --git a/tests/components/apcupsd/__init__.py b/tests/components/apcupsd/__init__.py index 2a786925e70..0efeac0e45c 100644 --- a/tests/components/apcupsd/__init__.py +++ b/tests/components/apcupsd/__init__.py @@ -4,15 +4,8 @@ from __future__ import annotations from collections import OrderedDict from typing import Final -from unittest.mock import patch -from homeassistant.components.apcupsd.const import DOMAIN -from homeassistant.components.apcupsd.coordinator import APCUPSdData -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} @@ -79,33 +72,3 @@ MOCK_MINIMAL_STATUS: Final = OrderedDict( ("END APC", "1970-01-01 00:00:00 0000"), ] ) - - -async def async_init_integration( - hass: HomeAssistant, - *, - host: str = "test", - status: dict[str, str] | None = None, - entry_id: str = "mocked-config-entry-id", -) -> MockConfigEntry: - """Set up the APC UPS Daemon integration in HomeAssistant.""" - if status is None: - status = MOCK_STATUS - - entry = MockConfigEntry( - entry_id=entry_id, - version=1, - domain=DOMAIN, - title="APCUPSd", - data=CONF_DATA | {CONF_HOST: host}, - unique_id=APCUPSdData(status).serial_no, - source=SOURCE_USER, - ) - - entry.add_to_hass(hass) - - with patch("aioapcaccess.request_status", return_value=status): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - return entry diff --git a/tests/components/apcupsd/conftest.py b/tests/components/apcupsd/conftest.py index 533694fdb1f..300613147cd 100644 --- a/tests/components/apcupsd/conftest.py +++ b/tests/components/apcupsd/conftest.py @@ -1,10 +1,21 @@ """Common fixtures for the APC UPS Daemon (APCUPSD) tests.""" -from collections.abc import Generator +from collections.abc import AsyncGenerator, Generator from unittest.mock import AsyncMock, patch import pytest +from homeassistant.components.apcupsd import PLATFORMS +from homeassistant.components.apcupsd.const import DOMAIN +from homeassistant.components.apcupsd.coordinator import APCUPSdData +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import CONF_DATA, MOCK_STATUS + +from tests.common import MockConfigEntry + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -13,3 +24,58 @@ def mock_setup_entry() -> Generator[AsyncMock]: "homeassistant.components.apcupsd.async_setup_entry", return_value=True ) as mock_setup_entry: yield mock_setup_entry + + +@pytest.fixture +async def mock_request_status( + request: pytest.FixtureRequest, +) -> AsyncGenerator[AsyncMock]: + """Return a mocked aioapcaccess.request_status function.""" + mocked_status = getattr(request, "param", None) or MOCK_STATUS + + with patch("aioapcaccess.request_status") as mock_request_status: + mock_request_status.return_value = mocked_status + yield mock_request_status + + +@pytest.fixture +def mock_config_entry( + request: pytest.FixtureRequest, + mock_request_status: AsyncMock, +) -> MockConfigEntry: + """Mock setting up a config entry.""" + entry_id = getattr(request, "param", None) + + return MockConfigEntry( + entry_id=entry_id, + version=1, + domain=DOMAIN, + title="APC UPS Daemon", + data=CONF_DATA, + unique_id=APCUPSdData(mock_request_status.return_value).serial_no, + source=SOURCE_USER, + ) + + +@pytest.fixture +def platforms() -> list[Platform]: + """Fixture to specify platforms to test.""" + return PLATFORMS + + +@pytest.fixture +async def init_integration( + request: pytest.FixtureRequest, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_request_status: AsyncMock, + platforms: list[Platform], +) -> MockConfigEntry: + """Set up APC UPS Daemon integration for testing.""" + mock_config_entry.add_to_hass(hass) + + with patch("homeassistant.components.apcupsd.PLATFORMS", platforms): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/apcupsd/snapshots/test_init.ambr b/tests/components/apcupsd/snapshots/test_init.ambr index 414c3e451fd..17c3ed0b797 100644 --- a/tests/components/apcupsd/snapshots/test_init.ambr +++ b/tests/components/apcupsd/snapshots/test_init.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_async_setup_entry[status0][device_MyUPS_XXXXXXXXXXXX] +# name: test_async_setup_entry[mock_request_status0-mocked-config-entry-id][device_MyUPS_XXXXXXXXXXXX] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -30,7 +30,7 @@ 'via_device_id': None, }) # --- -# name: test_async_setup_entry[status1][device_APC UPS_XXXX] +# name: test_async_setup_entry[mock_request_status1-mocked-config-entry-id][device_APC UPS_XXXX] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -61,7 +61,7 @@ 'via_device_id': None, }) # --- -# name: test_async_setup_entry[status2][device_APC UPS_] +# name: test_async_setup_entry[mock_request_status2-mocked-config-entry-id][device_APC UPS_] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , @@ -92,7 +92,7 @@ 'via_device_id': None, }) # --- -# name: test_async_setup_entry[status3][device_APC UPS_Blank] +# name: test_async_setup_entry[mock_request_status3-mocked-config-entry-id][device_APC UPS_Blank] DeviceRegistryEntrySnapshot({ 'area_id': None, 'config_entries': , diff --git a/tests/components/apcupsd/test_binary_sensor.py b/tests/components/apcupsd/test_binary_sensor.py index 0bf1c00d2f3..5f3493e172b 100644 --- a/tests/components/apcupsd/test_binary_sensor.py +++ b/tests/components/apcupsd/test_binary_sensor.py @@ -1,6 +1,6 @@ """Test binary sensors of APCUPSd integration.""" -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest from syrupy.assertion import SnapshotAssertion @@ -10,47 +10,60 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import slugify -from . import MOCK_STATUS, async_init_integration +from . import MOCK_STATUS -from tests.common import snapshot_platform +from tests.common import MockConfigEntry, snapshot_platform + +pytestmark = pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "init_integration" +) + + +@pytest.fixture +def platforms() -> list[Platform]: + """Overridden fixture to specify platforms to test.""" + return [Platform.BINARY_SENSOR] async def test_binary_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: - """Test states of binary sensors.""" - with patch("homeassistant.components.apcupsd.PLATFORMS", [Platform.BINARY_SENSOR]): - config_entry = await async_init_integration(hass, status=MOCK_STATUS) - await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + """Test states of binary sensor entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_no_binary_sensor(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "mock_request_status", + [{k: v for k, v in MOCK_STATUS.items() if k != "STATFLAG"}], + indirect=True, +) +async def test_no_binary_sensor( + hass: HomeAssistant, + mock_request_status: AsyncMock, +) -> None: """Test binary sensor when STATFLAG is not available.""" - status = MOCK_STATUS.copy() - status.pop("STATFLAG") - await async_init_integration(hass, status=status) - - device_slug = slugify(MOCK_STATUS["UPSNAME"]) + device_slug = slugify(mock_request_status.return_value["UPSNAME"]) state = hass.states.get(f"binary_sensor.{device_slug}_online_status") assert state is None @pytest.mark.parametrize( - ("override", "expected"), + ("mock_request_status", "expected"), [ - ("0x008", "on"), - ("0x02040010 Status Flag", "off"), + (MOCK_STATUS | {"STATFLAG": "0x008"}, "on"), + (MOCK_STATUS | {"STATFLAG": "0x02040010 Status Flag"}, "off"), ], + indirect=["mock_request_status"], ) -async def test_statflag(hass: HomeAssistant, override: str, expected: str) -> None: +async def test_statflag( + hass: HomeAssistant, + mock_request_status: AsyncMock, + expected: str, +) -> None: """Test binary sensor for different STATFLAG values.""" - status = MOCK_STATUS.copy() - status["STATFLAG"] = override - await async_init_integration(hass, status=status) - - device_slug = slugify(MOCK_STATUS["UPSNAME"]) - assert ( - hass.states.get(f"binary_sensor.{device_slug}_online_status").state == expected - ) + device_slug = slugify(mock_request_status.return_value["UPSNAME"]) + state = hass.states.get(f"binary_sensor.{device_slug}_online_status") + assert state.state == expected diff --git a/tests/components/apcupsd/test_config_flow.py b/tests/components/apcupsd/test_config_flow.py index 0a61d8c0ddb..f33b1472c92 100644 --- a/tests/components/apcupsd/test_config_flow.py +++ b/tests/components/apcupsd/test_config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock import pytest @@ -23,248 +23,202 @@ from tests.common import MockConfigEntry [OSError(), asyncio.IncompleteReadError(partial=b"", expected=100), TimeoutError()], ) async def test_config_flow_cannot_connect( - hass: HomeAssistant, exception: Exception + hass: HomeAssistant, + exception: Exception, + mock_request_status: AsyncMock, ) -> None: """Test config flow setup with a connection error.""" - with patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" - ) as mock_request_status: - mock_request_status.side_effect = exception + mock_request_status.side_effect = exception - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=CONF_DATA, - ) - assert result["type"] is FlowResultType.FORM - assert result["errors"]["base"] == "cannot_connect" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"]["base"] == "cannot_connect" async def test_config_flow_duplicate_host_port( - hass: HomeAssistant, mock_setup_entry: AsyncMock + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_request_status: AsyncMock, ) -> None: """Test duplicate config flow setup with the same host / port.""" - # First add an existing config entry to hass. - mock_entry = MockConfigEntry( - version=1, - domain=DOMAIN, - title="APCUPSd", - data=CONF_DATA, - unique_id=MOCK_STATUS["SERIALNO"], - source=SOURCE_USER, + mock_config_entry.add_to_hass(hass) + + # Assign the same host and port, which we should reject since the entry already exists. + mock_request_status.return_value = MOCK_STATUS + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - mock_entry.add_to_hass(hass) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" - with patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" - ) as mock_request_status: - # Assign the same host and port, which we should reject since the entry already exists. - mock_request_status.return_value = MOCK_STATUS - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - # Now we change the host with a different serial number and add it again. This should be successful. - another_host = CONF_DATA | {CONF_HOST: "another_host"} - mock_request_status.return_value = MOCK_STATUS | { - "SERIALNO": MOCK_STATUS["SERIALNO"] + "ZZZ" - } - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=another_host, - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == another_host + # Now we change the host with a different serial number and add it again. This should be successful. + another_host = CONF_DATA | {CONF_HOST: "another_host"} + mock_request_status.return_value = MOCK_STATUS | { + "SERIALNO": MOCK_STATUS["SERIALNO"] + "ZZZ" + } + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=another_host, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == another_host async def test_config_flow_duplicate_serial_number( - hass: HomeAssistant, mock_setup_entry: AsyncMock + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_request_status: AsyncMock, ) -> None: """Test duplicate config flow setup with different host but the same serial number.""" - # First add an existing config entry to hass. - mock_entry = MockConfigEntry( - version=1, - domain=DOMAIN, - title="APCUPSd", - data=CONF_DATA, - unique_id=MOCK_STATUS["SERIALNO"], - source=SOURCE_USER, + mock_config_entry.add_to_hass(hass) + + # Assign the different host and port, but we should still reject the creation since the + # serial number is the same as the existing entry. + mock_request_status.return_value = MOCK_STATUS + another_host = CONF_DATA | {CONF_HOST: "another_host"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=another_host, ) - mock_entry.add_to_hass(hass) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" - with patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" - ) as mock_request_status: - # Assign the different host and port, but we should still reject the creation since the - # serial number is the same as the existing entry. - mock_request_status.return_value = MOCK_STATUS - another_host = CONF_DATA | {CONF_HOST: "another_host"} - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=another_host, - ) - assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" - - # Now we change the serial number and add it again. This should be successful. - mock_request_status.return_value = MOCK_STATUS | { - "SERIALNO": MOCK_STATUS["SERIALNO"] + "ZZZ" - } - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=another_host - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == another_host + # Now we change the serial number and add it again. This should be successful. + mock_request_status.return_value = MOCK_STATUS | { + "SERIALNO": MOCK_STATUS["SERIALNO"] + "ZZZ" + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=another_host + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == another_host -async def test_flow_works(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: +async def test_flow_works( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_request_status: AsyncMock, +) -> None: """Test successful creation of config entries via user configuration.""" - with patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", - return_value=MOCK_STATUS, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_USER}, - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=CONF_DATA - ) - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["title"] == MOCK_STATUS["UPSNAME"] - assert result["data"] == CONF_DATA - assert result["result"].unique_id == MOCK_STATUS["SERIALNO"] + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=CONF_DATA + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == MOCK_STATUS["UPSNAME"] + assert result["data"] == CONF_DATA + assert result["result"].unique_id == MOCK_STATUS["SERIALNO"] - mock_setup_entry.assert_called_once() + mock_setup_entry.assert_called_once() @pytest.mark.parametrize( - ("extra_status", "expected_title"), + ("mock_request_status", "expected_title"), [ - ({"UPSNAME": "Friendly Name"}, "Friendly Name"), - ({"MODEL": "MODEL X"}, "MODEL X"), - ({"SERIALNO": "ZZZZ"}, "ZZZZ"), - # Some models report "Blank" as serial number, which we should treat it as not reported. - ({"SERIALNO": "Blank"}, "APC UPS"), - ({}, "APC UPS"), + (MOCK_MINIMAL_STATUS | {"UPSNAME": "Friendly Name"}, "Friendly Name"), + (MOCK_MINIMAL_STATUS | {"MODEL": "MODEL X"}, "MODEL X"), + (MOCK_MINIMAL_STATUS | {"SERIALNO": "ZZZZ"}, "ZZZZ"), + # Some models report "Blank" as the serial number, which we should treat it as not reported. + (MOCK_MINIMAL_STATUS | {"SERIALNO": "Blank"}, "APC UPS"), + (MOCK_MINIMAL_STATUS | {}, "APC UPS"), ], + indirect=["mock_request_status"], ) async def test_flow_minimal_status( hass: HomeAssistant, - extra_status: dict[str, str], expected_title: str, mock_setup_entry: AsyncMock, + mock_request_status: AsyncMock, ) -> None: """Test successful creation of config entries via user configuration when minimal status is reported. We test different combinations of minimal statuses, where the title of the integration will vary. """ - with patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status" - ) as mock_request_status: - status = MOCK_MINIMAL_STATUS | extra_status - mock_request_status.return_value = status - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA - ) - await hass.async_block_till_done() - assert result["type"] is FlowResultType.CREATE_ENTRY - assert result["data"] == CONF_DATA - assert result["title"] == expected_title - mock_setup_entry.assert_called_once() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["data"] == CONF_DATA + assert result["title"] == expected_title + mock_setup_entry.assert_called_once() async def test_reconfigure_flow_works( - hass: HomeAssistant, mock_setup_entry: AsyncMock + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_request_status: AsyncMock, ) -> None: """Test successful reconfiguration of an existing entry.""" - mock_entry = MockConfigEntry( - version=1, - domain=DOMAIN, - title="APCUPSd", - data=CONF_DATA, - unique_id=MOCK_STATUS["SERIALNO"], - source=SOURCE_USER, - ) - mock_entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) - result = await mock_entry.start_reconfigure_flow(hass) + result = await mock_config_entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" # New configuration data with different host/port. new_conf_data = {CONF_HOST: "new_host", CONF_PORT: 4321} - with patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", - return_value=MOCK_STATUS, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=new_conf_data - ) - await hass.async_block_till_done() - mock_setup_entry.assert_called_once() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=new_conf_data + ) + await hass.async_block_till_done() + mock_setup_entry.assert_called_once() assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" # Check that the entry was updated with the new configuration. - assert mock_entry.data[CONF_HOST] == new_conf_data[CONF_HOST] - assert mock_entry.data[CONF_PORT] == new_conf_data[CONF_PORT] + assert mock_config_entry.data[CONF_HOST] == new_conf_data[CONF_HOST] + assert mock_config_entry.data[CONF_PORT] == new_conf_data[CONF_PORT] async def test_reconfigure_flow_cannot_connect( - hass: HomeAssistant, mock_setup_entry: AsyncMock + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_request_status: AsyncMock, ) -> None: """Test reconfiguration with connection error and recovery.""" - mock_entry = MockConfigEntry( - version=1, - domain=DOMAIN, - title="APCUPSd", - data=CONF_DATA, - unique_id=MOCK_STATUS["SERIALNO"], - source=SOURCE_USER, - ) - mock_entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) - result = await mock_entry.start_reconfigure_flow(hass) + result = await mock_config_entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" # New configuration data with different host/port. new_conf_data = {CONF_HOST: "new_host", CONF_PORT: 4321} - with patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", - side_effect=OSError(), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=new_conf_data - ) + mock_request_status.side_effect = OSError() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=new_conf_data + ) assert result["type"] is FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Test recovery by fixing the connection issue. - with patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", - return_value=MOCK_STATUS, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=new_conf_data - ) + mock_request_status.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=new_conf_data + ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" - assert mock_entry.data == new_conf_data + assert mock_config_entry.data == new_conf_data @pytest.mark.parametrize( @@ -276,35 +230,27 @@ async def test_reconfigure_flow_cannot_connect( ], ) async def test_reconfigure_flow_wrong_device( - hass: HomeAssistant, unique_id_before: str | None, unique_id_after: str | None + hass: HomeAssistant, + unique_id_before: str | None, + unique_id_after: str, + mock_config_entry: MockConfigEntry, + mock_request_status: AsyncMock, ) -> None: """Test reconfiguration with a different device (wrong serial number).""" - mock_entry = MockConfigEntry( - version=1, - domain=DOMAIN, - title="APCUPSd", - data=CONF_DATA, - unique_id=unique_id_before, - source=SOURCE_USER, + mock_config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + mock_config_entry, unique_id=unique_id_before ) - mock_entry.add_to_hass(hass) - result = await mock_entry.start_reconfigure_flow(hass) + result = await mock_config_entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure" # New configuration data with different host/port. - new_conf_data = {CONF_HOST: "new_host", CONF_PORT: 4321} - # Make a copy of the status and modify the serial number if needed. - mock_status = {k: v for k, v in MOCK_STATUS.items() if k != "SERIALNO"} - mock_status["SERIALNO"] = unique_id_after - with patch( - "homeassistant.components.apcupsd.coordinator.aioapcaccess.request_status", - return_value=mock_status, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=new_conf_data - ) + mock_request_status.return_value = MOCK_STATUS | {"SERIALNO": unique_id_after} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_HOST: "new_host", CONF_PORT: 4321} + ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "wrong_apcupsd_daemon" diff --git a/tests/components/apcupsd/test_diagnostics.py b/tests/components/apcupsd/test_diagnostics.py index 67946a928f8..58612f05fa9 100644 --- a/tests/components/apcupsd/test_diagnostics.py +++ b/tests/components/apcupsd/test_diagnostics.py @@ -4,8 +4,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.core import HomeAssistant -from . import async_init_integration - +from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -14,7 +13,8 @@ async def test_diagnostics( hass: HomeAssistant, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, + init_integration: MockConfigEntry, ) -> None: """Test diagnostics.""" - entry = await async_init_integration(hass) + entry = init_integration assert await get_diagnostics_for_config_entry(hass, hass_client, entry) == snapshot diff --git a/tests/components/apcupsd/test_init.py b/tests/components/apcupsd/test_init.py index 4f6b55fe317..13abb00141d 100644 --- a/tests/components/apcupsd/test_init.py +++ b/tests/components/apcupsd/test_init.py @@ -1,28 +1,28 @@ """Test init of APCUPSd integration.""" import asyncio -from collections import OrderedDict -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.apcupsd.const import DOMAIN from homeassistant.components.apcupsd.coordinator import UPDATE_INTERVAL -from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.util import slugify, utcnow -from . import CONF_DATA, MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration +from . import MOCK_MINIMAL_STATUS, MOCK_STATUS from tests.common import MockConfigEntry, async_fire_time_changed +@pytest.mark.parametrize("mock_config_entry", ["mocked-config-entry-id"], indirect=True) @pytest.mark.parametrize( - "status", + "mock_request_status", [ # Contains "SERIALNO" and "UPSNAME" fields. # We should create devices for the entities and prefix their IDs with "MyUPS". @@ -32,23 +32,26 @@ from tests.common import MockConfigEntry, async_fire_time_changed MOCK_MINIMAL_STATUS | {"SERIALNO": "XXXX"}, # Does not contain either "SERIALNO" field or "UPSNAME" field. # Our integration should work fine without it by falling back to config entry ID as unique - # ID and "APC UPS" as default name. + # ID and "APC UPS" as the default name. MOCK_MINIMAL_STATUS, # Some models report "Blank" as SERIALNO, but we should treat it as not reported. MOCK_MINIMAL_STATUS | {"SERIALNO": "Blank"}, ], + indirect=True, ) async def test_async_setup_entry( hass: HomeAssistant, - status: OrderedDict, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, + init_integration: MockConfigEntry, + mock_request_status: AsyncMock, ) -> None: """Test a successful setup entry.""" - config_entry = await async_init_integration(hass, status=status) - device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, config_entry.unique_id or config_entry.entry_id)} - ) + status = mock_request_status.return_value + entry = init_integration + + identifiers = {(DOMAIN, entry.unique_id or entry.entry_id)} + device_entry = device_registry.async_get_device(identifiers=identifiers) name = f"device_{device_entry.name}_{status.get('SERIALNO', '')}" assert device_entry == snapshot(name=name) @@ -61,28 +64,26 @@ async def test_async_setup_entry( "error", [OSError(), asyncio.IncompleteReadError(partial=b"", expected=0)], ) -async def test_connection_error(hass: HomeAssistant, error: Exception) -> None: +async def test_connection_error( + hass: HomeAssistant, + error: Exception, + mock_config_entry: MockConfigEntry, + mock_request_status: AsyncMock, +) -> None: """Test connection error during integration setup.""" - entry = MockConfigEntry( - version=1, - domain=DOMAIN, - title="APCUPSd", - data=CONF_DATA, - source=SOURCE_USER, - ) + mock_config_entry.add_to_hass(hass) + mock_request_status.side_effect = error - entry.add_to_hass(hass) - - with patch("aioapcaccess.request_status", side_effect=error): - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state is ConfigEntryState.SETUP_RETRY + await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY -async def test_unload_remove_entry(hass: HomeAssistant) -> None: +async def test_unload_remove_entry( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: """Test successful unload and removal of an entry.""" - entry = await async_init_integration( - hass, host="test1", status=MOCK_STATUS, entry_id="entry-id-1" - ) + entry = init_integration assert entry.state is ConfigEntryState.LOADED # Unload the entry. @@ -96,37 +97,38 @@ async def test_unload_remove_entry(hass: HomeAssistant) -> None: assert len(hass.config_entries.async_entries(DOMAIN)) == 0 -async def test_availability(hass: HomeAssistant) -> None: +async def test_availability( + hass: HomeAssistant, + mock_request_status: AsyncMock, + init_integration: MockConfigEntry, +) -> None: """Ensure that we mark the entity's availability properly when network is down / back up.""" - await async_init_integration(hass) - - device_slug = slugify(MOCK_STATUS["UPSNAME"]) + device_slug = slugify(mock_request_status.return_value["UPSNAME"]) state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state != STATE_UNAVAILABLE assert pytest.approx(float(state.state)) == 14.0 - with patch("aioapcaccess.request_status") as mock_request_status: - # Mock a network error and then trigger an auto-polling event. - mock_request_status.side_effect = OSError() - future = utcnow() + UPDATE_INTERVAL - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + # Mock a network error and then trigger an auto-polling event. + mock_request_status.side_effect = OSError() + future = utcnow() + UPDATE_INTERVAL + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - # Sensors should be marked as unavailable. - state = hass.states.get(f"sensor.{device_slug}_load") - assert state - assert state.state == STATE_UNAVAILABLE + # Sensors should be marked as unavailable. + state = hass.states.get(f"sensor.{device_slug}_load") + assert state + assert state.state == STATE_UNAVAILABLE - # Reset the API to return a new status and update. - mock_request_status.side_effect = None - mock_request_status.return_value = MOCK_STATUS | {"LOADPCT": "15.0 Percent"} - future = future + UPDATE_INTERVAL - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + # Reset the API to return a new status and update. + mock_request_status.side_effect = None + mock_request_status.return_value = MOCK_STATUS | {"LOADPCT": "15.0 Percent"} + future = future + UPDATE_INTERVAL + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - # Sensors should be online now with the new value. - state = hass.states.get(f"sensor.{device_slug}_load") - assert state - assert state.state != STATE_UNAVAILABLE - assert pytest.approx(float(state.state)) == 15.0 + # Sensors should be online now with the new value. + state = hass.states.get(f"sensor.{device_slug}_load") + assert state + assert state.state != STATE_UNAVAILABLE + assert pytest.approx(float(state.state)) == 15.0 diff --git a/tests/components/apcupsd/test_sensor.py b/tests/components/apcupsd/test_sensor.py index af163d3cbc1..c605ba588f9 100644 --- a/tests/components/apcupsd/test_sensor.py +++ b/tests/components/apcupsd/test_sensor.py @@ -1,7 +1,7 @@ """Test sensors of APCUPSd integration.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest from syrupy.assertion import SnapshotAssertion @@ -19,53 +19,62 @@ from homeassistant.setup import async_setup_component from homeassistant.util import slugify from homeassistant.util.dt import utcnow -from . import MOCK_MINIMAL_STATUS, MOCK_STATUS, async_init_integration +from . import MOCK_MINIMAL_STATUS, MOCK_STATUS -from tests.common import async_fire_time_changed, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + +pytestmark = pytest.mark.usefixtures( + "entity_registry_enabled_by_default", "init_integration" +) + + +@pytest.fixture +def platforms() -> list[Platform]: + """Overridden fixture to specify platforms to test.""" + return [Platform.SENSOR] -@pytest.mark.usefixtures("entity_registry_enabled_by_default") async def test_sensor( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, ) -> None: - """Test states of sensor.""" - with patch("homeassistant.components.apcupsd.PLATFORMS", [Platform.SENSOR]): - config_entry = await async_init_integration(hass, status=MOCK_STATUS) - await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id) + """Test states of sensor entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) -async def test_state_update(hass: HomeAssistant) -> None: +async def test_state_update( + hass: HomeAssistant, + mock_request_status: AsyncMock, +) -> None: """Ensure the sensor state changes after updating the data.""" - await async_init_integration(hass) - - device_slug = slugify(MOCK_STATUS["UPSNAME"]) + device_slug = slugify(mock_request_status.return_value["UPSNAME"]) state = hass.states.get(f"sensor.{device_slug}_load") assert state assert state.state != STATE_UNAVAILABLE assert state.state == "14.0" - new_status = MOCK_STATUS | {"LOADPCT": "15.0 Percent"} - with patch("aioapcaccess.request_status", return_value=new_status): - future = utcnow() + timedelta(minutes=2) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + mock_request_status.return_value = MOCK_STATUS | {"LOADPCT": "15.0 Percent"} + future = utcnow() + timedelta(minutes=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = hass.states.get(f"sensor.{device_slug}_load") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "15.0" + state = hass.states.get(f"sensor.{device_slug}_load") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "15.0" -async def test_manual_update_entity(hass: HomeAssistant) -> None: +async def test_manual_update_entity( + hass: HomeAssistant, + mock_request_status: AsyncMock, +) -> None: """Test multiple simultaneous manual update entity via service homeassistant/update_entity. We should only do network call once for the multiple simultaneous update entity services. """ - await async_init_integration(hass) - - device_slug = slugify(MOCK_STATUS["UPSNAME"]) + device_slug = slugify(mock_request_status.return_value["UPSNAME"]) # Assert the initial state of sensor.ups_load. state = hass.states.get(f"sensor.{device_slug}_load") assert state @@ -75,41 +84,43 @@ async def test_manual_update_entity(hass: HomeAssistant) -> None: # Setup HASS for calling the update_entity service. await async_setup_component(hass, "homeassistant", {}) - with patch("aioapcaccess.request_status") as mock_request_status: - mock_request_status.return_value = MOCK_STATUS | { - "LOADPCT": "15.0 Percent", - "BCHARGE": "99.0 Percent", - } - # Now, we fast-forward the time to pass the debouncer cooldown, but put it - # before the normal update interval to see if the manual update works. - future = utcnow() + timedelta(seconds=REQUEST_REFRESH_COOLDOWN) - async_fire_time_changed(hass, future) - await hass.services.async_call( - "homeassistant", - "update_entity", - { - ATTR_ENTITY_ID: [ - f"sensor.{device_slug}_load", - f"sensor.{device_slug}_battery", - ] - }, - blocking=True, - ) - # Even if we requested updates for two entities, our integration should smartly - # group the API calls to just one. - assert mock_request_status.call_count == 1 + mock_request_status.return_value = MOCK_STATUS | { + "LOADPCT": "15.0 Percent", + "BCHARGE": "99.0 Percent", + } + # Now, we fast-forward the time to pass the debouncer cooldown, but put it + # before the normal update interval to see if the manual update works. + request_call_count_before = mock_request_status.call_count + future = utcnow() + timedelta(seconds=REQUEST_REFRESH_COOLDOWN) + async_fire_time_changed(hass, future) + await hass.services.async_call( + "homeassistant", + "update_entity", + { + ATTR_ENTITY_ID: [ + f"sensor.{device_slug}_load", + f"sensor.{device_slug}_battery", + ] + }, + blocking=True, + ) + # Even if we requested updates for two entities, our integration should smartly + # group the API calls to just one. + assert mock_request_status.call_count == request_call_count_before + 1 - # The new state should be effective. - state = hass.states.get(f"sensor.{device_slug}_load") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "15.0" + # The new state should be effective. + state = hass.states.get(f"sensor.{device_slug}_load") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "15.0" -async def test_sensor_unknown(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("mock_request_status", [MOCK_MINIMAL_STATUS], indirect=True) +async def test_sensor_unknown( + hass: HomeAssistant, + mock_request_status: AsyncMock, +) -> None: """Test if our integration can properly mark certain sensors as unknown when it becomes so.""" - await async_init_integration(hass, status=MOCK_MINIMAL_STATUS) - ups_mode_id = "sensor.apc_ups_mode" last_self_test_id = "sensor.apc_ups_last_self_test" @@ -121,20 +132,18 @@ async def test_sensor_unknown(hass: HomeAssistant) -> None: # Simulate an event (a self test) such that "LASTSTEST" field is being reported, the state of # the sensor should be properly updated with the corresponding value. - with patch("aioapcaccess.request_status") as mock_request_status: - mock_request_status.return_value = MOCK_MINIMAL_STATUS | { - "LASTSTEST": "1970-01-01 00:00:00 0000" - } - future = utcnow() + timedelta(minutes=2) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + mock_request_status.return_value = MOCK_MINIMAL_STATUS | { + "LASTSTEST": "1970-01-01 00:00:00 0000" + } + future = utcnow() + timedelta(minutes=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() assert hass.states.get(last_self_test_id).state == "1970-01-01 00:00:00 0000" # Simulate another event (e.g., daemon restart) such that "LASTSTEST" is no longer reported. - with patch("aioapcaccess.request_status") as mock_request_status: - mock_request_status.return_value = MOCK_MINIMAL_STATUS - future = utcnow() + timedelta(minutes=2) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + mock_request_status.return_value = MOCK_MINIMAL_STATUS + future = utcnow() + timedelta(minutes=2) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() # The state should become unknown again. assert hass.states.get(last_self_test_id).state == STATE_UNKNOWN