diff --git a/.coveragerc b/.coveragerc index 4dc3df0ba41..64acaa2d8e7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -542,15 +542,12 @@ omit = homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/const.py homeassistant/components/launch_library/sensor.py - homeassistant/components/lcn/__init__.py homeassistant/components/lcn/binary_sensor.py homeassistant/components/lcn/climate.py - homeassistant/components/lcn/const.py homeassistant/components/lcn/cover.py homeassistant/components/lcn/helpers.py homeassistant/components/lcn/light.py homeassistant/components/lcn/scene.py - homeassistant/components/lcn/schemas.py homeassistant/components/lcn/sensor.py homeassistant/components/lcn/services.py homeassistant/components/lcn/switch.py diff --git a/tests/components/lcn/conftest.py b/tests/components/lcn/conftest.py new file mode 100644 index 00000000000..aae4acfa914 --- /dev/null +++ b/tests/components/lcn/conftest.py @@ -0,0 +1,97 @@ +"""Test configuration and mocks for LCN component.""" +import json +from unittest.mock import AsyncMock, patch + +import pypck +from pypck.connection import PchkConnectionManager +import pypck.module +from pypck.module import GroupConnection, ModuleConnection +import pytest + +from homeassistant.components.lcn.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture + + +class MockModuleConnection(ModuleConnection): + """Fake a LCN module connection.""" + + status_request_handler = AsyncMock() + activate_status_request_handler = AsyncMock() + cancel_status_request_handler = AsyncMock() + send_command = AsyncMock(return_value=True) + + +class MockGroupConnection(GroupConnection): + """Fake a LCN group connection.""" + + send_command = AsyncMock(return_value=True) + + +class MockPchkConnectionManager(PchkConnectionManager): + """Fake connection handler.""" + + async def async_connect(self, timeout=30): + """Mock establishing a connection to PCHK.""" + self.authentication_completed_future.set_result(True) + self.license_error_future.set_result(True) + self.segment_scan_completed_event.set() + + async def async_close(self): + """Mock closing a connection to PCHK.""" + + @patch.object(pypck.connection, "ModuleConnection", MockModuleConnection) + @patch.object(pypck.connection, "GroupConnection", MockGroupConnection) + def get_address_conn(self, addr): + """Get LCN address connection.""" + return super().get_address_conn(addr, request_serials=False) + + send_command = AsyncMock() + + +def create_config_entry(name): + """Set up config entries with configuration data.""" + fixture_filename = f"lcn/config_entry_{name}.json" + entry_data = json.loads(load_fixture(fixture_filename)) + options = {} + + title = entry_data[CONF_HOST] + unique_id = fixture_filename + entry = MockConfigEntry( + domain=DOMAIN, + title=title, + unique_id=unique_id, + data=entry_data, + options=options, + ) + return entry + + +@pytest.fixture(name="entry") +def create_config_entry_pchk(): + """Return one specific config entry.""" + return create_config_entry("pchk") + + +@pytest.fixture(name="entry2") +def create_config_entry_myhome(): + """Return one specific config entry.""" + return create_config_entry("myhome") + + +async def init_integration(hass, entry): + """Set up the LCN integration in Home Assistant.""" + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +async def setup_component(hass): + """Set up the LCN component.""" + fixture_filename = "lcn/config.json" + config_data = json.loads(load_fixture(fixture_filename)) + + await async_setup_component(hass, DOMAIN, config_data) + await hass.async_block_till_done() diff --git a/tests/components/lcn/test_init.py b/tests/components/lcn/test_init.py new file mode 100644 index 00000000000..eef02b681d8 --- /dev/null +++ b/tests/components/lcn/test_init.py @@ -0,0 +1,106 @@ +"""Test init of LCN integration.""" +from unittest.mock import patch + +from pypck.connection import ( + PchkAuthenticationError, + PchkConnectionManager, + PchkLicenseError, +) + +from homeassistant import config_entries +from homeassistant.components.lcn.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import MockPchkConnectionManager, init_integration, setup_component + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_async_setup_entry(hass, entry): + """Test a successful setup entry and unload of entry.""" + await init_integration(hass, entry) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_async_setup_multiple_entries(hass, entry, entry2): + """Test a successful setup and unload of multiple entries.""" + for config_entry in (entry, entry2): + await init_integration(hass, config_entry) + assert config_entry.state == ConfigEntryState.LOADED + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + + for config_entry in (entry, entry2): + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.NOT_LOADED + + assert not hass.data.get(DOMAIN) + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_async_setup_entry_update(hass, entry): + """Test a successful setup entry if entry with same id already exists.""" + # setup first entry + entry.source = config_entries.SOURCE_IMPORT + + # create dummy entity for LCN platform as an orphan + entity_registry = await er.async_get_registry(hass) + dummy_entity = entity_registry.async_get_or_create( + "switch", DOMAIN, "dummy", config_entry=entry + ) + assert dummy_entity in entity_registry.entities.values() + + # add entity to hass and setup (should cleanup dummy entity) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert dummy_entity not in entity_registry.entities.values() + + +async def test_async_setup_entry_raises_authentication_error(hass, entry): + """Test that an authentication error is handled properly.""" + with patch.object( + PchkConnectionManager, "async_connect", side_effect=PchkAuthenticationError + ): + await init_integration(hass, entry) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_async_setup_entry_raises_license_error(hass, entry): + """Test that an authentication error is handled properly.""" + with patch.object( + PchkConnectionManager, "async_connect", side_effect=PchkLicenseError + ): + await init_integration(hass, entry) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_async_setup_entry_raises_timeout_error(hass, entry): + """Test that an authentication error is handled properly.""" + with patch.object(PchkConnectionManager, "async_connect", side_effect=TimeoutError): + await init_integration(hass, entry) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +@patch("pypck.connection.PchkConnectionManager", MockPchkConnectionManager) +async def test_async_setup_from_configuration_yaml(hass): + """Test a successful setup using data from configuration.yaml.""" + await async_setup_component(hass, "persistent_notification", {}) + + with patch("homeassistant.components.lcn.async_setup_entry") as async_setup_entry: + await setup_component(hass) + + assert async_setup_entry.await_count == 2 diff --git a/tests/fixtures/lcn/config.json b/tests/fixtures/lcn/config.json new file mode 100644 index 00000000000..50a1ca05e29 --- /dev/null +++ b/tests/fixtures/lcn/config.json @@ -0,0 +1,31 @@ +{ + "lcn": { + "connections": [ + { + "host": "192.168.2.41", + "port": 4114, + "username": "lcn", + "password": "lcn", + "sk_num_tries": 0, + "dim_mode": "steps200", + "name": "pchk" + }, + { + "name": "myhome", + "host": "192.168.2.42", + "port": 4114, + "username": "lcn", + "password": "lcn", + "sk_num_tries": 0, + "dim_mode": "steps200" + } + ], + "switches": [ + { + "name": "Switch_Output1", + "address": "s0.m7", + "output": "output1" + } + ] + } +} diff --git a/tests/fixtures/lcn/config_entry_myhome.json b/tests/fixtures/lcn/config_entry_myhome.json new file mode 100644 index 00000000000..8ab59d0087d --- /dev/null +++ b/tests/fixtures/lcn/config_entry_myhome.json @@ -0,0 +1,11 @@ +{ + "host": "myhome", + "ip_address": "192.168.2.42", + "port": 4114, + "username": "lcn", + "password": "lcn", + "sk_num_tries": 0, + "dim_mode": "STEPS200", + "devices": [], + "entities": [] +} diff --git a/tests/fixtures/lcn/config_entry_pchk.json b/tests/fixtures/lcn/config_entry_pchk.json new file mode 100644 index 00000000000..3058389a95d --- /dev/null +++ b/tests/fixtures/lcn/config_entry_pchk.json @@ -0,0 +1,29 @@ +{ + "host": "pchk", + "ip_address": "192.168.2.41", + "port": 4114, + "username": "lcn", + "password": "lcn", + "sk_num_tries": 0, + "dim_mode": "STEPS200", + "devices": [ + { + "address": [0, 7, false], + "name": "", + "hardware_serial": -1, + "software_serial": -1, + "hardware_type": -1 + } + ], + "entities": [ + { + "address": [0, 7, false], + "name": "Switch_Output1", + "resource": "output1", + "domain": "switch", + "domain_data": { + "output": "OUTPUT1" + } + } + ] +}