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.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]
)

View File

@ -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")

View File

@ -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%]"
}
}
}

View File

@ -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",

View File

@ -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"

View File

@ -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"