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>pull/40151/head
parent
540b925659
commit
5ea04d64f6
|
@ -3,13 +3,18 @@ import asyncio
|
|||
import itertools
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from august.authenticator import ValidationResult
|
||||
from august.exceptions import AugustApiAIOHTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
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.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -29,7 +34,7 @@ from .const import (
|
|||
MIN_TIME_BETWEEN_DETAIL_UPDATES,
|
||||
VERIFICATION_CODE_KEY,
|
||||
)
|
||||
from .exceptions import InvalidAuth, RequireValidation
|
||||
from .exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from .gateway import AugustGateway
|
||||
from .subscriber import AugustSubscriberMixin
|
||||
|
||||
|
@ -113,10 +118,7 @@ async def async_setup_august(hass, config_entry, august_gateway):
|
|||
await august_gateway.async_authenticate()
|
||||
except RequireValidation:
|
||||
await async_request_validation(hass, config_entry, august_gateway)
|
||||
return False
|
||||
except InvalidAuth:
|
||||
_LOGGER.error("Password is no longer valid. Please set up August again")
|
||||
return False
|
||||
raise
|
||||
|
||||
# We still use the configurator to get a new 2fa code
|
||||
# when needed since config_flow doesn't have a way
|
||||
|
@ -171,8 +173,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
try:
|
||||
await august_gateway.async_setup(entry.data)
|
||||
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
|
||||
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):
|
||||
|
|
|
@ -4,7 +4,7 @@ import logging
|
|||
from august.authenticator import ValidationResult
|
||||
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 .const import (
|
||||
|
@ -19,18 +19,8 @@ from .gateway import AugustGateway
|
|||
|
||||
_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(
|
||||
hass: core.HomeAssistant,
|
||||
data,
|
||||
august_gateway,
|
||||
):
|
||||
|
@ -79,6 +69,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
"""Store an AugustGateway()."""
|
||||
self._august_gateway = None
|
||||
self.user_auth_details = {}
|
||||
self._needs_reset = False
|
||||
super().__init__()
|
||||
|
||||
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)
|
||||
errors = {}
|
||||
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:
|
||||
info = await async_validate_input(
|
||||
self.hass,
|
||||
user_input,
|
||||
combined_inputs,
|
||||
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:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except RequireValidation:
|
||||
self.user_auth_details = user_input
|
||||
self.user_auth_details.update(user_input)
|
||||
|
||||
return await self.async_step_validation()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
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(
|
||||
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):
|
||||
|
@ -135,3 +141,23 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
self._abort_if_unique_id_configured()
|
||||
|
||||
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 logging
|
||||
import os
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from august.api_async import ApiAsync
|
||||
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 .const import (
|
||||
|
@ -32,29 +38,14 @@ class AugustGateway:
|
|||
self._access_token_cache_file = None
|
||||
self._hass = hass
|
||||
self._config = None
|
||||
self._api = None
|
||||
self._authenticator = 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
|
||||
self.api = None
|
||||
self.authenticator = None
|
||||
self.authentication = None
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
"""Access token for the api."""
|
||||
return self._authentication.access_token
|
||||
|
||||
@property
|
||||
def api(self):
|
||||
"""August api object from py-august."""
|
||||
return self._api
|
||||
return self.authentication.access_token
|
||||
|
||||
def config_entry(self):
|
||||
"""Config entry."""
|
||||
|
@ -78,12 +69,12 @@ class AugustGateway:
|
|||
)
|
||||
self._config = conf
|
||||
|
||||
self._api = ApiAsync(
|
||||
self.api = ApiAsync(
|
||||
self._aiohttp_session, timeout=self._config.get(CONF_TIMEOUT)
|
||||
)
|
||||
|
||||
self._authenticator = AuthenticatorAsync(
|
||||
self._api,
|
||||
self.authenticator = AuthenticatorAsync(
|
||||
self.api,
|
||||
self._config[CONF_LOGIN_METHOD],
|
||||
self._config[CONF_USERNAME],
|
||||
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):
|
||||
"""Authenticate with the details provided to setup."""
|
||||
self._authentication = None
|
||||
self.authentication = None
|
||||
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:
|
||||
_LOGGER.error("Unable to connect to August service: %s", str(ex))
|
||||
raise CannotConnect from ex
|
||||
|
||||
if self._authentication.state == AuthenticationState.BAD_PASSWORD:
|
||||
if self.authentication.state == AuthenticationState.BAD_PASSWORD:
|
||||
raise InvalidAuth
|
||||
|
||||
if self._authentication.state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
if self.authentication.state == AuthenticationState.REQUIRES_VALIDATION:
|
||||
raise RequireValidation
|
||||
|
||||
if self._authentication.state != AuthenticationState.AUTHENTICATED:
|
||||
_LOGGER.error(
|
||||
"Unknown authentication state: %s", self._authentication.state
|
||||
)
|
||||
if self.authentication.state != AuthenticationState.AUTHENTICATED:
|
||||
_LOGGER.error("Unknown authentication state: %s", self.authentication.state)
|
||||
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):
|
||||
"""Refresh the august access token if needed."""
|
||||
|
@ -130,4 +138,4 @@ class AugustGateway:
|
|||
self.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"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "Account is already configured"
|
||||
"already_configured": "Account is already configured",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
},
|
||||
"step": {
|
||||
"validation": {
|
||||
|
@ -28,4 +29,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,32 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Account is already configured"
|
||||
"config": {
|
||||
"error": {
|
||||
"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": {
|
||||
"cannot_connect": "Failed to connect, please try again",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
"description": "Please check your {login_method} ({username}) and enter the verification code below"
|
||||
},
|
||||
"user": {
|
||||
"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": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"title": "Setup an August account"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.AuthenticatorAsync.async_authenticate")
|
||||
async def _mock_setup_august(hass, api_instance, authenticate_mock, api_mock):
|
||||
"""Set up august integration."""
|
||||
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
|
||||
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)
|
||||
|
||||
|
||||
def _mock_august_authentication(token_text, token_timestamp):
|
||||
def _mock_august_authentication(token_text, token_timestamp, state):
|
||||
authentication = MagicMock(name="august.authentication")
|
||||
type(authentication).state = PropertyMock(
|
||||
return_value=AuthenticationState.AUTHENTICATED
|
||||
)
|
||||
type(authentication).state = PropertyMock(return_value=state)
|
||||
type(authentication).access_token = PropertyMock(return_value=token_text)
|
||||
type(authentication).access_token_expires = PropertyMock(
|
||||
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 tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_form(hass):
|
||||
|
@ -84,6 +85,29 @@ async def test_form_invalid_auth(hass):
|
|||
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):
|
||||
"""Test we handle cannot connect error."""
|
||||
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()
|
||||
assert len(mock_setup.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."""
|
||||
from august.authenticator_common import AuthenticationState
|
||||
|
||||
from homeassistant.components.august.const import DOMAIN
|
||||
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)
|
||||
|
||||
|
||||
@patch("homeassistant.components.august.gateway.ApiAsync.async_get_operable_locks")
|
||||
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.async_authenticate")
|
||||
@patch("homeassistant.components.august.gateway.AuthenticatorAsync.should_refresh")
|
||||
@patch(
|
||||
|
@ -23,9 +26,12 @@ async def _patched_refresh_access_token(
|
|||
refresh_access_token_mock,
|
||||
should_refresh_mock,
|
||||
authenticate_mock,
|
||||
async_get_operable_locks_mock,
|
||||
):
|
||||
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)
|
||||
mocked_config = _mock_get_config()
|
||||
|
@ -38,7 +44,7 @@ async def _patched_refresh_access_token(
|
|||
|
||||
should_refresh_mock.return_value = True
|
||||
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()
|
||||
refresh_access_token_mock.assert_called()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
"""The tests for the august platform."""
|
||||
import asyncio
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from august.authenticator_common import AuthenticationState
|
||||
from august.exceptions import AugustApiAIOHTTPError
|
||||
|
||||
from homeassistant import setup
|
||||
|
@ -12,7 +14,10 @@ from homeassistant.components.august.const import (
|
|||
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 (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_PASSWORD,
|
||||
|
@ -30,6 +35,7 @@ from tests.async_mock import patch
|
|||
from tests.common import MockConfigEntry
|
||||
from tests.components.august.mocks import (
|
||||
_create_august_with_devices,
|
||||
_mock_august_authentication,
|
||||
_mock_doorsense_enabled_august_lock_detail,
|
||||
_mock_doorsense_missing_august_lock_detail,
|
||||
_mock_get_config,
|
||||
|
@ -54,8 +60,8 @@ async def test_august_is_offline(hass):
|
|||
side_effect=asyncio.TimeoutError,
|
||||
):
|
||||
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
|
||||
|
||||
|
||||
|
@ -158,7 +164,7 @@ async def test_set_up_from_yaml(hass):
|
|||
await hass.async_block_till_done()
|
||||
assert len(mock_setup_august.mock_calls) == 1
|
||||
call = mock_setup_august.call_args
|
||||
args, kwargs = call
|
||||
args, _ = call
|
||||
imported_config_entry = args[1]
|
||||
# The import must use DEFAULT_AUGUST_CONFIG_FILE so they
|
||||
# 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_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…
Reference in New Issue