Add reauth flow to Tile (#62415)

This commit is contained in:
Aaron Bach 2021-12-20 13:11:26 -07:00 committed by GitHub
parent 9eb1a44c03
commit b051704c4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 32 deletions

View file

@ -11,7 +11,7 @@ from pytile.tile import Tile
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -68,9 +68,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session=websession, session=websession,
) )
tiles = await client.async_get_tiles() tiles = await client.async_get_tiles()
except InvalidAuthError: except InvalidAuthError as err:
LOGGER.error("Invalid credentials provided") raise ConfigEntryAuthFailed("Invalid credentials") from err
return False
except TileError as err: except TileError as err:
raise ConfigEntryNotReady("Error during integration setup") from err raise ConfigEntryNotReady("Error during integration setup") from err
@ -78,6 +77,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Update the Tile.""" """Update the Tile."""
try: try:
await tile.async_update() await tile.async_update()
except InvalidAuthError as err:
raise ConfigEntryAuthFailed("Invalid credentials") from err
except SessionExpiredError: except SessionExpiredError:
LOGGER.info("Tile session expired; creating a new one") LOGGER.info("Tile session expired; creating a new one")
await client.async_init() await client.async_init()

View file

@ -4,15 +4,29 @@ from __future__ import annotations
from typing import Any from typing import Any
from pytile import async_login from pytile import async_login
from pytile.errors import TileError from pytile.errors import InvalidAuthError, TileError
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN from .const import DOMAIN, LOGGER
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
STEP_USER_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
@ -22,37 +36,74 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the config flow.""" """Initialize the config flow."""
self.data_schema = vol.Schema( self._password: str | None = None
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} self._username: str | None = None
)
async def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult: async def _async_verify(self, step_id: str, schema: vol.Schema) -> FlowResult:
"""Show the form to the user.""" """Attempt to authenticate the provided credentials."""
return self.async_show_form( assert self._username
step_id="user", data_schema=self.data_schema, errors=errors or {} assert self._password
)
errors = {}
session = aiohttp_client.async_get_clientsession(self.hass)
try:
await async_login(self._username, self._password, session=session)
except InvalidAuthError:
errors["base"] = "invalid_auth"
except TileError as err:
LOGGER.error("Unknown Tile error: %s", err)
errors["base"] = "unknown"
if errors:
return self.async_show_form(
step_id=step_id, data_schema=schema, errors=errors
)
data = {CONF_USERNAME: self._username, CONF_PASSWORD: self._password}
if existing_entry := await self.async_set_unique_id(self._username):
self.hass.config_entries.async_update_entry(existing_entry, data=data)
self.hass.async_create_task(
self.hass.config_entries.async_reload(existing_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=self._username, data=data)
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
"""Import a config entry from configuration.yaml.""" """Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config) return await self.async_step_user(import_config)
async def async_step_reauth(self, config: ConfigType) -> FlowResult:
"""Handle configuration by re-auth."""
self._username = config[CONF_USERNAME]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle re-auth completion."""
if not user_input:
return self.async_show_form(
step_id="reauth_confirm", data_schema=STEP_REAUTH_SCHEMA
)
self._password = user_input[CONF_PASSWORD]
return await self._async_verify("reauth_confirm", STEP_REAUTH_SCHEMA)
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle the start of the config flow.""" """Handle the start of the config flow."""
if not user_input: if not user_input:
return await self._show_form() return self.async_show_form(step_id="user", data_schema=STEP_USER_SCHEMA)
await self.async_set_unique_id(user_input[CONF_USERNAME]) await self.async_set_unique_id(user_input[CONF_USERNAME])
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
session = aiohttp_client.async_get_clientsession(self.hass) self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
try: return await self._async_verify("user", STEP_USER_SCHEMA)
await async_login(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], session=session
)
except TileError:
return await self._show_form({"base": "invalid_auth"})
return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input)

View file

@ -1,6 +1,12 @@
{ {
"config": { "config": {
"step": { "step": {
"reauth_confirm": {
"title": "Re-authenticate Tile",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
},
"user": { "user": {
"title": "Configure Tile", "title": "Configure Tile",
"data": { "data": {
@ -13,7 +19,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%]"
} }
}, },
"options": { "options": {

View file

@ -1,12 +1,19 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Account is already configured" "already_configured": "Account is already configured",
"reauth_successful": "Re-authentication was successful"
}, },
"error": { "error": {
"invalid_auth": "Invalid authentication" "invalid_auth": "Invalid authentication"
}, },
"step": { "step": {
"reauth_confirm": {
"data": {
"password": "Password"
},
"title": "Re-authenticate Tile"
},
"user": { "user": {
"data": { "data": {
"password": "Password", "password": "Password",

View file

@ -1,11 +1,12 @@
"""Define tests for the Tile config flow.""" """Define tests for the Tile config flow."""
from unittest.mock import patch from unittest.mock import patch
from pytile.errors import TileError import pytest
from pytile.errors import InvalidAuthError, TileError
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.tile import DOMAIN from homeassistant.components.tile import DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -30,8 +31,15 @@ async def test_duplicate_error(hass):
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_invalid_credentials(hass): @pytest.mark.parametrize(
"""Test that invalid credentials key throws an error.""" "err,err_string",
[
(InvalidAuthError, "invalid_auth"),
(TileError, "unknown"),
],
)
async def test_errors(hass, err, err_string):
"""Test that errors are handled correctly."""
conf = { conf = {
CONF_USERNAME: "user@host.com", CONF_USERNAME: "user@host.com",
CONF_PASSWORD: "123abc", CONF_PASSWORD: "123abc",
@ -39,13 +47,13 @@ async def test_invalid_credentials(hass):
with patch( with patch(
"homeassistant.components.tile.config_flow.async_login", "homeassistant.components.tile.config_flow.async_login",
side_effect=TileError, side_effect=err,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=conf DOMAIN, context={"source": SOURCE_USER}, data=conf
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "invalid_auth"} assert result["errors"] == {"base": err_string}
async def test_step_import(hass): async def test_step_import(hass):
@ -69,6 +77,41 @@ async def test_step_import(hass):
} }
async def test_step_reauth(hass):
"""Test that the reauth step works."""
conf = {
CONF_USERNAME: "user@host.com",
CONF_PASSWORD: "123abc",
}
MockConfigEntry(domain=DOMAIN, unique_id="user@host.com", data=conf).add_to_hass(
hass
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_REAUTH},
data={CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password"},
)
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth_confirm"
with patch(
"homeassistant.components.tile.async_setup_entry", return_value=True
), patch("homeassistant.components.tile.config_flow.async_login"):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PASSWORD: "password"}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "reauth_successful"
assert len(hass.config_entries.async_entries()) == 1
async def test_step_user(hass): async def test_step_user(hass):
"""Test that the user step works.""" """Test that the user step works."""
conf = { conf = {