Add reauthentication flow for Powerfox integration (#132225)

* Add reauthentication flow for Powerfox integration

* Update quality scale
This commit is contained in:
Klaas Schoute 2024-12-04 01:48:35 +01:00 committed by GitHub
parent 7a98497710
commit 1fe2a928a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 163 additions and 8 deletions

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from typing import Any from typing import Any
from powerfox import Powerfox, PowerfoxAuthenticationError, PowerfoxConnectionError from powerfox import Powerfox, PowerfoxAuthenticationError, PowerfoxConnectionError
@ -20,6 +21,12 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
} }
) )
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
class PowerfoxConfigFlow(ConfigFlow, domain=DOMAIN): class PowerfoxConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Powerfox.""" """Handle a config flow for Powerfox."""
@ -28,7 +35,8 @@ class PowerfoxConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle the initial step.""" """Handle the initial step."""
errors: dict[str, str] = {} errors = {}
if user_input is not None: if user_input is not None:
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]}) self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
client = Powerfox( client = Powerfox(
@ -55,3 +63,40 @@ class PowerfoxConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
data_schema=STEP_USER_DATA_SCHEMA, data_schema=STEP_USER_DATA_SCHEMA,
) )
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication flow for Powerfox."""
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 flow for Powerfox."""
errors = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
client = Powerfox(
username=reauth_entry.data[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
session=async_get_clientsession(self.hass),
)
try:
await client.all_devices()
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",
description_placeholders={"email": reauth_entry.data[CONF_EMAIL]},
data_schema=STEP_REAUTH_SCHEMA,
errors=errors,
)

View File

@ -2,10 +2,17 @@
from __future__ import annotations from __future__ import annotations
from powerfox import Device, Powerfox, PowerfoxConnectionError, Poweropti from powerfox import (
Device,
Powerfox,
PowerfoxAuthenticationError,
PowerfoxConnectionError,
Poweropti,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@ -36,5 +43,7 @@ class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]):
"""Fetch data from Powerfox API.""" """Fetch data from Powerfox API."""
try: try:
return await self.client.device(device_id=self.device.id) return await self.client.device(device_id=self.device.id)
except PowerfoxConnectionError as error: except PowerfoxAuthenticationError as err:
raise UpdateFailed(error) from error raise ConfigEntryAuthFailed(err) from err
except PowerfoxConnectionError as err:
raise UpdateFailed(err) from err

View File

@ -46,7 +46,7 @@ rules:
status: exempt status: exempt
comment: | comment: |
This integration uses a coordinator to handle updates. This integration uses a coordinator to handle updates.
reauthentication-flow: todo reauthentication-flow: done
test-coverage: done test-coverage: done
# Gold # Gold

View File

@ -11,6 +11,16 @@
"email": "The email address of your Powerfox account.", "email": "The email address of your Powerfox account.",
"password": "The password of your Powerfox account." "password": "The password of your Powerfox account."
} }
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The password for {email} is no longer valid.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::powerfox::config::step::user::data_description::password%]"
}
} }
}, },
"error": { "error": {
@ -18,7 +28,8 @@
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]" "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
}, },
"entity": { "entity": {

View File

@ -1,6 +1,6 @@
"""Test the Powerfox config flow.""" """Test the Powerfox config flow."""
from unittest.mock import AsyncMock from unittest.mock import AsyncMock, patch
from powerfox import PowerfoxAuthenticationError, PowerfoxConnectionError from powerfox import PowerfoxAuthenticationError, PowerfoxConnectionError
import pytest import pytest
@ -136,6 +136,7 @@ async def test_exceptions(
assert result.get("type") is FlowResultType.FORM assert result.get("type") is FlowResultType.FORM
assert result.get("errors") == {"base": error} assert result.get("errors") == {"base": error}
# Recover from error
mock_powerfox_client.all_devices.side_effect = None mock_powerfox_client.all_devices.side_effect = None
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@ -143,3 +144,75 @@ async def test_exceptions(
user_input={CONF_EMAIL: "test@powerfox.test", CONF_PASSWORD: "test-password"}, user_input={CONF_EMAIL: "test@powerfox.test", CONF_PASSWORD: "test-password"},
) )
assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("type") is FlowResultType.CREATE_ENTRY
async def test_step_reauth(
hass: HomeAssistant,
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"
with patch(
"homeassistant.components.powerfox.config_flow.Powerfox",
autospec=True,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: "new-password"},
)
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "reauth_successful"
assert len(hass.config_entries.async_entries()) == 1
assert mock_config_entry.data[CONF_PASSWORD] == "new-password"
@pytest.mark.parametrize(
("exception", "error"),
[
(PowerfoxConnectionError, "cannot_connect"),
(PowerfoxAuthenticationError, "invalid_auth"),
],
)
async def test_step_reauth_exceptions(
hass: HomeAssistant,
mock_powerfox_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
exception: Exception,
error: str,
) -> None:
"""Test exceptions during re-authentication flow."""
mock_powerfox_client.all_devices.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_PASSWORD: "new-password"},
)
assert result.get("type") is FlowResultType.FORM
assert result.get("errors") == {"base": error}
# Recover from error
mock_powerfox_client.all_devices.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: "new-password"},
)
assert result.get("type") is FlowResultType.ABORT
assert result.get("reason") == "reauth_successful"
assert len(hass.config_entries.async_entries()) == 1
assert mock_config_entry.data[CONF_PASSWORD] == "new-password"

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from powerfox import PowerfoxConnectionError from powerfox import PowerfoxAuthenticationError, PowerfoxConnectionError
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -43,3 +43,20 @@ async def test_config_entry_not_ready(
await hass.async_block_till_done() await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_entry_exception(
hass: HomeAssistant,
mock_powerfox_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_client.device.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"