From 2da73ea0687c0a7be8fa43bd71a872937aee111c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:33:04 +0100 Subject: [PATCH] Add connectivity checks to renault config flow (#131251) * Add connectivity checks to renault config flow * Parametrize * Sort * merge --- .../components/renault/config_flow.py | 41 +++++++++---------- homeassistant/components/renault/strings.json | 4 +- tests/components/renault/test_config_flow.py | 27 +++++++++--- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/renault/config_flow.py b/homeassistant/components/renault/config_flow.py index 68024a71499..70544a5637f 100644 --- a/homeassistant/components/renault/config_flow.py +++ b/homeassistant/components/renault/config_flow.py @@ -5,7 +5,9 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any +import aiohttp from renault_api.const import AVAILABLE_LOCALES +from renault_api.gigya.exceptions import GigyaException import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -27,12 +29,11 @@ REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a Renault config flow.""" - VERSION = 1 + renault_hub: RenaultHub def __init__(self) -> None: """Initialize the Renault config flow.""" self.renault_config: dict[str, Any] = {} - self.renault_hub: RenaultHub | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -41,24 +42,28 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): Ask the user for API keys. """ + errors: dict[str, str] = {} if user_input: locale = user_input[CONF_LOCALE] self.renault_config.update(user_input) self.renault_config.update(AVAILABLE_LOCALES[locale]) self.renault_hub = RenaultHub(self.hass, locale) - if not await self.renault_hub.attempt_login( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD] - ): - return self._show_user_form({"base": "invalid_credentials"}) - return await self.async_step_kamereon() - return self._show_user_form() - - def _show_user_form(self, errors: dict[str, Any] | None = None) -> ConfigFlowResult: - """Show the API keys form.""" + try: + login_success = await self.renault_hub.attempt_login( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) + except (aiohttp.ClientConnectionError, GigyaException): + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + errors["base"] = "unknown" + else: + if login_success: + return await self.async_step_kamereon() + errors["base"] = "invalid_credentials" return self.async_show_form( step_id="user", data_schema=USER_SCHEMA, - errors=errors or {}, + errors=errors, ) async def async_step_kamereon( @@ -74,18 +79,12 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): title=user_input[CONF_KAMEREON_ACCOUNT_ID], data=self.renault_config ) - assert self.renault_hub accounts = await self.renault_hub.get_account_ids() if len(accounts) == 0: return self.async_abort(reason="kamereon_no_account") if len(accounts) == 1: - await self.async_set_unique_id(accounts[0]) - self._abort_if_unique_id_configured() - - self.renault_config[CONF_KAMEREON_ACCOUNT_ID] = accounts[0] - return self.async_create_entry( - title=self.renault_config[CONF_KAMEREON_ACCOUNT_ID], - data=self.renault_config, + return await self.async_step_kamereon( + user_input={CONF_KAMEREON_ACCOUNT_ID: accounts[0]} ) return self.async_show_form( @@ -122,6 +121,6 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="reauth_confirm", data_schema=REAUTH_SCHEMA, - errors=errors or {}, + errors=errors, description_placeholders={CONF_USERNAME: reauth_entry.data[CONF_USERNAME]}, ) diff --git a/homeassistant/components/renault/strings.json b/homeassistant/components/renault/strings.json index 9cc34edb82f..90463d75478 100644 --- a/homeassistant/components/renault/strings.json +++ b/homeassistant/components/renault/strings.json @@ -6,7 +6,9 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { "kamereon": { diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index ce69cc4a5c0..56e0c8a99d7 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, PropertyMock, patch +import aiohttp import pytest from renault_api.gigya.exceptions import InvalidCredentialsException from renault_api.kamereon import schemas @@ -23,20 +24,35 @@ from tests.common import MockConfigEntry, load_fixture pytestmark = pytest.mark.usefixtures("mock_setup_entry") +@pytest.mark.parametrize( + ("exception", "error"), + [ + (Exception, "unknown"), + (aiohttp.ClientConnectionError, "cannot_connect"), + ( + InvalidCredentialsException(403042, "invalid loginID or password"), + "invalid_credentials", + ), + ], +) async def test_config_flow_single_account( - hass: HomeAssistant, mock_setup_entry: AsyncMock + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + exception: Exception | type[Exception], + error: str, ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {} + assert result["step_id"] == "user" + assert not result["errors"] - # Failed credentials + # Raise error with patch( "renault_api.renault_session.RenaultSession.login", - side_effect=InvalidCredentialsException(403042, "invalid loginID or password"), + side_effect=exception, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -48,7 +64,8 @@ async def test_config_flow_single_account( ) assert result["type"] is FlowResultType.FORM - assert result["errors"] == {"base": "invalid_credentials"} + assert result["step_id"] == "user" + assert result["errors"] == {"base": error} renault_account = AsyncMock() type(renault_account).account_id = PropertyMock(return_value="account_id_1")