1
mirror of https://github.com/home-assistant/core synced 2024-09-06 10:29:55 +02:00

Add reauth support to flume (#49991)

This commit is contained in:
J. Nick Koston 2021-05-03 07:30:22 -10:00 committed by GitHub
parent c49fa6f1ed
commit 302cab185d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 192 additions and 56 deletions

View File

@ -1,7 +1,4 @@
"""The flume integration."""
from functools import partial
import logging
from pyflume import FlumeAuth, FlumeDeviceList
from requests import Session
from requests.exceptions import RequestException
@ -14,7 +11,7 @@ from homeassistant.const import (
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import (
BASE_TOKEN_FILENAME,
@ -25,12 +22,9 @@ from .const import (
PLATFORMS,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up flume from a config entry."""
def _setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Config entry set up in executor."""
config = entry.data
username = config[CONF_USERNAME]
@ -42,32 +36,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
http_session = Session()
try:
flume_auth = await hass.async_add_executor_job(
partial(
FlumeAuth,
username,
password,
client_id,
client_secret,
flume_token_file=flume_token_full_path,
http_session=http_session,
)
)
flume_devices = await hass.async_add_executor_job(
partial(
FlumeDeviceList,
flume_auth,
http_session=http_session,
)
flume_auth = FlumeAuth(
username,
password,
client_id,
client_secret,
flume_token_file=flume_token_full_path,
http_session=http_session,
)
flume_devices = FlumeDeviceList(flume_auth, http_session=http_session)
except RequestException as ex:
raise ConfigEntryNotReady from ex
except Exception as ex: # pylint: disable=broad-except
_LOGGER.error("Invalid credentials for flume: %s", ex)
return False
raise ConfigEntryAuthFailed from ex
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
return flume_auth, flume_devices, http_session
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up flume from a config entry."""
flume_auth, flume_devices, http_session = await hass.async_add_executor_job(
_setup_entry, hass, entry
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
FLUME_DEVICES: flume_devices,
FLUME_AUTH: flume_auth,
FLUME_HTTP_SESSION: http_session,

View File

@ -1,6 +1,6 @@
"""Config flow for flume integration."""
from functools import partial
import logging
import os
from pyflume import FlumeAuth, FlumeDeviceList
from requests.exceptions import RequestException
@ -33,38 +33,46 @@ DATA_SCHEMA = vol.Schema(
)
async def validate_input(hass: core.HomeAssistant, data):
def _validate_input(hass: core.HomeAssistant, data: dict, clear_token_file: bool):
"""Validate in the executor."""
flume_token_full_path = hass.config.path(
f"{BASE_TOKEN_FILENAME}-{data[CONF_USERNAME]}"
)
if clear_token_file and os.path.exists(flume_token_full_path):
os.unlink(flume_token_full_path)
return FlumeDeviceList(
FlumeAuth(
data[CONF_USERNAME],
data[CONF_PASSWORD],
data[CONF_CLIENT_ID],
data[CONF_CLIENT_SECRET],
flume_token_file=flume_token_full_path,
)
)
async def validate_input(
hass: core.HomeAssistant, data: dict, clear_token_file: bool = False
):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
username = data[CONF_USERNAME]
password = data[CONF_PASSWORD]
client_id = data[CONF_CLIENT_ID]
client_secret = data[CONF_CLIENT_SECRET]
flume_token_full_path = hass.config.path(f"{BASE_TOKEN_FILENAME}-{username}")
try:
flume_auth = await hass.async_add_executor_job(
partial(
FlumeAuth,
username,
password,
client_id,
client_secret,
flume_token_file=flume_token_full_path,
)
flume_devices = await hass.async_add_executor_job(
_validate_input, hass, data, clear_token_file
)
flume_devices = await hass.async_add_executor_job(FlumeDeviceList, flume_auth)
except RequestException as err:
raise CannotConnect from err
except Exception as err:
_LOGGER.exception("Auth exception")
raise InvalidAuth from err
if not flume_devices or not flume_devices.device_list:
raise CannotConnect
# Return info that you want to store in the config entry.
return {"title": username}
return {"title": data[CONF_USERNAME]}
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@ -72,6 +80,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self):
"""Init flume config flow."""
self._reauth_unique_id = None
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
@ -85,10 +97,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
errors[CONF_PASSWORD] = "invalid_auth"
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
@ -98,6 +107,43 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle import."""
return await self.async_step_user(user_input)
async def async_step_reauth(self, user_input=None):
"""Handle reauth."""
self._reauth_unique_id = self.context["unique_id"]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
"""Handle reauth input."""
errors = {}
existing_entry = await self.async_set_unique_id(self._reauth_unique_id)
if user_input is not None:
new_data = {**existing_entry.data, CONF_PASSWORD: user_input[CONF_PASSWORD]}
try:
await validate_input(self.hass, new_data, clear_token_file=True)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors[CONF_PASSWORD] = "invalid_auth"
else:
self.hass.config_entries.async_update_entry(
existing_entry, data=new_data
)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
description_placeholders={
CONF_USERNAME: existing_entry.data[CONF_USERNAME]
},
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@ -15,9 +15,17 @@
"client_id": "Client ID",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"reauth_confirm": {
"description": "The password for {username} is no longer valid.",
"title": "Reauthenticate your Flume Account",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}

View File

@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "Account is already configured"
"already_configured": "Account is already configured",
"reauth_successful": "Re-authentication was successful"
},
"error": {
"cannot_connect": "Failed to connect",
@ -9,6 +10,13 @@
"unknown": "Unexpected error"
},
"step": {
"reauth_confirm": {
"data": {
"password": "Password"
},
"description": "The password for {username} is no longer valid.",
"title": "Reauthenticate your Flume Account"
},
"user": {
"data": {
"client_id": "Client ID",

View File

@ -12,6 +12,8 @@ from homeassistant.const import (
CONF_USERNAME,
)
from tests.common import MockConfigEntry
def _get_mocked_flume_device_list():
flume_device_list_mock = MagicMock()
@ -124,7 +126,7 @@ async def test_form_invalid_auth(hass):
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"}
assert result2["errors"] == {"password": "invalid_auth"}
async def test_form_cannot_connect(hass):
@ -151,3 +153,82 @@ async def test_form_cannot_connect(hass):
assert result2["type"] == "form"
assert result2["errors"] == {"base": "cannot_connect"}
async def test_reauth(hass):
"""Test we can reauth."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_USERNAME: "test@test.org",
CONF_CLIENT_ID: "client_id",
CONF_CLIENT_SECRET: "client_secret",
},
unique_id="test@test.org",
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_REAUTH, "unique_id": "test@test.org"},
)
assert result["type"] == "form"
assert result["step_id"] == "reauth_confirm"
with patch(
"homeassistant.components.flume.config_flow.FlumeAuth",
return_value=True,
), patch(
"homeassistant.components.flume.config_flow.FlumeDeviceList",
side_effect=Exception,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_PASSWORD: "test-password",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"password": "invalid_auth"}
with patch(
"homeassistant.components.flume.config_flow.FlumeAuth",
return_value=True,
), patch(
"homeassistant.components.flume.config_flow.FlumeDeviceList",
side_effect=requests.exceptions.ConnectionError(),
):
result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"],
{
CONF_PASSWORD: "test-password",
},
)
assert result3["type"] == "form"
assert result3["errors"] == {"base": "cannot_connect"}
mock_flume_device_list = _get_mocked_flume_device_list()
with patch(
"homeassistant.components.flume.config_flow.FlumeAuth",
return_value=True,
), patch(
"homeassistant.components.flume.config_flow.FlumeDeviceList",
return_value=mock_flume_device_list,
), patch(
"homeassistant.components.flume.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result4 = await hass.config_entries.flow.async_configure(
result3["flow_id"],
{
CONF_PASSWORD: "test-password",
},
)
assert mock_setup_entry.called
assert result4["type"] == "abort"
assert result4["reason"] == "reauth_successful"