From 5ea04d64f601a0d2851fd20b847c42d8331efe84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2020 10:35:01 -0500 Subject: [PATCH] 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 * block until patch complete Co-authored-by: Martin Hjelmare --- homeassistant/components/august/__init__.py | 40 ++++- .../components/august/config_flow.py | 62 +++++--- homeassistant/components/august/gateway.py | 78 +++++----- homeassistant/components/august/strings.json | 5 +- .../components/august/translations/en.json | 55 +++---- tests/components/august/mocks.py | 17 ++- tests/components/august/test_config_flow.py | 70 +++++++++ tests/components/august/test_gateway.py | 10 +- tests/components/august/test_init.py | 142 +++++++++++++++++- 9 files changed, 379 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index e0d7749dcbb..feaf61450e8 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -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): diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index bf6f1d9cd81..f595479c0cf 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -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) diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index 6918907611f..b72bb52e710 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -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 diff --git a/homeassistant/components/august/strings.json b/homeassistant/components/august/strings.json index 880c13c7fe2..254e8146984 100644 --- a/homeassistant/components/august/strings.json +++ b/homeassistant/components/august/strings.json @@ -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 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/august/translations/en.json b/homeassistant/components/august/translations/en.json index b8bf1b1bc03..254e8146984 100644 --- a/homeassistant/components/august/translations/en.json +++ b/homeassistant/components/august/translations/en.json @@ -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" + } } -} \ No newline at end of file + } +} diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index c471dfca2a9..93b64ebbd3f 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -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 diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index 2f20347acae..1c23976a6f9 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -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 diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index ec035b9ec38..c1aa0723baa 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -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() diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index bcc05e51c71..f954ff83c25 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -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() == []