Implement reauth for smarttub (#47628)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Matt Zimmerman 2021-04-22 00:28:24 -07:00 committed by GitHub
parent c10836fcee
commit 8c52dfa1c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 183 additions and 58 deletions

View File

@ -10,44 +10,84 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from .const import DOMAIN from .const import DOMAIN
from .controller import SmartTubController from .controller import SmartTubController
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema( DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str}
) )
_LOGGER = logging.getLogger(__name__)
class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""SmartTub configuration flow.""" """SmartTub configuration flow."""
VERSION = 1 VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
def __init__(self) -> None:
"""Instantiate config flow."""
super().__init__()
self._reauth_input = None
self._reauth_entry = None
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user.""" """Handle a flow initiated by the user."""
errors = {} errors = {}
if user_input is not None:
controller = SmartTubController(self.hass)
try:
account = await controller.login(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except LoginFailed:
errors["base"] = "invalid_auth"
else:
await self.async_set_unique_id(account.id)
if self._reauth_input is None:
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
# this is a reauth attempt
if self._reauth_entry.unique_id != self.unique_id:
# there is a config entry matching this account, but it is not the one we were trying to reauth
return self.async_abort(reason="already_configured")
self.hass.config_entries.async_update_entry(
self._reauth_entry, data=user_input
)
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_reauth(self, user_input=None):
"""Get new credentials if the current ones don't work anymore."""
self._reauth_input = dict(user_input)
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(self, user_input=None):
"""Dialog that informs the user that reauth is required."""
if user_input is None: if user_input is None:
# same as DATA_SCHEMA but with default email
data_schema = vol.Schema(
{
vol.Required(
CONF_EMAIL, default=self._reauth_input.get(CONF_EMAIL)
): 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="reauth_confirm",
data_schema=data_schema,
) )
return await self.async_step_user(user_input)
controller = SmartTubController(self.hass)
try:
account = await controller.login(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except LoginFailed:
errors["base"] = "invalid_auth"
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
existing_entry = await self.async_set_unique_id(account.id)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=user_input[CONF_EMAIL], data=user_input)

View File

@ -25,3 +25,5 @@ ATTR_LIGHTS = "lights"
ATTR_PUMPS = "pumps" ATTR_PUMPS = "pumps"
ATTR_REMINDERS = "reminders" ATTR_REMINDERS = "reminders"
ATTR_STATUS = "status" ATTR_STATUS = "status"
CONF_CONFIG_ENTRY = "config_entry"

View File

@ -10,7 +10,7 @@ from smarttub import APIError, LoginFailed, SmartTub
from smarttub.api import Account from smarttub.api import Account
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -52,10 +52,9 @@ class SmartTubController:
self._account = await self.login( self._account = await self.login(
entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD] entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD]
) )
except LoginFailed: except LoginFailed as ex:
# credentials were changed or invalidated, we need new ones # credentials were changed or invalidated, we need new ones
raise ConfigEntryAuthFailed from ex
return False
except ( except (
asyncio.TimeoutError, asyncio.TimeoutError,
client_exceptions.ClientOSError, client_exceptions.ClientOSError,

View File

@ -8,13 +8,17 @@
"email": "[%key:common::config_flow::data::email%]", "email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
} }
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The SmartTub integration needs to re-authenticate your account"
} }
}, },
"error": { "error": {
"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_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
} }

View File

