diff --git a/.coveragerc b/.coveragerc index 2dabb5e966f..375d7df4d0e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -752,7 +752,6 @@ omit = homeassistant/components/sesame/lock.py homeassistant/components/seven_segments/image_processing.py homeassistant/components/seventeentrack/sensor.py - homeassistant/components/sharkiq/vacuum.py homeassistant/components/shiftr/* homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index fb61e54f98f..968ee91f8d6 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -60,7 +60,7 @@ async def async_setup_entry(hass, config_entry): shark_vacs = await ayla_api.async_get_devices(False) device_names = ", ".join([d.name for d in shark_vacs]) - LOGGER.debug("Found %d Shark IQ device(s): %s", len(device_names), device_names) + LOGGER.debug("Found %d Shark IQ device(s): %s", len(shark_vacs), device_names) coordinator = SharkIqUpdateCoordinator(hass, config_entry, ayla_api, shark_vacs) await coordinator.async_refresh() diff --git a/tests/components/sharkiq/const.py b/tests/components/sharkiq/const.py index 392b4a863d3..305d12ddfa7 100644 --- a/tests/components/sharkiq/const.py +++ b/tests/components/sharkiq/const.py @@ -71,3 +71,4 @@ TEST_USERNAME = "test-username" TEST_PASSWORD = "test-password" UNIQUE_ID = "foo@bar.com" CONFIG = {CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD} +ENTRY_ID = "0123456789abcdef0123456789abcdef" diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index f588b5f82a2..3183f6fdee2 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -1,24 +1,18 @@ """Test the Shark IQ config flow.""" import aiohttp -from sharkiqpy import SharkIqAuthError +import pytest +from sharkiqpy import AylaApi, SharkIqAuthError from homeassistant import config_entries, setup from homeassistant.components.sharkiq.const import DOMAIN +from homeassistant.core import HomeAssistant from .const import CONFIG, TEST_PASSWORD, TEST_USERNAME, UNIQUE_ID -from tests.async_mock import MagicMock, PropertyMock, patch +from tests.async_mock import patch from tests.common import MockConfigEntry -def _create_mocked_ayla(connect=None): - """Create a mocked AylaApi object.""" - mocked_ayla = MagicMock() - type(mocked_ayla).sign_in = PropertyMock(side_effect=connect) - type(mocked_ayla).async_sign_in = PropertyMock(side_effect=connect) - return mocked_ayla - - async def test_form(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -46,75 +40,37 @@ async def test_form(hass): "password": TEST_PASSWORD, } await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 + mock_setup.assert_called_once() + mock_setup_entry.assert_called_once() -async def test_form_invalid_auth(hass): - """Test we handle invalid auth.""" +@pytest.mark.parametrize( + "exc,base_error", + [ + (SharkIqAuthError, "invalid_auth"), + (aiohttp.ClientError, "cannot_connect"), + (TypeError, "unknown"), + ], +) +async def test_form_error(hass: HomeAssistant, exc: Exception, base_error: str): + """Test form errors.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mocked_ayla = _create_mocked_ayla(connect=SharkIqAuthError) - with patch( - "homeassistant.components.sharkiq.config_flow.get_ayla_api", - return_value=mocked_ayla, - ): + with patch.object(AylaApi, "async_sign_in", side_effect=exc): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], CONFIG, ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["errors"].get("base") == base_error -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - mocked_ayla = _create_mocked_ayla(connect=aiohttp.ClientError) - - with patch( - "homeassistant.components.sharkiq.config_flow.get_ayla_api", - return_value=mocked_ayla, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - CONFIG, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_other_error(hass): - """Test we handle other errors.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - mocked_ayla = _create_mocked_ayla(connect=TypeError) - - with patch( - "homeassistant.components.sharkiq.config_flow.get_ayla_api", - return_value=mocked_ayla, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], CONFIG - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} - - -async def test_reauth(hass): +async def test_reauth_success(hass: HomeAssistant): """Test reauth flow.""" - with patch( - "homeassistant.components.sharkiq.vacuum.async_setup_entry", - return_value=True, - ), patch("sharkiqpy.AylaApi.async_sign_in", return_value=True): + with patch("sharkiqpy.AylaApi.async_sign_in", return_value=True): mock_config = MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG) mock_config.add_to_hass(hass) @@ -125,21 +81,33 @@ async def test_reauth(hass): assert result["type"] == "abort" assert result["reason"] == "reauth_successful" - with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=SharkIqAuthError): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": "reauth", "unique_id": UNIQUE_ID}, - data=CONFIG, - ) - assert result["type"] == "form" - assert result["errors"] == {"base": "invalid_auth"} - with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=RuntimeError): +@pytest.mark.parametrize( + "side_effect,result_type,msg_field,msg", + [ + (SharkIqAuthError, "form", "errors", "invalid_auth"), + (aiohttp.ClientError, "abort", "reason", "cannot_connect"), + (TypeError, "abort", "reason", "unknown"), + ], +) +async def test_reauth( + hass: HomeAssistant, + side_effect: Exception, + result_type: str, + msg_field: str, + msg: str, +): + """Test reauth failures.""" + with patch("sharkiqpy.AylaApi.async_sign_in", side_effect=side_effect): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth", "unique_id": UNIQUE_ID}, data=CONFIG, ) - assert result["type"] == "abort" - assert result["reason"] == "unknown" + msg_value = result[msg_field] + if msg_field == "errors": + msg_value = msg_value.get("base") + + assert result["type"] == result_type + assert msg_value == msg diff --git a/tests/components/sharkiq/test_shark_iq.py b/tests/components/sharkiq/test_shark_iq.py deleted file mode 100644 index 927f62b138c..00000000000 --- a/tests/components/sharkiq/test_shark_iq.py +++ /dev/null @@ -1,267 +0,0 @@ -"""Test the Shark IQ vacuum entity.""" -from copy import deepcopy -import enum -import json -from typing import Dict, List - -from sharkiqpy import AylaApi, Properties, SharkIqAuthError, SharkIqVacuum, get_ayla_api - -from homeassistant.components.sharkiq import SharkIqUpdateCoordinator -from homeassistant.components.sharkiq.vacuum import ( - ATTR_ERROR_CODE, - ATTR_ERROR_MSG, - ATTR_LOW_LIGHT, - ATTR_RECHARGE_RESUME, - SharkVacuumEntity, -) -from homeassistant.components.vacuum import ( - STATE_CLEANING, - STATE_DOCKED, - STATE_IDLE, - STATE_PAUSED, - STATE_RETURNING, - SUPPORT_BATTERY, - SUPPORT_FAN_SPEED, - SUPPORT_LOCATE, - SUPPORT_PAUSE, - SUPPORT_RETURN_HOME, - SUPPORT_START, - SUPPORT_STATE, - SUPPORT_STATUS, - SUPPORT_STOP, -) -from homeassistant.config_entries import ConfigEntriesFlowManager, ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import UpdateFailed - -from .const import ( - SHARK_DEVICE_DICT, - SHARK_METADATA_DICT, - SHARK_PROPERTIES_DICT, - TEST_PASSWORD, - TEST_USERNAME, -) - -from tests.async_mock import MagicMock, patch - -MockAyla = MagicMock(spec=AylaApi) # pylint: disable=invalid-name - - -def _set_property(self, property_name, value): - """Set a property locally without hitting the API.""" - if isinstance(property_name, enum.Enum): - property_name = property_name.value - if isinstance(value, enum.Enum): - value = value.value - self.properties_full[property_name]["value"] = value - - -async def _async_set_property(self, property_name, value): - """Set a property locally without hitting the API.""" - _set_property(self, property_name, value) - - -def _get_mock_shark_vac(ayla_api: AylaApi) -> SharkIqVacuum: - """Create a crude sharkiq vacuum with mocked properties.""" - shark = SharkIqVacuum(ayla_api, SHARK_DEVICE_DICT) - shark.properties_full = deepcopy(SHARK_PROPERTIES_DICT) - return shark - - -async def _async_list_devices(_) -> List[Dict]: - """Generate a dummy of async_list_devices output.""" - return [SHARK_DEVICE_DICT] - - -@patch.object(SharkIqVacuum, "set_property_value", new=_set_property) -@patch.object(SharkIqVacuum, "async_set_property_value", new=_async_set_property) -async def test_shark_operation_modes(hass: HomeAssistant) -> None: - """Test all of the shark vacuum operation modes.""" - ayla_api = MockAyla() - shark_vac = _get_mock_shark_vac(ayla_api) - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac]) - shark = SharkVacuumEntity(shark_vac, coordinator) - - # These come from the setup - assert isinstance(shark.is_docked, bool) and not shark.is_docked - assert ( - isinstance(shark.recharging_to_resume, bool) and not shark.recharging_to_resume - ) - # Go through the operation modes while it's "off the dock" - await shark.async_start() - assert shark.operating_mode == shark.state == STATE_CLEANING - await shark.async_pause() - assert shark.operating_mode == shark.state == STATE_PAUSED - await shark.async_stop() - assert shark.operating_mode == shark.state == STATE_IDLE - await shark.async_return_to_base() - assert shark.operating_mode == shark.state == STATE_RETURNING - - # Test the docked modes - await shark.async_stop() - shark.sharkiq.set_property_value(Properties.RECHARGING_TO_RESUME, 1) - shark.sharkiq.set_property_value(Properties.DOCKED_STATUS, 1) - assert isinstance(shark.is_docked, bool) and shark.is_docked - assert isinstance(shark.recharging_to_resume, bool) and shark.recharging_to_resume - assert shark.state == STATE_DOCKED - - shark.sharkiq.set_property_value(Properties.RECHARGING_TO_RESUME, 0) - assert shark.state == STATE_DOCKED - - await shark.async_set_fan_speed("Eco") - assert shark.fan_speed == "Eco" - await shark.async_set_fan_speed("Max") - assert shark.fan_speed == "Max" - await shark.async_set_fan_speed("Normal") - assert shark.fan_speed == "Normal" - - assert set(shark.fan_speed_list) == {"Normal", "Max", "Eco"} - - -@patch.object(SharkIqVacuum, "set_property_value", new=_set_property) -async def test_shark_vac_properties(hass: HomeAssistant) -> None: - """Test all of the shark vacuum property accessors.""" - ayla_api = MockAyla() - shark_vac = _get_mock_shark_vac(ayla_api) - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac]) - shark = SharkVacuumEntity(shark_vac, coordinator) - - assert shark.name == "Sharknado" - assert shark.serial_number == "AC000Wxxxxxxxxx" - assert shark.model == "RV1000A" - - assert shark.battery_level == 50 - assert shark.fan_speed == "Eco" - shark.sharkiq.set_property_value(Properties.POWER_MODE, 0) - assert shark.fan_speed == "Normal" - assert isinstance(shark.recharge_resume, bool) and shark.recharge_resume - assert isinstance(shark.low_light, bool) and not shark.low_light - - target_state_attributes = { - ATTR_ERROR_CODE: 7, - ATTR_ERROR_MSG: "Cliff sensor is blocked", - ATTR_RECHARGE_RESUME: True, - ATTR_LOW_LIGHT: False, - } - state_json = json.dumps(shark.device_state_attributes, sort_keys=True) - target_json = json.dumps(target_state_attributes, sort_keys=True) - assert state_json == target_json - - assert not shark.should_poll - - -@patch.object(SharkIqVacuum, "set_property_value", new=_set_property) -@patch.object(SharkIqVacuum, "async_set_property_value", new=_async_set_property) -async def test_shark_metadata(hass: HomeAssistant) -> None: - """Test shark properties coming from metadata.""" - ayla_api = MockAyla() - shark_vac = _get_mock_shark_vac(ayla_api) - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac]) - shark = SharkVacuumEntity(shark_vac, coordinator) - shark.sharkiq._update_metadata( # pylint: disable=protected-access - SHARK_METADATA_DICT - ) - - target_device_info = { - "identifiers": {("sharkiq", "AC000Wxxxxxxxxx")}, - "name": "Sharknado", - "manufacturer": "Shark", - "model": "RV1001AE", - "sw_version": "Dummy Firmware 1.0", - } - - assert shark.device_info == target_device_info - - -def _get_async_update(err=None): - async def _async_update(_) -> bool: - if err is not None: - raise err - return True - - return _async_update - - -@patch.object(AylaApi, "async_list_devices", new=_async_list_devices) -async def test_updates(hass: HomeAssistant) -> None: - """Test the update coordinator update functions.""" - ayla_api = get_ayla_api(TEST_USERNAME, TEST_PASSWORD) - shark_vac = _get_mock_shark_vac(ayla_api) - mock_config = MagicMock(spec=ConfigEntry) - coordinator = SharkIqUpdateCoordinator(hass, mock_config, ayla_api, [shark_vac]) - - with patch.object(SharkIqVacuum, "async_update", new=_get_async_update()): - update_called = ( - await coordinator._async_update_data() # pylint: disable=protected-access - ) - assert update_called - - update_failed = False - with patch.object( - SharkIqVacuum, "async_update", new=_get_async_update(SharkIqAuthError) - ), patch.object(HomeAssistant, "async_create_task"), patch.object( - ConfigEntriesFlowManager, "async_init" - ): - try: - await coordinator._async_update_data() # pylint: disable=protected-access - except UpdateFailed: - update_failed = True - assert update_failed - - -async def test_coordinator_match(hass: HomeAssistant): - """Test that sharkiq-coordinator references work.""" - ayla_api = get_ayla_api(TEST_PASSWORD, TEST_USERNAME) - shark_vac1 = _get_mock_shark_vac(ayla_api) - shark_vac2 = _get_mock_shark_vac(ayla_api) - shark_vac2._dsn = "FOOBAR!" # pylint: disable=protected-access - - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac1]) - - api = SharkVacuumEntity(shark_vac1, coordinator) - coordinator.last_update_success = True - coordinator._online_dsns = set() # pylint: disable=protected-access - assert not api.is_online - assert not api.available - - coordinator._online_dsns = { # pylint: disable=protected-access - shark_vac1.serial_number - } - assert api.is_online - assert api.available - - coordinator.last_update_success = False - assert not api.available - - -async def test_simple_properties(hass: HomeAssistant): - """Test that simple properties work as intended.""" - ayla_api = get_ayla_api(TEST_PASSWORD, TEST_USERNAME) - shark_vac1 = _get_mock_shark_vac(ayla_api) - coordinator = SharkIqUpdateCoordinator(hass, None, ayla_api, [shark_vac1]) - entity = SharkVacuumEntity(shark_vac1, coordinator) - - assert entity.unique_id == "AC000Wxxxxxxxxx" - - assert entity.supported_features == ( - SUPPORT_BATTERY - | SUPPORT_FAN_SPEED - | SUPPORT_PAUSE - | SUPPORT_RETURN_HOME - | SUPPORT_START - | SUPPORT_STATE - | SUPPORT_STATUS - | SUPPORT_STOP - | SUPPORT_LOCATE - ) - - assert entity.error_code == 7 - assert entity.error_message == "Cliff sensor is blocked" - shark_vac1.properties_full[Properties.ERROR_CODE.value]["value"] = 0 - assert entity.error_code == 0 - assert entity.error_message is None - - assert ( - coordinator.online_dsns - is coordinator._online_dsns # pylint: disable=protected-access - ) diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py new file mode 100644 index 00000000000..3b40011b8e6 --- /dev/null +++ b/tests/components/sharkiq/test_vacuum.py @@ -0,0 +1,242 @@ +"""Test the Shark IQ vacuum entity.""" +from copy import deepcopy +import enum +from typing import Any, Iterable, List, Optional + +import pytest +from sharkiqpy import AylaApi, SharkIqAuthError, SharkIqVacuum + +from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY +from homeassistant.components.sharkiq import DOMAIN +from homeassistant.components.sharkiq.vacuum import ( + ATTR_ERROR_CODE, + ATTR_ERROR_MSG, + ATTR_LOW_LIGHT, + ATTR_RECHARGE_RESUME, + FAN_SPEEDS_MAP, +) +from homeassistant.components.vacuum import ( + ATTR_BATTERY_LEVEL, + ATTR_FAN_SPEED, + ATTR_FAN_SPEED_LIST, + SERVICE_LOCATE, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, + STATE_CLEANING, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, + SUPPORT_BATTERY, + SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, + SUPPORT_PAUSE, + SUPPORT_RETURN_HOME, + SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STATUS, + SUPPORT_STOP, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import ( + CONFIG, + ENTRY_ID, + SHARK_DEVICE_DICT, + SHARK_METADATA_DICT, + SHARK_PROPERTIES_DICT, + TEST_USERNAME, +) + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +VAC_ENTITY_ID = f"vacuum.{SHARK_DEVICE_DICT['product_name'].lower()}" +EXPECTED_FEATURES = ( + SUPPORT_BATTERY + | SUPPORT_FAN_SPEED + | SUPPORT_PAUSE + | SUPPORT_RETURN_HOME + | SUPPORT_START + | SUPPORT_STATE + | SUPPORT_STATUS + | SUPPORT_STOP + | SUPPORT_LOCATE +) + + +class MockAyla(AylaApi): + """Mocked AylaApi that doesn't do anything.""" + + async def async_sign_in(self): + """Instead of signing in, just return.""" + + async def async_list_devices(self) -> List[dict]: + """Return the device list.""" + return [SHARK_DEVICE_DICT] + + async def async_get_devices(self, update: bool = True) -> List[SharkIqVacuum]: + """Get the list of devices.""" + shark = MockShark(self, SHARK_DEVICE_DICT) + shark.properties_full = deepcopy(SHARK_PROPERTIES_DICT) + shark._update_metadata(SHARK_METADATA_DICT) # pylint: disable=protected-access + return [shark] + + async def async_request(self, http_method: str, url: str, **kwargs): + """Don't make an HTTP request.""" + + +class MockShark(SharkIqVacuum): + """Mocked SharkIqVacuum that won't hit the API.""" + + async def async_update(self, property_list: Optional[Iterable[str]] = None): + """Don't do anything.""" + + def set_property_value(self, property_name, value): + """Set a property locally without hitting the API.""" + if isinstance(property_name, enum.Enum): + property_name = property_name.value + if isinstance(value, enum.Enum): + value = value.value + self.properties_full[property_name]["value"] = value + + async def async_set_property_value(self, property_name, value): + """Set a property locally without hitting the API.""" + self.set_property_value(property_name, value) + + +@pytest.fixture(autouse=True) +@patch("sharkiqpy.ayla_api.AylaApi", MockAyla) +async def setup_integration(hass): + """Build the mock integration.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id=TEST_USERNAME, data=CONFIG, entry_id=ENTRY_ID + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +async def test_simple_properties(hass: HomeAssistant): + """Test that simple properties work as intended.""" + state = hass.states.get(VAC_ENTITY_ID) + registry = await hass.helpers.entity_registry.async_get_registry() + entity = registry.async_get(VAC_ENTITY_ID) + + assert entity + assert state + assert state.state == STATE_CLEANING + assert entity.unique_id == "AC000Wxxxxxxxxx" + + +@pytest.mark.parametrize( + "attribute,target_value", + [ + (ATTR_SUPPORTED_FEATURES, EXPECTED_FEATURES), + (ATTR_BATTERY_LEVEL, 50), + (ATTR_FAN_SPEED, "Eco"), + (ATTR_FAN_SPEED_LIST, list(FAN_SPEEDS_MAP)), + (ATTR_ERROR_CODE, 7), + (ATTR_ERROR_MSG, "Cliff sensor is blocked"), + (ATTR_LOW_LIGHT, False), + (ATTR_RECHARGE_RESUME, True), + ], +) +async def test_initial_attributes( + hass: HomeAssistant, attribute: str, target_value: Any +): + """Test initial config attributes.""" + state = hass.states.get(VAC_ENTITY_ID) + assert state.attributes.get(attribute) == target_value + + +@pytest.mark.parametrize( + "service,target_state", + [ + (SERVICE_STOP, STATE_IDLE), + (SERVICE_PAUSE, STATE_PAUSED), + (SERVICE_RETURN_TO_BASE, STATE_RETURNING), + (SERVICE_START, STATE_CLEANING), + ], +) +async def test_cleaning_states(hass: HomeAssistant, service: str, target_state: str): + """Test cleaning states.""" + service_data = {ATTR_ENTITY_ID: VAC_ENTITY_ID} + await hass.services.async_call("vacuum", service, service_data, blocking=True) + state = hass.states.get(VAC_ENTITY_ID) + assert state.state == target_state + + +@pytest.mark.parametrize("fan_speed", list(FAN_SPEEDS_MAP)) +async def test_fan_speed(hass: HomeAssistant, fan_speed: str) -> None: + """Test setting fan speeds.""" + service_data = {ATTR_ENTITY_ID: VAC_ENTITY_ID, ATTR_FAN_SPEED: fan_speed} + await hass.services.async_call( + "vacuum", SERVICE_SET_FAN_SPEED, service_data, blocking=True + ) + state = hass.states.get(VAC_ENTITY_ID) + assert state.attributes.get(ATTR_FAN_SPEED) == fan_speed + + +@pytest.mark.parametrize( + "device_property,target_value", + [ + ("manufacturer", "Shark"), + ("model", "RV1001AE"), + ("name", "Sharknado"), + ("sw_version", "Dummy Firmware 1.0"), + ], +) +async def test_device_properties( + hass: HomeAssistant, device_property: str, target_value: str +): + """Test device properties.""" + registry = await hass.helpers.device_registry.async_get_registry() + device = registry.async_get_device({(DOMAIN, "AC000Wxxxxxxxxx")}, []) + assert getattr(device, device_property) == target_value + + +async def test_locate(hass): + """Test that the locate command works.""" + with patch.object(SharkIqVacuum, "async_find_device") as mock_locate: + data = {ATTR_ENTITY_ID: VAC_ENTITY_ID} + await hass.services.async_call("vacuum", SERVICE_LOCATE, data, blocking=True) + mock_locate.assert_called_once() + + +@pytest.mark.parametrize( + "side_effect,success", + [ + (None, True), + (SharkIqAuthError, False), + (RuntimeError, False), + ], +) +async def test_coordinator_updates( + hass: HomeAssistant, side_effect: Optional[Exception], success: bool +) -> None: + """Test the update coordinator update functions.""" + coordinator = hass.data[DOMAIN][ENTRY_ID] + + await async_setup_component(hass, "homeassistant", {}) + + with patch.object( + MockShark, "async_update", side_effect=side_effect + ) as mock_update: + data = {ATTR_ENTITY_ID: [VAC_ENTITY_ID]} + await hass.services.async_call( + "homeassistant", SERVICE_UPDATE_ENTITY, data, blocking=True + ) + assert coordinator.last_update_success == success + mock_update.assert_called_once() + + state = hass.states.get(VAC_ENTITY_ID) + assert (state.state == STATE_UNAVAILABLE) != success