diff --git a/homeassistant/components/sunweg/__init__.py b/homeassistant/components/sunweg/__init__.py index 6c39a04127e..86da0a247b1 100644 --- a/homeassistant/components/sunweg/__init__.py +++ b/homeassistant/components/sunweg/__init__.py @@ -11,6 +11,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.typing import StateType, UndefinedType from homeassistant.util import Throttle @@ -27,8 +28,7 @@ async def async_setup_entry( """Load the saved entities.""" api = APIHelper(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) if not await hass.async_add_executor_job(api.authenticate): - _LOGGER.error("Username or Password may be incorrect!") - return False + raise ConfigEntryAuthFailed("Username or Password may be incorrect!") hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SunWEGData( api, entry.data[CONF_PLANT_ID] ) diff --git a/homeassistant/components/sunweg/config_flow.py b/homeassistant/components/sunweg/config_flow.py index c4af05a0cc9..2b5e49c2cb9 100644 --- a/homeassistant/components/sunweg/config_flow.py +++ b/homeassistant/components/sunweg/config_flow.py @@ -1,6 +1,9 @@ """Config flow for Sun WEG integration.""" -from sunweg.api import APIHelper +from collections.abc import Mapping +from typing import Any + +from sunweg.api import APIHelper, SunWegApiError import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -18,37 +21,61 @@ class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialise sun weg server flow.""" self.api: APIHelper = None - self.data: dict = {} + self.data: dict[str, Any] = {} @callback - def _async_show_user_form(self, errors=None) -> ConfigFlowResult: + def _async_show_user_form(self, step_id: str, errors=None) -> ConfigFlowResult: """Show the form to the user.""" + default_username = "" + if CONF_USERNAME in self.data: + default_username = self.data[CONF_USERNAME] data_schema = vol.Schema( { - vol.Required(CONF_USERNAME): str, + vol.Required(CONF_USERNAME, default=default_username): str, vol.Required(CONF_PASSWORD): str, } ) return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id=step_id, data_schema=data_schema, errors=errors ) + def _set_auth_data( + self, step: str, username: str, password: str + ) -> ConfigFlowResult | None: + """Set username and password.""" + if self.api: + # Set username and password + self.api.username = username + self.api.password = password + else: + # Initialise the library with the username & password + self.api = APIHelper(username, password) + + try: + if not self.api.authenticate(): + return self._async_show_user_form(step, {"base": "invalid_auth"}) + except SunWegApiError: + return self._async_show_user_form(step, {"base": "timeout_connect"}) + + return None + async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle the start of the config flow.""" if not user_input: - return self._async_show_user_form() - - # Initialise the library with the username & password - self.api = APIHelper(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) - login_response = await self.hass.async_add_executor_job(self.api.authenticate) - - if not login_response: - return self._async_show_user_form({"base": "invalid_auth"}) + return self._async_show_user_form("user") # Store authentication info self.data = user_input - return await self.async_step_plant() + + conf_result = await self.hass.async_add_executor_job( + self._set_auth_data, + "user", + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + + return await self.async_step_plant() if conf_result is None else conf_result async def async_step_plant(self, user_input=None) -> ConfigFlowResult: """Handle adding a "plant" to Home Assistant.""" @@ -72,3 +99,37 @@ class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() self.data.update(user_input) return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauthorization request from SunWEG.""" + self.data.update(entry_data) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauthorization flow.""" + if user_input is None: + return self._async_show_user_form("reauth_confirm") + + self.data.update(user_input) + conf_result = await self.hass.async_add_executor_job( + self._set_auth_data, + "reauth_confirm", + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + if conf_result is not None: + return conf_result + + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + if entry is not None: + data: Mapping[str, Any] = self.data + self.hass.config_entries.async_update_entry(entry, data=data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/sunweg/strings.json b/homeassistant/components/sunweg/strings.json index 3a910e62940..6033bc314bc 100644 --- a/homeassistant/components/sunweg/strings.json +++ b/homeassistant/components/sunweg/strings.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "no_plants": "No plants have been found on this account" + "no_plants": "No plants have been found on this account", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" }, "step": { "plant": { @@ -19,6 +21,13 @@ "username": "[%key:common::config_flow::data::username%]" }, "title": "Enter your Sun WEG information" + }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + }, + "title": "[%key:common::config_flow::title::reauth%]" } } } diff --git a/tests/components/sunweg/common.py b/tests/components/sunweg/common.py index 616f5c0137f..096113f6609 100644 --- a/tests/components/sunweg/common.py +++ b/tests/components/sunweg/common.py @@ -12,6 +12,7 @@ SUNWEG_USER_INPUT = { SUNWEG_MOCK_ENTRY = MockConfigEntry( domain=DOMAIN, + unique_id=0, data={ CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password", diff --git a/tests/components/sunweg/test_config_flow.py b/tests/components/sunweg/test_config_flow.py index 84957a419dd..54ad4f3f234 100644 --- a/tests/components/sunweg/test_config_flow.py +++ b/tests/components/sunweg/test_config_flow.py @@ -2,14 +2,14 @@ from unittest.mock import patch -from sunweg.api import APIHelper +from sunweg.api import APIHelper, SunWegApiError from homeassistant import config_entries, data_entry_flow from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .common import SUNWEG_USER_INPUT +from .common import SUNWEG_MOCK_ENTRY, SUNWEG_USER_INPUT from tests.common import MockConfigEntry @@ -40,12 +40,99 @@ async def test_incorrect_login(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "invalid_auth"} -async def test_no_plants_on_account(hass: HomeAssistant) -> None: - """Test registering an integration with no plants available.""" +async def test_server_unavailable(hass: HomeAssistant) -> None: + """Test when the SunWEG server don't respond.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + with patch.object( + APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error") + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], SUNWEG_USER_INPUT + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "timeout_connect"} + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test reauth flow.""" + mock_entry = SUNWEG_MOCK_ENTRY + mock_entry.add_to_hass(hass) + + entries = hass.config_entries.async_entries() + assert len(entries) == 1 + assert entries[0].data[CONF_USERNAME] == SUNWEG_MOCK_ENTRY.data[CONF_USERNAME] + assert entries[0].data[CONF_PASSWORD] == SUNWEG_MOCK_ENTRY.data[CONF_PASSWORD] + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch.object(APIHelper, "authenticate", return_value=False): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=SUNWEG_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} + + with patch.object( + APIHelper, "authenticate", side_effect=SunWegApiError("Internal Server Error") + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=SUNWEG_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "timeout_connect"} + + with patch.object(APIHelper, "authenticate", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=SUNWEG_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + entries = hass.config_entries.async_entries() + + assert len(entries) == 1 + assert entries[0].data[CONF_USERNAME] == SUNWEG_USER_INPUT[CONF_USERNAME] + assert entries[0].data[CONF_PASSWORD] == SUNWEG_USER_INPUT[CONF_PASSWORD] + + +async def test_no_plants_on_account(hass: HomeAssistant) -> None: + """Test registering an integration with wrong auth then with no plants available.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch.object(APIHelper, "authenticate", return_value=False): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], SUNWEG_USER_INPUT + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + with ( patch.object(APIHelper, "authenticate", return_value=True), patch.object(APIHelper, "listPlants", return_value=[]), @@ -63,22 +150,21 @@ async def test_multiple_plant_ids(hass: HomeAssistant, plant_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - user_input = SUNWEG_USER_INPUT.copy() - plant_list = [plant_fixture, plant_fixture] with ( patch.object(APIHelper, "authenticate", return_value=True), - patch.object(APIHelper, "listPlants", return_value=plant_list), + patch.object( + APIHelper, "listPlants", return_value=[plant_fixture, plant_fixture] + ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], SUNWEG_USER_INPUT ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "plant" - user_input = {CONF_PLANT_ID: 123456} result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], {CONF_PLANT_ID: 123456} ) await hass.async_block_till_done() @@ -93,7 +179,6 @@ async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - user_input = SUNWEG_USER_INPUT.copy() with ( patch.object(APIHelper, "authenticate", return_value=True), @@ -104,7 +189,7 @@ async def test_one_plant_on_account(hass: HomeAssistant, plant_fixture) -> None: ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], SUNWEG_USER_INPUT ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @@ -120,7 +205,6 @@ async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - user_input = SUNWEG_USER_INPUT.copy() with ( patch.object(APIHelper, "authenticate", return_value=True), @@ -131,7 +215,7 @@ async def test_existing_plant_configured(hass: HomeAssistant, plant_fixture) -> ), ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], SUNWEG_USER_INPUT ) assert result["type"] == "abort" diff --git a/tests/components/sunweg/test_init.py b/tests/components/sunweg/test_init.py index cc2e880d82e..41edda38a5a 100644 --- a/tests/components/sunweg/test_init.py +++ b/tests/components/sunweg/test_init.py @@ -10,6 +10,7 @@ from homeassistant.components.sunweg.const import DOMAIN, DeviceType from homeassistant.components.sunweg.sensor_types.sensor_entity_description import ( SunWEGSensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -193,3 +194,16 @@ async def test_sunwegdata_get_data_never_reset() -> None: never_resets=entity_description.never_resets, previous_value_drop_threshold=entity_description.previous_value_drop_threshold, ) == (2.8, None) + + +async def test_reauth_started(hass: HomeAssistant) -> None: + """Test reauth flow started.""" + mock_entry = SUNWEG_MOCK_ENTRY + mock_entry.add_to_hass(hass) + with patch.object(APIHelper, "authenticate", return_value=False): + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert mock_entry.state is ConfigEntryState.SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm"