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:
J. Nick Koston 2020-09-16 10:35:01 -05:00 committed by GitHub
parent 540b925659
commit 5ea04d64f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 379 additions and 100 deletions

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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 @@
} }
} }
} }
} }

View File

@ -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"
}
}
} }
} }
}

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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() == []