diff --git a/homeassistant/components/peblar/__init__.py b/homeassistant/components/peblar/__init__.py index 43c48e28bd0..a055a1a02c8 100644 --- a/homeassistant/components/peblar/__init__.py +++ b/homeassistant/components/peblar/__init__.py @@ -15,7 +15,7 @@ from peblar import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: PeblarConfigEntry) -> bo except PeblarConnectionError as err: raise ConfigEntryNotReady("Could not connect to Peblar charger") from err except PeblarAuthenticationError as err: - raise ConfigEntryError("Could not login to Peblar charger") from err + raise ConfigEntryAuthFailed from err except PeblarError as err: raise ConfigEntryNotReady( "Unknown error occurred while connecting to Peblar charger" diff --git a/homeassistant/components/peblar/config_flow.py b/homeassistant/components/peblar/config_flow.py index a9cfb7d89b9..809cb13746e 100644 --- a/homeassistant/components/peblar/config_flow.py +++ b/homeassistant/components/peblar/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping from typing import Any from aiohttp import CookieJar @@ -129,3 +130,53 @@ class PeblarFlowHandler(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle initiation of re-authentication with a Peblar device.""" + 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 with a Peblar device.""" + errors = {} + + if user_input is not None: + reauth_entry = self._get_reauth_entry() + peblar = Peblar( + host=reauth_entry.data[CONF_HOST], + session=async_create_clientsession( + self.hass, cookie_jar=CookieJar(unsafe=True) + ), + ) + try: + await peblar.login(password=user_input[CONF_PASSWORD]) + except PeblarAuthenticationError: + errors[CONF_PASSWORD] = "invalid_auth" + except PeblarConnectionError: + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + reauth_entry, + data={ + CONF_HOST: reauth_entry.data[CONF_HOST], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + } + ), + errors=errors, + ) diff --git a/homeassistant/components/peblar/quality_scale.yaml b/homeassistant/components/peblar/quality_scale.yaml index 3dc470ce76b..2b0684793a8 100644 --- a/homeassistant/components/peblar/quality_scale.yaml +++ b/homeassistant/components/peblar/quality_scale.yaml @@ -36,7 +36,7 @@ rules: integration-owner: done log-when-unavailable: done parallel-updates: todo - reauthentication-flow: todo + reauthentication-flow: done test-coverage: todo # Gold devices: todo diff --git a/homeassistant/components/peblar/strings.json b/homeassistant/components/peblar/strings.json index 01022a19c38..0cce7ed8191 100644 --- a/homeassistant/components/peblar/strings.json +++ b/homeassistant/components/peblar/strings.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "no_serial_number": "The discovered Peblar device did not provide a serial number." + "no_serial_number": "The discovered Peblar device did not provide a serial number.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -10,6 +11,15 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "[%key:component::peblar::config::step::user::data_description::password%]" + }, + "description": "Reauthenticate with your Peblar RV charger.\n\nTo do so, you will need to enter your new password you use to log into Peblar's device web interface." + }, "user": { "data": { "host": "[%key:common::config_flow::data::host%]", diff --git a/tests/components/peblar/test_config_flow.py b/tests/components/peblar/test_config_flow.py index 4e3ab008047..a4a461b6bba 100644 --- a/tests/components/peblar/test_config_flow.py +++ b/tests/components/peblar/test_config_flow.py @@ -319,3 +319,75 @@ async def test_user_flow_with_zeroconf_in_progress(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert not hass.config_entries.flow.async_progress() + + +@pytest.mark.usefixtures("mock_peblar") +async def test_reauth_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the reauthentication configuration flow.""" + mock_config_entry.add_to_hass(hass) + assert mock_config_entry.data[CONF_PASSWORD] == "OMGSPIDERS" + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "OMGPUPPIES"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.127", + CONF_PASSWORD: "OMGPUPPIES", + } + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (PeblarConnectionError, {"base": "cannot_connect"}), + (PeblarAuthenticationError, {CONF_PASSWORD: "invalid_auth"}), + (Exception, {"base": "unknown"}), + ], +) +async def test_reauth_flow_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_peblar: MagicMock, + side_effect: Exception, + expected_error: dict[str, str], +) -> None: + """Test we show form on a error.""" + mock_config_entry.add_to_hass(hass) + mock_peblar.login.side_effect = side_effect + + result = await mock_config_entry.start_reauth_flow(hass) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == expected_error + + mock_peblar.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "OMGPUPPIES", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" diff --git a/tests/components/peblar/test_init.py b/tests/components/peblar/test_init.py index ca7b0d88c24..6e6a9c2af05 100644 --- a/tests/components/peblar/test_init.py +++ b/tests/components/peblar/test_init.py @@ -7,7 +7,7 @@ import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.peblar.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -70,6 +70,17 @@ async def test_config_entry_authentication_failed( assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == mock_config_entry.entry_id + @pytest.mark.usefixtures("init_integration") async def test_peblar_device_entry(