@ -1,14 +1,17 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Device is already configured", "already_configured": "Account is already configured",
"reauth_successful": "Re-authentication was successful" "reauth_successful": "Re-authentication was successful"
}, },
"error": { "error": {
"invalid_auth": "Invalid authentication", "invalid_auth": "Invalid authentication"
"unknown": "Unexpected error"
}, },
"step": { "step": {
"reauth_confirm": {
"description": "The SmartTub integration needs to re-authenticate your account",
"title": "Reauthenticate Integration"
},
"user": { "user": {
"data": { "data": {
"email": "Email", "email": "Email",

View File

@ -3,8 +3,11 @@ from unittest.mock import patch
from smarttub import LoginFailed from smarttub import LoginFailed
from homeassistant import config_entries from homeassistant import config_entries, data_entry_flow
from homeassistant.components.smarttub.const import DOMAIN from homeassistant.components.smarttub.const import DOMAIN
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from tests.common import MockConfigEntry
async def test_form(hass): async def test_form(hass):
@ -19,29 +22,19 @@ async def test_form(hass):
"homeassistant.components.smarttub.async_setup_entry", "homeassistant.components.smarttub.async_setup_entry",
return_value=True, return_value=True,
) as mock_setup_entry: ) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"email": "test-email", "password": "test-password"}, {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"},
) )
assert result2["type"] == "create_entry" assert result["type"] == "create_entry"
assert result2["title"] == "test-email" assert result["title"] == "test-email"
assert result2["data"] == { assert result["data"] == {
"email": "test-email", CONF_EMAIL: "test-email",
"password": "test-password", CONF_PASSWORD: "test-password",
} }
await hass.async_block_till_done() await hass.async_block_till_done()
mock_setup_entry.assert_called_once() mock_setup_entry.assert_called_once()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {"email": "test-email2", "password": "test-password2"}
)
assert result2["type"] == "abort"
assert result2["reason"] == "reauth_successful"
async def test_form_invalid_auth(hass, smarttub_api): async def test_form_invalid_auth(hass, smarttub_api):
@ -52,10 +45,81 @@ async def test_form_invalid_auth(hass, smarttub_api):
smarttub_api.login.side_effect = LoginFailed smarttub_api.login.side_effect = LoginFailed
result2 = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{"email": "test-email", "password": "test-password"}, {CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"},
) )
assert result2["type"] == "form" assert result["type"] == "form"
assert result2["errors"] == {"base": "invalid_auth"} assert result["errors"] == {"base": "invalid_auth"}
async def test_reauth_success(hass, smarttub_api, account):
"""Test reauthentication flow."""
mock_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_EMAIL: "test-email", CONF_PASSWORD: "test-password"},
unique_id=account.id,
)
mock_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": mock_entry.unique_id,
"entry_id": mock_entry.entry_id,
},
data=mock_entry.data,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_EMAIL: "test-email3", CONF_PASSWORD: "test-password3"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "reauth_successful"
assert mock_entry.data[CONF_EMAIL] == "test-email3"
assert mock_entry.data[CONF_PASSWORD] == "test-password3"
async def test_reauth_wrong_account(hass, smarttub_api, account):
"""Test reauthentication flow if the user enters credentials for a different already-configured account."""
mock_entry1 = MockConfigEntry(
domain=DOMAIN,
data={CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"},
unique_id=account.id,
)
mock_entry1.add_to_hass(hass)
mock_entry2 = MockConfigEntry(
domain=DOMAIN,
data={CONF_EMAIL: "test-email2", CONF_PASSWORD: "test-password2"},
unique_id="mockaccount2",
)
mock_entry2.add_to_hass(hass)
# we try to reauth account #2, and the user successfully authenticates to account #1
account.id = mock_entry1.unique_id
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": mock_entry2.unique_id,
"entry_id": mock_entry2.entry_id,
},
data=mock_entry2.data,
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"

View File

@ -1,13 +1,16 @@
"""Test smarttub setup process.""" """Test smarttub setup process."""
import asyncio import asyncio
from unittest.mock import patch
from smarttub import LoginFailed from smarttub import LoginFailed
from homeassistant.components import smarttub from homeassistant.components import smarttub
from homeassistant.components.smarttub.const import DOMAIN
from homeassistant.config_entries import ( from homeassistant.config_entries import (
ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_ERROR,
ENTRY_STATE_SETUP_RETRY, ENTRY_STATE_SETUP_RETRY,
SOURCE_REAUTH,
) )
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -35,8 +38,18 @@ async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_a
smarttub_api.login.side_effect = LoginFailed smarttub_api.login.side_effect = LoginFailed
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id) with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init:
assert config_entry.state == ENTRY_STATE_SETUP_ERROR await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state == ENTRY_STATE_SETUP_ERROR
mock_flow_init.assert_called_with(
DOMAIN,
context={
"source": SOURCE_REAUTH,
"entry_id": config_entry.entry_id,
"unique_id": config_entry.unique_id,
},
data=config_entry.data,
)
async def test_config_passed_to_config_entry(hass, config_entry, config_data): async def test_config_passed_to_config_entry(hass, config_entry, config_data):