diff --git a/homeassistant/components/powerfox_local/config_flow.py b/homeassistant/components/powerfox_local/config_flow.py index 94e67a691213..bcfb71908fde 100644 --- a/homeassistant/components/powerfox_local/config_flow.py +++ b/homeassistant/components/powerfox_local/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping from typing import Any from powerfox import PowerfoxAuthenticationError, PowerfoxConnectionError, PowerfoxLocal @@ -21,6 +22,12 @@ STEP_USER_DATA_SCHEMA = vol.Schema( } ) +STEP_REAUTH_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } +) + class PowerfoxLocalConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Powerfox Local.""" @@ -33,7 +40,7 @@ class PowerfoxLocalConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the user step.""" - errors: dict[str, str] = {} + errors = {} if user_input is not None: self._host = user_input[CONF_HOST] @@ -84,6 +91,45 @@ class PowerfoxLocalConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a confirmation flow for zeroconf discovery.""" return self._async_create_entry() + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle re-authentication flow.""" + self._host = entry_data[CONF_HOST] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle re-authentication confirmation.""" + errors = {} + + if user_input is not None: + self._api_key = user_input[CONF_API_KEY] + reauth_entry = self._get_reauth_entry() + client = PowerfoxLocal( + host=reauth_entry.data[CONF_HOST], + api_key=user_input[CONF_API_KEY], + session=async_get_clientsession(self.hass), + ) + try: + await client.value() + except PowerfoxAuthenticationError: + errors["base"] = "invalid_auth" + except PowerfoxConnectionError: + errors["base"] = "cannot_connect" + else: + return self.async_update_reload_and_abort( + reauth_entry, + data_updates=user_input, + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=STEP_REAUTH_DATA_SCHEMA, + errors=errors, + ) + def _async_create_entry(self) -> ConfigFlowResult: """Create a config entry.""" return self.async_create_entry( diff --git a/homeassistant/components/powerfox_local/coordinator.py b/homeassistant/components/powerfox_local/coordinator.py index 62c7481c4187..813cd815436c 100644 --- a/homeassistant/components/powerfox_local/coordinator.py +++ b/homeassistant/components/powerfox_local/coordinator.py @@ -2,11 +2,17 @@ from __future__ import annotations -from powerfox import LocalResponse, PowerfoxConnectionError, PowerfoxLocal +from powerfox import ( + LocalResponse, + PowerfoxAuthenticationError, + PowerfoxConnectionError, + PowerfoxLocal, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -40,6 +46,12 @@ class PowerfoxLocalDataUpdateCoordinator(DataUpdateCoordinator[LocalResponse]): """Fetch data from the local poweropti.""" try: return await self.client.value() + except PowerfoxAuthenticationError as err: + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="invalid_auth", + translation_placeholders={"error": str(err)}, + ) from err except PowerfoxConnectionError as err: raise UpdateFailed( translation_domain=DOMAIN, diff --git a/homeassistant/components/powerfox_local/quality_scale.yaml b/homeassistant/components/powerfox_local/quality_scale.yaml index 14aef3642918..ce0faf0e7878 100644 --- a/homeassistant/components/powerfox_local/quality_scale.yaml +++ b/homeassistant/components/powerfox_local/quality_scale.yaml @@ -43,7 +43,7 @@ rules: integration-owner: done log-when-unavailable: done parallel-updates: done - reauthentication-flow: todo + reauthentication-flow: done test-coverage: done # Gold diff --git a/homeassistant/components/powerfox_local/strings.json b/homeassistant/components/powerfox_local/strings.json index db6c06b55241..cb19b5c0028c 100644 --- a/homeassistant/components/powerfox_local/strings.json +++ b/homeassistant/components/powerfox_local/strings.json @@ -2,13 +2,24 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "[%key:component::powerfox_local::config::step::user::data_description::api_key%]" + }, + "description": "The API key for your Poweropti device is no longer valid.", + "title": "[%key:common::config_flow::title::reauth%]" + }, "user": { "data": { "api_key": "[%key:common::config_flow::data::api_key%]", @@ -43,6 +54,9 @@ } }, "exceptions": { + "invalid_auth": { + "message": "Error while authenticating with the device: {error}" + }, "update_failed": { "message": "Error while updating the device: {error}" } diff --git a/tests/components/powerfox_local/test_config_flow.py b/tests/components/powerfox_local/test_config_flow.py index 65de963b71e1..d055b68e73e7 100644 --- a/tests/components/powerfox_local/test_config_flow.py +++ b/tests/components/powerfox_local/test_config_flow.py @@ -184,3 +184,70 @@ async def test_user_flow_exceptions( user_input={CONF_HOST: MOCK_HOST, CONF_API_KEY: MOCK_API_KEY}, ) assert result.get("type") is FlowResultType.CREATE_ENTRY + + +async def test_step_reauth( + hass: HomeAssistant, + mock_powerfox_local_client: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test re-authentication flow.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) + + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_API_KEY: "new-api-key"}, + ) + + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "reauth_successful" + + assert mock_config_entry.data[CONF_API_KEY] == "new-api-key" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (PowerfoxConnectionError, "cannot_connect"), + (PowerfoxAuthenticationError, "invalid_auth"), + ], +) +async def test_step_reauth_exceptions( + hass: HomeAssistant, + mock_powerfox_local_client: AsyncMock, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test exceptions during re-authentication flow.""" + mock_powerfox_local_client.value.side_effect = exception + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) + + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_API_KEY: "new-api-key"}, + ) + assert result.get("type") is FlowResultType.FORM + assert result.get("errors") == {"base": error} + + # Recover from error + mock_powerfox_local_client.value.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_API_KEY: "new-api-key"}, + ) + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "reauth_successful" + + assert mock_config_entry.data[CONF_API_KEY] == "new-api-key" diff --git a/tests/components/powerfox_local/test_init.py b/tests/components/powerfox_local/test_init.py index e2d71ef7f79e..f84afe66407a 100644 --- a/tests/components/powerfox_local/test_init.py +++ b/tests/components/powerfox_local/test_init.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock -from powerfox import PowerfoxConnectionError +from powerfox import PowerfoxAuthenticationError, PowerfoxConnectionError from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -43,3 +43,20 @@ async def test_config_entry_not_ready( await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_entry_exception( + hass: HomeAssistant, + mock_powerfox_local_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test ConfigEntryNotReady when API raises an exception during entry setup.""" + mock_config_entry.add_to_hass(hass) + mock_powerfox_local_client.value.side_effect = PowerfoxAuthenticationError + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm"