SunWEG reauth flow (#105861)

* feat(sunweg): reauth flow

* fix(sunweg): autentication as sunweg 2.1.0

* fix: configflowresult

* chore(sunweg): dedupe code

* chore(sunweg): using entry_id instead of unique_id

* test(sunweg): added test launch reauth flow

* chore(sunweg): moved test_reauth_started test

* chore(sunweg): formatting

* chore(sunweg): formating

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Lucas Mindêllo de Andrade 2024-03-28 09:53:32 -03:00 committed by GitHub
parent 596436d679
commit 5fb12c93aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 201 additions and 32 deletions

View file

@ -11,6 +11,7 @@ from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.typing import StateType, UndefinedType from homeassistant.helpers.typing import StateType, UndefinedType
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -27,8 +28,7 @@ async def async_setup_entry(
"""Load the saved entities.""" """Load the saved entities."""
api = APIHelper(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]) api = APIHelper(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
if not await hass.async_add_executor_job(api.authenticate): if not await hass.async_add_executor_job(api.authenticate):
_LOGGER.error("Username or Password may be incorrect!") raise ConfigEntryAuthFailed("Username or Password may be incorrect!")
return False
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SunWEGData( hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SunWEGData(
api, entry.data[CONF_PLANT_ID] api, entry.data[CONF_PLANT_ID]
) )

View file

@ -1,6 +1,9 @@
"""Config flow for Sun WEG integration.""" """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 import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@ -18,37 +21,61 @@ class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialise sun weg server flow.""" """Initialise sun weg server flow."""
self.api: APIHelper = None self.api: APIHelper = None
self.data: dict = {} self.data: dict[str, Any] = {}
@callback @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.""" """Show the form to the user."""
default_username = ""
if CONF_USERNAME in self.data:
default_username = self.data[CONF_USERNAME]
data_schema = vol.Schema( data_schema = vol.Schema(
{ {
vol.Required(CONF_USERNAME): str, vol.Required(CONF_USERNAME, default=default_username): str,
vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PASSWORD): str,
} }
) )
return self.async_show_form( 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: async def async_step_user(self, user_input=None) -> ConfigFlowResult:
"""Handle the start of the config flow.""" """Handle the start of the config flow."""
if not user_input: if not user_input:
return self._async_show_user_form() return self._async_show_user_form("user")
# 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"})
# Store authentication info # Store authentication info
self.data = user_input 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: async def async_step_plant(self, user_input=None) -> ConfigFlowResult:
"""Handle adding a "plant" to Home Assistant.""" """Handle adding a "plant" to Home Assistant."""
@ -72,3 +99,37 @@ class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
self.data.update(user_input) self.data.update(user_input)
return self.async_create_entry(title=self.data[CONF_NAME], data=self.data) 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")

View file

@ -1,10 +1,12 @@
{ {
"config": { "config": {
"abort": { "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": { "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": { "step": {
"plant": { "plant": {
@ -19,6 +21,13 @@
"username": "[%key:common::config_flow::data::username%]" "username": "[%key:common::config_flow::data::username%]"
}, },
"title": "Enter your Sun WEG information" "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%]"
} }
} }
} }

View file

@ -12,6 +12,7 @@ SUNWEG_USER_INPUT = {
SUNWEG_MOCK_ENTRY = MockConfigEntry( SUNWEG_MOCK_ENTRY = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
unique_id=0,
data={ data={
CONF_USERNAME: "user@email.com", CONF_USERNAME: "user@email.com",
CONF_PASSWORD: "password", CONF_PASSWORD: "password",

View file

@ -2,14 +2,14 @@
from unittest.mock import patch 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 import config_entries, data_entry_flow
from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN from homeassistant.components.sunweg.const import CONF_PLANT_ID, DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant 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 from tests.common import MockConfigEntry
@ -40,12 +40,99 @@ async def test_incorrect_login(hass: HomeAssistant) -> None:
assert result["errors"] == {"base": "invalid_auth"} assert result["errors"] == {"base": "invalid_auth"}
async def test_no_plants_on_account(hass: HomeAssistant) -> None: async def test_server_unavailable(hass: HomeAssistant) -> None:
"""Test registering an integration with no plants available.""" """Test when the SunWEG server don't respond."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} 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 ( with (
patch.object(APIHelper, "authenticate", return_value=True), patch.object(APIHelper, "authenticate", return_value=True),
patch.object(APIHelper, "listPlants", return_value=[]), 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
user_input = SUNWEG_USER_INPUT.copy()
plant_list = [plant_fixture, plant_fixture]
with ( with (
patch.object(APIHelper, "authenticate", return_value=True), 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 = 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["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "plant" assert result["step_id"] == "plant"
user_input = {CONF_PLANT_ID: 123456}
result = await hass.config_entries.flow.async_configure( 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() 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
user_input = SUNWEG_USER_INPUT.copy()
with ( with (
patch.object(APIHelper, "authenticate", return_value=True), 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 = 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 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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
user_input = SUNWEG_USER_INPUT.copy()
with ( with (
patch.object(APIHelper, "authenticate", return_value=True), 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input result["flow_id"], SUNWEG_USER_INPUT
) )
assert result["type"] == "abort" assert result["type"] == "abort"

View file

@ -10,6 +10,7 @@ from homeassistant.components.sunweg.const import DOMAIN, DeviceType
from homeassistant.components.sunweg.sensor_types.sensor_entity_description import ( from homeassistant.components.sunweg.sensor_types.sensor_entity_description import (
SunWEGSensorEntityDescription, SunWEGSensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component 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, never_resets=entity_description.never_resets,
previous_value_drop_threshold=entity_description.previous_value_drop_threshold, previous_value_drop_threshold=entity_description.previous_value_drop_threshold,
) == (2.8, None) ) == (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"