mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Prompt to reauth when the august password is changed or token expires (#40103)
* Prompt to reauth when the august password is changed or token expires * augment missing config flow coverage * augment test coverage * Adjust test * Update homeassistant/components/august/__init__.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * block until patch complete Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
540b925659
commit
5ea04d64f6
@ -3,13 +3,18 @@ import asyncio
|
|||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError, ClientResponseError
|
||||||
from august.authenticator import ValidationResult
|
from august.authenticator import ValidationResult
|
||||||
from august.exceptions import AugustApiAIOHTTPError
|
from august.exceptions import AugustApiAIOHTTPError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
from homeassistant.const import (
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_TIMEOUT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
HTTP_UNAUTHORIZED,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
@ -29,7 +34,7 @@ from .const import (
|
|||||||
MIN_TIME_BETWEEN_DETAIL_UPDATES,
|
MIN_TIME_BETWEEN_DETAIL_UPDATES,
|
||||||
VERIFICATION_CODE_KEY,
|
VERIFICATION_CODE_KEY,
|
||||||
)
|
)
|
||||||
from .exceptions import InvalidAuth, RequireValidation
|
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||||
from .gateway import AugustGateway
|
from .gateway import AugustGateway
|
||||||
from .subscriber import AugustSubscriberMixin
|
from .subscriber import AugustSubscriberMixin
|
||||||
|
|
||||||
@ -113,10 +118,7 @@ async def async_setup_august(hass, config_entry, august_gateway):
|
|||||||
await august_gateway.async_authenticate()
|
await august_gateway.async_authenticate()
|
||||||
except RequireValidation:
|
except RequireValidation:
|
||||||
await async_request_validation(hass, config_entry, august_gateway)
|
await async_request_validation(hass, config_entry, august_gateway)
|
||||||
return False
|
raise
|
||||||
except InvalidAuth:
|
|
||||||
_LOGGER.error("Password is no longer valid. Please set up August again")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# We still use the configurator to get a new 2fa code
|
# We still use the configurator to get a new 2fa code
|
||||||
# when needed since config_flow doesn't have a way
|
# when needed since config_flow doesn't have a way
|
||||||
@ -171,8 +173,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||||||
try:
|
try:
|
||||||
await august_gateway.async_setup(entry.data)
|
await august_gateway.async_setup(entry.data)
|
||||||
return await async_setup_august(hass, entry, august_gateway)
|
return await async_setup_august(hass, entry, august_gateway)
|
||||||
except asyncio.TimeoutError as err:
|
except ClientResponseError as err:
|
||||||
|
if err.status == HTTP_UNAUTHORIZED:
|
||||||
|
_async_start_reauth(hass, entry)
|
||||||
|
return False
|
||||||
|
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
except InvalidAuth:
|
||||||
|
_async_start_reauth(hass, entry)
|
||||||
|
return False
|
||||||
|
except RequireValidation:
|
||||||
|
return False
|
||||||
|
except (CannotConnect, asyncio.TimeoutError) as err:
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
|
|
||||||
|
def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": "reauth"},
|
||||||
|
data=entry.data,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_LOGGER.error("Password is no longer valid. Please reauthenticate")
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
@ -4,7 +4,7 @@ import logging
|
|||||||
from august.authenticator import ValidationResult
|
from august.authenticator import ValidationResult
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries, core
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -19,18 +19,8 @@ from .gateway import AugustGateway
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
|
|
||||||
vol.Required(CONF_USERNAME): str,
|
|
||||||
vol.Required(CONF_PASSWORD): str,
|
|
||||||
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_validate_input(
|
async def async_validate_input(
|
||||||
hass: core.HomeAssistant,
|
|
||||||
data,
|
data,
|
||||||
august_gateway,
|
august_gateway,
|
||||||
):
|
):
|
||||||
@ -79,6 +69,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Store an AugustGateway()."""
|
"""Store an AugustGateway()."""
|
||||||
self._august_gateway = None
|
self._august_gateway = None
|
||||||
self.user_auth_details = {}
|
self.user_auth_details = {}
|
||||||
|
self._needs_reset = False
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
@ -87,30 +78,45 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._august_gateway = AugustGateway(self.hass)
|
self._august_gateway = AugustGateway(self.hass)
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
await self._august_gateway.async_setup(user_input)
|
combined_inputs = {**self.user_auth_details, **user_input}
|
||||||
|
await self._august_gateway.async_setup(combined_inputs)
|
||||||
|
if self._needs_reset:
|
||||||
|
self._needs_reset = False
|
||||||
|
await self._august_gateway.async_reset_authentication()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
info = await async_validate_input(
|
info = await async_validate_input(
|
||||||
self.hass,
|
combined_inputs,
|
||||||
user_input,
|
|
||||||
self._august_gateway,
|
self._august_gateway,
|
||||||
)
|
)
|
||||||
await self.async_set_unique_id(user_input[CONF_USERNAME])
|
|
||||||
return self.async_create_entry(title=info["title"], data=info["data"])
|
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except RequireValidation:
|
except RequireValidation:
|
||||||
self.user_auth_details = user_input
|
self.user_auth_details.update(user_input)
|
||||||
|
|
||||||
return await self.async_step_validation()
|
return await self.async_step_validation()
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
self.user_auth_details.update(user_input)
|
||||||
|
|
||||||
|
existing_entry = await self.async_set_unique_id(
|
||||||
|
combined_inputs[CONF_USERNAME]
|
||||||
|
)
|
||||||
|
if existing_entry:
|
||||||
|
self.hass.config_entries.async_update_entry(
|
||||||
|
existing_entry, data=info["data"]
|
||||||
|
)
|
||||||
|
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||||
|
return self.async_abort(reason="reauth_successful")
|
||||||
|
return self.async_create_entry(title=info["title"], data=info["data"])
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=self._async_build_schema(), errors=errors
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_validation(self, user_input=None):
|
async def async_step_validation(self, user_input=None):
|
||||||
@ -135,3 +141,23 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
return await self.async_step_user(user_input)
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
async def async_step_reauth(self, data):
|
||||||
|
"""Handle configuration by re-auth."""
|
||||||
|
self.user_auth_details = dict(data)
|
||||||
|
self._needs_reset = True
|
||||||
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
def _async_build_schema(self):
|
||||||
|
"""Generate the config flow schema."""
|
||||||
|
base_schema = {
|
||||||
|
vol.Required(CONF_LOGIN_METHOD, default="phone"): vol.In(LOGIN_METHODS),
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.Coerce(int),
|
||||||
|
}
|
||||||
|
for key in self.user_auth_details:
|
||||||
|
if key == CONF_PASSWORD or key not in base_schema:
|
||||||
|
continue
|
||||||
|
del base_schema[key]
|
||||||
|
return vol.Schema(base_schema)
|
||||||
|
@ -2,12 +2,18 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError, ClientResponseError
|
||||||
from august.api_async import ApiAsync
|
from august.api_async import ApiAsync
|
||||||
from august.authenticator_async import AuthenticationState, AuthenticatorAsync
|
from august.authenticator_async import AuthenticationState, AuthenticatorAsync
|
||||||
|
|
||||||
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
from homeassistant.const import (
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_TIMEOUT,
|
||||||
|
CONF_USERNAME,
|
||||||
|
HTTP_UNAUTHORIZED,
|
||||||
|
)
|
||||||
from homeassistant.helpers import aiohttp_client
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -32,29 +38,14 @@ class AugustGateway:
|
|||||||
self._access_token_cache_file = None
|
self._access_token_cache_file = None
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._config = None
|
self._config = None
|
||||||
self._api = None
|
self.api = None
|
||||||
self._authenticator = None
|
self.authenticator = None
|
||||||
self._authentication = None
|
self.authentication = None
|
||||||
|
|
||||||
@property
|
|
||||||
def authenticator(self):
|
|
||||||
"""August authentication object from py-august."""
|
|
||||||
return self._authenticator
|
|
||||||
|
|
||||||
@property
|
|
||||||
def authentication(self):
|
|
||||||
"""August authentication object from py-august."""
|
|
||||||
return self._authentication
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def access_token(self):
|
def access_token(self):
|
||||||
"""Access token for the api."""
|
"""Access token for the api."""
|
||||||
return self._authentication.access_token
|
return self.authentication.access_token
|
||||||
|
|
||||||
@property
|
|
||||||
def api(self):
|
|
||||||
"""August api object from py-august."""
|
|
||||||
return self._api
|
|
||||||
|
|
||||||
def config_entry(self):
|
def config_entry(self):
|
||||||
"""Config entry."""
|
"""Config entry."""
|
||||||
@ -78,12 +69,12 @@ class AugustGateway:
|
|||||||
)
|
)
|
||||||
self._config = conf
|
self._config = conf
|
||||||
|
|
||||||
self._api = ApiAsync(
|
self.api = ApiAsync(
|
||||||
self._aiohttp_session, timeout=self._config.get(CONF_TIMEOUT)
|
self._aiohttp_session, timeout=self._config.get(CONF_TIMEOUT)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._authenticator = AuthenticatorAsync(
|
self.authenticator = AuthenticatorAsync(
|
||||||
self._api,
|
self.api,
|
||||||
self._config[CONF_LOGIN_METHOD],
|
self._config[CONF_LOGIN_METHOD],
|
||||||
self._config[CONF_USERNAME],
|
self._config[CONF_USERNAME],
|
||||||
self._config[CONF_PASSWORD],
|
self._config[CONF_PASSWORD],
|
||||||
@ -93,30 +84,47 @@ class AugustGateway:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._authenticator.async_setup_authentication()
|
await self.authenticator.async_setup_authentication()
|
||||||
|
|
||||||
async def async_authenticate(self):
|
async def async_authenticate(self):
|
||||||
"""Authenticate with the details provided to setup."""
|
"""Authenticate with the details provided to setup."""
|
||||||
self._authentication = None
|
self.authentication = None
|
||||||
try:
|
try:
|
||||||
self._authentication = await self.authenticator.async_authenticate()
|
self.authentication = await self.authenticator.async_authenticate()
|
||||||
|
if self.authentication.state == AuthenticationState.AUTHENTICATED:
|
||||||
|
# Call the locks api to verify we are actually
|
||||||
|
# authenticated because we can be authenticated
|
||||||
|
# by have no access
|
||||||
|
await self.api.async_get_operable_locks(self.access_token)
|
||||||
|
except ClientResponseError as ex:
|
||||||
|
if ex.status == HTTP_UNAUTHORIZED:
|
||||||
|
raise InvalidAuth from ex
|
||||||
|
|
||||||
|
raise CannotConnect from ex
|
||||||
except ClientError as ex:
|
except ClientError as ex:
|
||||||
_LOGGER.error("Unable to connect to August service: %s", str(ex))
|
_LOGGER.error("Unable to connect to August service: %s", str(ex))
|
||||||
raise CannotConnect from ex
|
raise CannotConnect from ex
|
||||||
|
|
||||||
if self._authentication.state == AuthenticationState.BAD_PASSWORD:
|
if self.authentication.state == AuthenticationState.BAD_PASSWORD:
|
||||||
raise InvalidAuth
|
raise InvalidAuth
|
||||||
|
|
||||||
if self._authentication.state == AuthenticationState.REQUIRES_VALIDATION:
|
if self.authentication.state == AuthenticationState.REQUIRES_VALIDATION:
|
||||||
raise RequireValidation
|
raise RequireValidation
|
||||||
|
|
||||||
if self._authentication.state != AuthenticationState.AUTHENTICATED:
|
if self.authentication.state != AuthenticationState.AUTHENTICATED:
|
||||||
_LOGGER.error(
|
_LOGGER.error("Unknown authentication state: %s", self.authentication.state)
|
||||||
"Unknown authentication state: %s", self._authentication.state
|
|
||||||
)
|
|
||||||
raise InvalidAuth
|
raise InvalidAuth
|
||||||
|
|
||||||
return self._authentication
|
return self.authentication
|
||||||
|
|
||||||
|
async def async_reset_authentication(self):
|
||||||
|
"""Remove the cache file."""
|
||||||
|
await self._hass.async_add_executor_job(self._reset_authentication)
|
||||||
|
|
||||||
|
def _reset_authentication(self):
|
||||||
|
"""Remove the cache file."""
|
||||||
|
if os.path.exists(self._access_token_cache_file):
|
||||||
|
os.unlink(self._access_token_cache_file)
|
||||||
|
|
||||||
async def async_refresh_access_token_if_needed(self):
|
async def async_refresh_access_token_if_needed(self):
|
||||||
"""Refresh the august access token if needed."""
|
"""Refresh the august access token if needed."""
|
||||||
@ -130,4 +138,4 @@ class AugustGateway:
|
|||||||
self.authentication.access_token_expires,
|
self.authentication.access_token_expires,
|
||||||
refreshed_authentication.access_token_expires,
|
refreshed_authentication.access_token_expires,
|
||||||
)
|
)
|
||||||
self._authentication = refreshed_authentication
|
self.authentication = refreshed_authentication
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
"invalid_auth": "Invalid authentication"
|
"invalid_auth": "Invalid authentication"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Account is already configured"
|
"already_configured": "Account is already configured",
|
||||||
|
"reauth_successful": "Re-authentication was successful"
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"validation": {
|
"validation": {
|
||||||
@ -28,4 +29,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,32 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"error": {
|
||||||
"already_configured": "Account is already configured"
|
"unknown": "Unexpected error",
|
||||||
|
"cannot_connect": "Failed to connect, please try again",
|
||||||
|
"invalid_auth": "Invalid authentication"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Account is already configured",
|
||||||
|
"reauth_successful": "Re-authentication was successful"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"validation": {
|
||||||
|
"title": "Two factor authentication",
|
||||||
|
"data": {
|
||||||
|
"code": "Verification code"
|
||||||
},
|
},
|
||||||
"error": {
|
"description": "Please check your {login_method} ({username}) and enter the verification code below"
|
||||||
"cannot_connect": "Failed to connect, please try again",
|
},
|
||||||
"invalid_auth": "Invalid authentication",
|
"user": {
|
||||||
"unknown": "Unexpected error"
|
"description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
|
||||||
|
"data": {
|
||||||
|
"timeout": "Timeout (seconds)",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"login_method": "Login Method"
|
||||||
},
|
},
|
||||||
"step": {
|
"title": "Setup an August account"
|
||||||
"user": {
|
}
|
||||||
"data": {
|
|
||||||
"login_method": "Login Method",
|
|
||||||
"password": "Password",
|
|
||||||
"timeout": "Timeout (seconds)",
|
|
||||||
"username": "Username"
|
|
||||||
},
|
|
||||||
"description": "If the Login Method is 'email', Username is the email address. If the Login Method is 'phone', Username is the phone number in the format '+NNNNNNNNN'.",
|
|
||||||
"title": "Setup an August account"
|
|
||||||
},
|
|
||||||
"validation": {
|
|
||||||
"data": {
|
|
||||||
"code": "Verification code"
|
|
||||||
},
|
|
||||||
"description": "Please check your {login_method} ({username}) and enter the verification code below",
|
|
||||||
"title": "Two factor authentication"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -43,12 +43,21 @@ def _mock_get_config():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_authenticator(auth_state):
|
||||||
|
"""Mock an august authenticator."""
|
||||||
|
authenticator = MagicMock()
|
||||||
|
type(authenticator).state = PropertyMock(return_value=auth_state)
|
||||||
|
return authenticator
|
||||||
|
|
||||||
|
|
||||||
@patch("homeassistant.components.august.gateway.ApiAsync")
|
@patch("homeassistant.components.august.gateway.ApiAsync")
|
||||||
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
|
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
|
||||||
async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock):
|
async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock):
|
||||||
"""Set up august integration."""
|
"""Set up august integration."""
|
||||||
authenticate_mock.side_effect = MagicMock(
|
authenticate_mock.side_effect = MagicMock(
|
||||||
return_value=_mock_august_authentication("original_token", 1234)
|
return_value=_mock_august_authentication(
|
||||||
|
"original_token", 1234, AuthenticationState.AUTHENTICATED
|
||||||
|
)
|
||||||
)
|
)
|
||||||
api_mock.return_value = api_instance
|
api_mock.return_value = api_instance
|
||||||
assert await async_setup_component(hass, DOMAIN, _mock_get_config())
|
assert await async_setup_component(hass, DOMAIN, _mock_get_config())
|
||||||
@ -185,11 +194,9 @@ async def _mock_setup_august_with_api_side_effects(hass, api_call_side_effects):
|
|||||||
return await _mock_setup_august(hass, api_instance)
|
return await _mock_setup_august(hass, api_instance)
|
||||||
|
|
||||||
|
|
||||||
def _mock_august_authentication(token_text, token_timestamp):
|
def _mock_august_authentication(token_text, token_timestamp, state):
|
||||||
authentication = MagicMock(name="august.authentication")
|
authentication = MagicMock(name="august.authentication")
|
||||||
type(authentication).state = PropertyMock(
|
type(authentication).state = PropertyMock(return_value=state)
|
||||||
return_value=AuthenticationState.AUTHENTICATED
|
|
||||||
)
|
|
||||||
type(authentication).access_token = PropertyMock(return_value=token_text)
|
type(authentication).access_token = PropertyMock(return_value=token_text)
|
||||||
type(authentication).access_token_expires = PropertyMock(
|
type(authentication).access_token_expires = PropertyMock(
|
||||||
return_value=token_timestamp
|
return_value=token_timestamp
|
||||||
|
@ -17,6 +17,7 @@ from homeassistant.components.august.exceptions import (
|
|||||||
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
||||||
|
|
||||||
from tests.async_mock import patch
|
from tests.async_mock import patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
async def test_form(hass):
|
async def test_form(hass):
|
||||||
@ -84,6 +85,29 @@ async def test_form_invalid_auth(hass):
|
|||||||
assert result2["errors"] == {"base": "invalid_auth"}
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_unexpected_exception(hass):
|
||||||
|
"""Test we handle an unexpected exception."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.august.config_flow.AugustGateway.async_authenticate",
|
||||||
|
side_effect=ValueError,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_LOGIN_METHOD: "email",
|
||||||
|
CONF_USERNAME: "my@email.tld",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "unknown"}
|
||||||
|
|
||||||
|
|
||||||
async def test_form_cannot_connect(hass):
|
async def test_form_cannot_connect(hass):
|
||||||
"""Test we handle cannot connect error."""
|
"""Test we handle cannot connect error."""
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
@ -197,3 +221,49 @@ async def test_form_needs_validate(hass):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(mock_setup.mock_calls) == 1
|
assert len(mock_setup.mock_calls) == 1
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_reauth(hass):
|
||||||
|
"""Test reauthenticate."""
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_LOGIN_METHOD: "email",
|
||||||
|
CONF_USERNAME: "my@email.tld",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
CONF_INSTALL_ID: None,
|
||||||
|
CONF_TIMEOUT: 10,
|
||||||
|
CONF_ACCESS_TOKEN_CACHE_FILE: ".my@email.tld.august.conf",
|
||||||
|
},
|
||||||
|
unique_id="my@email.tld",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "reauth"}, data=entry.data
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.august.config_flow.AugustGateway.async_authenticate",
|
||||||
|
return_value=True,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.august.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.august.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_PASSWORD: "new-test-password",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "abort"
|
||||||
|
assert result2["reason"] == "reauth_successful"
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""The gateway tests for the august platform."""
|
"""The gateway tests for the august platform."""
|
||||||
|
from august.authenticator_common import AuthenticationState
|
||||||
|
|
||||||
from homeassistant.components.august.const import DOMAIN
|
from homeassistant.components.august.const import DOMAIN
|
||||||
from homeassistant.components.august.gateway import AugustGateway
|
from homeassistant.components.august.gateway import AugustGateway
|
||||||
|
|
||||||
@ -11,6 +13,7 @@ async def test_refresh_access_token(hass):
|
|||||||
await _patched_refresh_access_token(hass, "new_token", 5678)
|
await _patched_refresh_access_token(hass, "new_token", 5678)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("homeassistant.components.august.gateway.ApiAsync.async_get_operable_locks")
|
||||||
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
|
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
|
||||||
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh")
|
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh")
|
||||||
@patch(
|
@patch(
|
||||||
@ -23,9 +26,12 @@ async def _patched_refresh_access_token(
|
|||||||
refresh_access_token_mock,
|
refresh_access_token_mock,
|
||||||
should_refresh_mock,
|
should_refresh_mock,
|
||||||
authenticate_mock,
|
authenticate_mock,
|
||||||
|
async_get_operable_locks_mock,
|
||||||
):
|
):
|
||||||
authenticate_mock.side_effect = MagicMock(
|
authenticate_mock.side_effect = MagicMock(
|
||||||
return_value=_mock_august_authentication("original_token", 1234)
|
return_value=_mock_august_authentication(
|
||||||
|
"original_token", 1234, AuthenticationState.AUTHENTICATED
|
||||||
|
)
|
||||||
)
|
)
|
||||||
august_gateway = AugustGateway(hass)
|
august_gateway = AugustGateway(hass)
|
||||||
mocked_config = _mock_get_config()
|
mocked_config = _mock_get_config()
|
||||||
@ -38,7 +44,7 @@ async def _patched_refresh_access_token(
|
|||||||
|
|
||||||
should_refresh_mock.return_value = True
|
should_refresh_mock.return_value = True
|
||||||
refresh_access_token_mock.return_value = _mock_august_authentication(
|
refresh_access_token_mock.return_value = _mock_august_authentication(
|
||||||
new_token, new_token_expire_time
|
new_token, new_token_expire_time, AuthenticationState.AUTHENTICATED
|
||||||
)
|
)
|
||||||
await august_gateway.async_refresh_access_token_if_needed()
|
await august_gateway.async_refresh_access_token_if_needed()
|
||||||
refresh_access_token_mock.assert_called()
|
refresh_access_token_mock.assert_called()
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""The tests for the august platform."""
|
"""The tests for the august platform."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from aiohttp import ClientResponseError
|
||||||
|
from august.authenticator_common import AuthenticationState
|
||||||
from august.exceptions import AugustApiAIOHTTPError
|
from august.exceptions import AugustApiAIOHTTPError
|
||||||
|
|
||||||
from homeassistant import setup
|
from homeassistant import setup
|
||||||
@ -12,7 +14,10 @@ from homeassistant.components.august.const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||||
from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY
|
from homeassistant.config_entries import (
|
||||||
|
ENTRY_STATE_SETUP_ERROR,
|
||||||
|
ENTRY_STATE_SETUP_RETRY,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
@ -30,6 +35,7 @@ from tests.async_mock import patch
|
|||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.components.august.mocks import (
|
from tests.components.august.mocks import (
|
||||||
_create_august_with_devices,
|
_create_august_with_devices,
|
||||||
|
_mock_august_authentication,
|
||||||
_mock_doorsense_enabled_august_lock_detail,
|
_mock_doorsense_enabled_august_lock_detail,
|
||||||
_mock_doorsense_missing_august_lock_detail,
|
_mock_doorsense_missing_august_lock_detail,
|
||||||
_mock_get_config,
|
_mock_get_config,
|
||||||
@ -54,8 +60,8 @@ async def test_august_is_offline(hass):
|
|||||||
side_effect=asyncio.TimeoutError,
|
side_effect=asyncio.TimeoutError,
|
||||||
):
|
):
|
||||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
|
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
@ -158,7 +164,7 @@ async def test_set_up_from_yaml(hass):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert len(mock_setup_august.mock_calls) == 1
|
assert len(mock_setup_august.mock_calls) == 1
|
||||||
call = mock_setup_august.call_args
|
call = mock_setup_august.call_args
|
||||||
args, kwargs = call
|
args, _ = call
|
||||||
imported_config_entry = args[1]
|
imported_config_entry = args[1]
|
||||||
# The import must use DEFAULT_AUGUST_CONFIG_FILE so they
|
# The import must use DEFAULT_AUGUST_CONFIG_FILE so they
|
||||||
# do not loose their token when config is migrated
|
# do not loose their token when config is migrated
|
||||||
@ -170,3 +176,133 @@ async def test_set_up_from_yaml(hass):
|
|||||||
CONF_TIMEOUT: None,
|
CONF_TIMEOUT: None,
|
||||||
CONF_USERNAME: "mocked_username",
|
CONF_USERNAME: "mocked_username",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_auth_fails(hass):
|
||||||
|
"""Config entry state is ENTRY_STATE_SETUP_ERROR when auth fails."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=_mock_get_config()[DOMAIN],
|
||||||
|
title="August august",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert hass.config_entries.flow.async_progress() == []
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
with patch(
|
||||||
|
"august.authenticator_async.AuthenticatorAsync.async_authenticate",
|
||||||
|
side_effect=ClientResponseError(None, None, status=401),
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state == ENTRY_STATE_SETUP_ERROR
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
|
||||||
|
assert flows[0]["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_bad_password(hass):
|
||||||
|
"""Config entry state is ENTRY_STATE_SETUP_ERROR when the password has been changed."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=_mock_get_config()[DOMAIN],
|
||||||
|
title="August august",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert hass.config_entries.flow.async_progress() == []
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
with patch(
|
||||||
|
"august.authenticator_async.AuthenticatorAsync.async_authenticate",
|
||||||
|
return_value=_mock_august_authentication(
|
||||||
|
"original_token", 1234, AuthenticationState.BAD_PASSWORD
|
||||||
|
),
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state == ENTRY_STATE_SETUP_ERROR
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
|
||||||
|
assert flows[0]["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_http_failure(hass):
|
||||||
|
"""Config entry state is ENTRY_STATE_SETUP_RETRY when august is offline."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=_mock_get_config()[DOMAIN],
|
||||||
|
title="August august",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert hass.config_entries.flow.async_progress() == []
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
with patch(
|
||||||
|
"august.authenticator_async.AuthenticatorAsync.async_authenticate",
|
||||||
|
side_effect=ClientResponseError(None, None, status=500),
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
|
||||||
|
|
||||||
|
assert hass.config_entries.flow.async_progress() == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unknown_auth_state(hass):
|
||||||
|
"""Config entry state is ENTRY_STATE_SETUP_ERROR when august is in an unknown auth state."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=_mock_get_config()[DOMAIN],
|
||||||
|
title="August august",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert hass.config_entries.flow.async_progress() == []
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
with patch(
|
||||||
|
"august.authenticator_async.AuthenticatorAsync.async_authenticate",
|
||||||
|
return_value=_mock_august_authentication("original_token", 1234, None),
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state == ENTRY_STATE_SETUP_ERROR
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
|
||||||
|
assert flows[0]["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_requires_validation_state(hass):
|
||||||
|
"""Config entry state is ENTRY_STATE_SETUP_ERROR when august requires validation."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=_mock_get_config()[DOMAIN],
|
||||||
|
title="August august",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert hass.config_entries.flow.async_progress() == []
|
||||||
|
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
with patch(
|
||||||
|
"august.authenticator_async.AuthenticatorAsync.async_authenticate",
|
||||||
|
return_value=_mock_august_authentication(
|
||||||
|
"original_token", 1234, AuthenticationState.REQUIRES_VALIDATION
|
||||||
|
),
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state == ENTRY_STATE_SETUP_ERROR
|
||||||
|
|
||||||
|
assert hass.config_entries.flow.async_progress() == []
|
||||||
|
Loading…
x
Reference in New Issue
Block a user