From f6600bbc209130cffee3b8b115e8309488eafc73 Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 13 May 2022 18:41:01 -0400 Subject: [PATCH] Add Aladdin connect config flow (#68304) * Adding flow and async * Fixes to init * Lint and type * Fixed coveragerc file * Added Test Coverage * Added Update Listener and removed unused code * Wrong integration name in init. * Nothing * Added yaml import flow * Added YAML import functionality * Added back aladdin_connect files to coverage rc * Removed commented code * Clean up error message * Update homeassistant/components/aladdin_connect/__init__.py Co-authored-by: G Johansson * Update homeassistant/components/aladdin_connect/__init__.py Co-authored-by: G Johansson * Update homeassistant/components/aladdin_connect/config_flow.py Co-authored-by: G Johansson * Updated Documentation errors * recommended change broke cover.py - backed out * Cleaned up unused defenitions * implimented recommended changes from gjohansson * Dev environment cleanup * Raised errors for better recovery, replaced removed update files, utilized PLATFORM vars to init platform * Added back removal * Added Code Owner * Fixed more comment errors and import duplicates * Added test coverage and formated code * Added test coverage for model and init * Added test_cover for full testing coverage * Added await to async call * Added missing asserts to failure tests * Updated tranlsation * Fixed wording in yaml import function, white space in const.py, return from validate_input. * Update homeassistant/components/aladdin_connect/config_flow.py Co-authored-by: Robert Svensson * "too much" whitespace * Added back mising strings.json errors * Added ConfigFlowReconfig and tests * Finished up reauth config flow and associated tests * Added reauth to strings, removed username from reauth * recommended changes, ran script.translations, added auth test to reauth * put back self.entry.data unpack. * Cleanup for error message, fixed missing "asserts" in tests * Added yaml import assertions * Fixed documentation errors in test_cover. * remove unused string. * revised tests and wording for yaml import * Documentation cleanup. * Changed sideeffect names Co-authored-by: G Johansson Co-authored-by: Robert Svensson --- .coveragerc | 2 - CODEOWNERS | 2 + .../components/aladdin_connect/__init__.py | 37 ++ .../components/aladdin_connect/config_flow.py | 134 ++++++++ .../components/aladdin_connect/const.py | 1 + .../components/aladdin_connect/cover.py | 58 ++-- .../components/aladdin_connect/manifest.json | 5 +- .../components/aladdin_connect/strings.json | 29 ++ .../aladdin_connect/translations/en.json | 27 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/aladdin_connect/__init__.py | 1 + .../aladdin_connect/test_config_flow.py | 323 ++++++++++++++++++ .../components/aladdin_connect/test_cover.py | 272 +++++++++++++++ tests/components/aladdin_connect/test_init.py | 76 +++++ .../components/aladdin_connect/test_model.py | 18 + 16 files changed, 962 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/config_flow.py create mode 100644 homeassistant/components/aladdin_connect/strings.json create mode 100644 homeassistant/components/aladdin_connect/translations/en.json create mode 100644 tests/components/aladdin_connect/__init__.py create mode 100644 tests/components/aladdin_connect/test_config_flow.py create mode 100644 tests/components/aladdin_connect/test_cover.py create mode 100644 tests/components/aladdin_connect/test_init.py create mode 100644 tests/components/aladdin_connect/test_model.py diff --git a/.coveragerc b/.coveragerc index ef7cd7b847c..7ce0e5ee299 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,5 @@ [run] source = homeassistant - omit = homeassistant/__main__.py homeassistant/helpers/signal.py @@ -42,7 +41,6 @@ omit = homeassistant/components/airtouch4/const.py homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/sensor.py - homeassistant/components/aladdin_connect/* homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 8be3146b432..d07ce028ba9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -50,6 +50,8 @@ build.json @home-assistant/supervisor /tests/components/airvisual/ @bachya /homeassistant/components/airzone/ @Noltari /tests/components/airzone/ @Noltari +/homeassistant/components/aladdin_connect/ @mkmer +/tests/components/aladdin_connect/ @mkmer /homeassistant/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alert/ @home-assistant/core diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 90196616dc5..cbd4a195a3a 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1 +1,38 @@ """The aladdin_connect component.""" +import logging +from typing import Final + +from aladdin_connect import AladdinConnectClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady + +from .const import DOMAIN + +_LOGGER: Final = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.COVER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up platform from a ConfigEntry.""" + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + acc = AladdinConnectClient(username, password) + try: + if not await hass.async_add_executor_job(acc.login): + raise ConfigEntryAuthFailed("Incorrect Password") + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + raise ConfigEntryNotReady from ex + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py new file mode 100644 index 00000000000..f912d36a3f0 --- /dev/null +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -0,0 +1,134 @@ +"""Config flow for Aladdin Connect cover integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aladdin_connect import AladdinConnectClient +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + acc = AladdinConnectClient(data[CONF_USERNAME], data[CONF_PASSWORD]) + try: + login = await hass.async_add_executor_job(acc.login) + except (TypeError, KeyError, NameError, ValueError) as ex: + raise ConnectionError from ex + else: + if not login: + raise InvalidAuth + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aladdin Connect.""" + + VERSION = 1 + entry: config_entries.ConfigEntry | None + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Aladdin Connect.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Aladdin Connect.""" + errors: dict[str, str] = {} + + if user_input: + assert self.entry is not None + password = user_input[CONF_PASSWORD] + data = { + CONF_USERNAME: self.entry.data[CONF_USERNAME], + CONF_PASSWORD: password, + } + + try: + await validate_input(self.hass, data) + except ConnectionError: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + else: + + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_PASSWORD: password, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=REAUTH_SCHEMA, + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + await validate_input(self.hass, user_input) + except ConnectionError: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + + else: + await self.async_set_unique_id( + user_input["username"].lower(), raise_on_progress=False + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title="Aladdin Connect", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import( + self, import_data: dict[str, Any] | None = None + ) -> FlowResult: + """Import Aladin Connect config from configuration.yaml.""" + return await self.async_step_user(import_data) + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index 3069680c753..7a11cf63a9e 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -16,4 +16,5 @@ STATES_MAP: Final[dict[str, str]] = { "closing": STATE_CLOSING, } +DOMAIN = "aladdin_connect" SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 05c24fc9a37..9e18abe21f6 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -7,12 +7,12 @@ from typing import Any, Final from aladdin_connect import AladdinConnectClient import voluptuous as vol -from homeassistant.components import persistent_notification from homeassistant.components.cover import ( PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, CoverDeviceClass, CoverEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, @@ -21,11 +21,12 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import NOTIFICATION_ID, NOTIFICATION_TITLE, STATES_MAP, SUPPORTED_FEATURES +from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES from .model import DoorDevice _LOGGER: Final = logging.getLogger(__name__) @@ -35,33 +36,44 @@ PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Aladdin Connect platform.""" - - username: str = config[CONF_USERNAME] - password: str = config[CONF_PASSWORD] - acc = AladdinConnectClient(username, password) - - try: - if not acc.login(): - raise ValueError("Username or Password is incorrect") - add_entities( - (AladdinDevice(acc, door) for door in acc.get_doors()), - update_before_add=True, + """Set up Aladdin Connect devices yaml depreciated.""" + _LOGGER.warning( + "Configuring Aladdin Connect through yaml is deprecated" + "Please remove it from your configuration as it has already been imported to a config entry" + ) + await hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Aladdin Connect platform.""" + acc = hass.data[DOMAIN][config_entry.entry_id] + try: + doors = await hass.async_add_executor_job(acc.get_doors) + except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) - persistent_notification.create( - hass, - f"Error: {ex}
You will need to restart hass after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) + raise ConfigEntryNotReady from ex + + async_add_entities( + (AladdinDevice(acc, door) for door in doors), + update_before_add=True, + ) class AladdinDevice(CoverEntity): @@ -71,7 +83,7 @@ class AladdinDevice(CoverEntity): _attr_supported_features = SUPPORTED_FEATURES def __init__(self, acc: AladdinConnectClient, device: DoorDevice) -> None: - """Initialize the cover.""" + """Initialize the Aladdin Connect cover.""" self._acc = acc self._device_id = device["device_id"] self._number = device["door_number"] diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index e28b2adff42..b9ea214d996 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -3,7 +3,8 @@ "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "requirements": ["aladdin_connect==0.4"], - "codeowners": [], + "codeowners": ["@mkmer"], "iot_class": "cloud_polling", - "loggers": ["aladdin_connect"] + "loggers": ["aladdin_connect"], + "config_flow": true } diff --git a/homeassistant/components/aladdin_connect/strings.json b/homeassistant/components/aladdin_connect/strings.json new file mode 100644 index 00000000000..ff42ca14bc3 --- /dev/null +++ b/homeassistant/components/aladdin_connect/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Aladdin Connect integration needs to re-authenticate your account", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/aladdin_connect/translations/en.json b/homeassistant/components/aladdin_connect/translations/en.json new file mode 100644 index 00000000000..959e88fb2ae --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The Aladdin Connect integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 70451b22001..ae2ab6339aa 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -21,6 +21,7 @@ FLOWS = { "airtouch4", "airvisual", "airzone", + "aladdin_connect", "alarmdecoder", "almond", "ambee", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85077aa295a..96ef2b55568 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -248,6 +248,9 @@ airthings_cloud==0.1.0 # homeassistant.components.airtouch4 airtouch4pyapi==1.0.5 +# homeassistant.components.aladdin_connect +aladdin_connect==0.4 + # homeassistant.components.ambee ambee==0.4.0 diff --git a/tests/components/aladdin_connect/__init__.py b/tests/components/aladdin_connect/__init__.py new file mode 100644 index 00000000000..6e108ed88df --- /dev/null +++ b/tests/components/aladdin_connect/__init__.py @@ -0,0 +1 @@ +"""The tests for Aladdin Connect platforms.""" diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py new file mode 100644 index 00000000000..37ec64ba6f0 --- /dev/null +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -0,0 +1,323 @@ +"""Test the Aladdin Connect config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.aladdin_connect.config_flow import InvalidAuth +from homeassistant.components.aladdin_connect.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Aladdin Connect" + assert result2["data"] == { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + side_effect=TypeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_already_configured(hass): + """Test we handle already configured error.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == config_entries.SOURCE_USER + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.aladdin_connect.cover.async_setup_platform", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Aladdin Connect" + assert result2["data"] == { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a successful reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-username", "password": "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.aladdin_connect.cover.async_setup_platform", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "new-password", + } + + +async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: + """Test a successful reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-username", "password": "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.aladdin_connect.cover.async_setup_platform", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=False, + ), patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_flow_other_error(hass: HomeAssistant) -> None: + """Test an unsuccessful reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-username", "password": "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.aladdin_connect.cover.async_setup_platform", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + side_effect=ValueError, + ), patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py new file mode 100644 index 00000000000..4904d904ad4 --- /dev/null +++ b/tests/components/aladdin_connect/test_cover.py @@ -0,0 +1,272 @@ +"""Test the Aladdin Connect Cover.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.aladdin_connect.const import DOMAIN +import homeassistant.components.aladdin_connect.cover as cover +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +YAML_CONFIG = {"username": "test-user", "password": "test-password"} + +DEVICE_CONFIG_OPEN = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "open", + "link_status": "Connected", +} + +DEVICE_CONFIG_OPENING = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "opening", + "link_status": "Connected", +} + +DEVICE_CONFIG_CLOSED = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "closed", + "link_status": "Connected", +} + +DEVICE_CONFIG_CLOSING = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "closing", + "link_status": "Connected", +} + +DEVICE_CONFIG_DISCONNECTED = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "open", + "link_status": "Disconnected", +} + +DEVICE_CONFIG_BAD = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "open", +} +DEVICE_CONFIG_BAD_NO_DOOR = { + "device_id": 533255, + "door_number": 2, + "name": "home", + "status": "open", + "link_status": "Disconnected", +} + + +@pytest.mark.parametrize( + "side_effect", + [ + (TypeError), + (KeyError), + (NameError), + (ValueError), + ], +) +async def test_setup_get_doors_errors( + hass: HomeAssistant, side_effect: Exception +) -> None: + """Test component setup Get Doors Errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + side_effect=side_effect, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + +@pytest.mark.parametrize( + "side_effect", + [ + (TypeError), + (KeyError), + (NameError), + (ValueError), + ], +) +async def test_setup_login_error(hass: HomeAssistant, side_effect: Exception) -> None: + """Test component setup Login Errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + side_effect=side_effect, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) is False + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + +async def test_setup_component_noerror(hass: HomeAssistant) -> None: + """Test component setup No Error.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ): + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_cover_operation(hass: HomeAssistant) -> None: + """Test component setup open cover, close cover.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_OPEN], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert COVER_DOMAIN in hass.config.components + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.open_door", + return_value=True, + ): + await hass.services.async_call( + "cover", "open_cover", {"entity_id": "cover.home"}, blocking=True + ) + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.close_door", + return_value=True, + ): + await hass.services.async_call( + "cover", "close_cover", {"entity_id": "cover.home"}, blocking=True + ) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_CLOSED], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state == STATE_CLOSED + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_OPEN], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state == STATE_OPEN + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_OPENING], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state == STATE_OPENING + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_CLOSING], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state == STATE_CLOSING + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_BAD], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_BAD_NO_DOOR], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state + + +async def test_yaml_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture): + """Test setup YAML import.""" + assert COVER_DOMAIN not in hass.config.components + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_CLOSED], + ): + await cover.async_setup_platform(hass, YAML_CONFIG, None) + await hass.async_block_till_done() + + assert "Configuring Aladdin Connect through yaml is deprecated" in caplog.text + + assert hass.config_entries.async_entries(DOMAIN) + config_data = hass.config_entries.async_entries(DOMAIN)[0].data + assert config_data[CONF_USERNAME] == "test-user" + assert config_data[CONF_PASSWORD] == "test-password" diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py new file mode 100644 index 00000000000..c9814adb051 --- /dev/null +++ b/tests/components/aladdin_connect/test_init.py @@ -0,0 +1,76 @@ +"""Test for Aladdin Connect init logic.""" +from unittest.mock import patch + +from homeassistant.components.aladdin_connect.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +YAML_CONFIG = {"username": "test-user", "password": "test-password"} + + +async def test_unload_entry(hass: HomeAssistant): + """Test successful unload of entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-user", "password": "test-password"}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ): + + assert (await async_setup_component(hass, DOMAIN, entry)) is True + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_entry_password_fail(hass: HomeAssistant): + """Test successful unload of entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-user", "password": "test-password"}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=False, + ): + + assert (await async_setup_component(hass, DOMAIN, entry)) is True + assert entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_load_and_unload(hass: HomeAssistant) -> None: + """Test loading and unloading Aladdin Connect entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ): + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert await config_entry.async_unload(hass) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.NOT_LOADED diff --git a/tests/components/aladdin_connect/test_model.py b/tests/components/aladdin_connect/test_model.py new file mode 100644 index 00000000000..e802ae53b74 --- /dev/null +++ b/tests/components/aladdin_connect/test_model.py @@ -0,0 +1,18 @@ +"""Test the Aladdin Connect model class.""" +from homeassistant.components.aladdin_connect.model import DoorDevice +from homeassistant.core import HomeAssistant + + +async def test_model(hass: HomeAssistant) -> None: + """Test model for Aladdin Connect Model.""" + test_values = { + "device_id": "1", + "door_number": "2", + "name": "my door", + "status": "good", + } + result2 = DoorDevice(test_values) + assert result2["device_id"] == "1" + assert result2["door_number"] == "2" + assert result2["name"] == "my door" + assert result2["status"] == "good"