diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 235df79b976ef5..8bfb19979862f9 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -29,6 +29,7 @@ SleepIQPauseUpdateCoordinator, SleepIQSleepDataCoordinator, ) +from .helpers import is_invalid_auth _LOGGER = logging.getLogger(__name__) @@ -51,8 +52,6 @@ }, extra=vol.ALLOW_EXTRA, ) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up sleepiq component.""" if DOMAIN in config: @@ -78,8 +77,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) -> b try: await gateway.login(email, password) except SleepIQLoginException as err: - _LOGGER.error("Could not authenticate with SleepIQ server") - raise ConfigEntryAuthFailed(err) from err + if is_invalid_auth(err): + _LOGGER.error("Could not authenticate with SleepIQ server") + raise ConfigEntryAuthFailed(err) from err + raise ConfigEntryNotReady( + str(err) or "Retryable SleepIQ login failure" + ) from err except SleepIQTimeoutException as err: raise ConfigEntryNotReady( str(err) or "Timed out during authentication" diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index 0a473404eb92e1..e4256680e1e651 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -15,6 +15,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +from .helpers import is_invalid_auth _LOGGER = logging.getLogger(__name__) @@ -117,8 +118,10 @@ async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> str gateway = AsyncSleepIQ(client_session=client_session) try: await gateway.login(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) - except SleepIQLoginException: - return "invalid_auth" + except SleepIQLoginException as err: + if is_invalid_auth(err): + return "invalid_auth" + return "cannot_connect" except SleepIQTimeoutException: return "cannot_connect" diff --git a/homeassistant/components/sleepiq/helpers.py b/homeassistant/components/sleepiq/helpers.py new file mode 100644 index 00000000000000..27ae40510f4697 --- /dev/null +++ b/homeassistant/components/sleepiq/helpers.py @@ -0,0 +1,12 @@ +"""Helpers for the SleepIQ integration.""" + +from __future__ import annotations + +from asyncsleepiq import SleepIQLoginException + +INVALID_AUTH_ERROR = "incorrect username or password" + + +def is_invalid_auth(err: SleepIQLoginException) -> bool: + """Return if a SleepIQ login exception indicates invalid credentials.""" + return INVALID_AUTH_ERROR in str(err).lower() diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index 26007d42e7dad8..f6d19ad0c13374 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -57,7 +57,14 @@ async def test_show_set_form(hass: HomeAssistant) -> None: @pytest.mark.parametrize( ("side_effect", "error"), [ - (SleepIQLoginException, "invalid_auth"), + ( + SleepIQLoginException("Incorrect username or password"), + "invalid_auth", + ), + ( + SleepIQLoginException("Connection failure: Cannot connect to host"), + "cannot_connect", + ), (SleepIQTimeoutException, "cannot_connect"), ], ) @@ -115,3 +122,38 @@ async def test_reauth_password(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + ( + SleepIQLoginException("Incorrect username or password"), + "invalid_auth", + ), + ( + SleepIQLoginException("Connection failure: Cannot connect to host"), + "cannot_connect", + ), + (SleepIQTimeoutException, "cannot_connect"), + ], +) +async def test_reauth_failure(hass: HomeAssistant, side_effect, error) -> None: + """Test reauth form errors on login failure.""" + + entry = await setup_platform(hass) + result = await entry.start_reauth_flow(hass) + + with patch( + "homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.FORM + assert result2["step_id"] == "reauth_confirm" + assert result2["errors"] == {"base": error} diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index 65e9e63a372e5e..cd3ff775e00162 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -55,9 +55,24 @@ async def test_unload_entry(hass: HomeAssistant, mock_asyncsleepiq) -> None: async def test_entry_setup_login_error(hass: HomeAssistant, mock_asyncsleepiq) -> None: """Test when sleepiq client is unable to login.""" - mock_asyncsleepiq.login.side_effect = SleepIQLoginException + mock_asyncsleepiq.login.side_effect = SleepIQLoginException( + "Incorrect username or password" + ) + entry = await setup_platform(hass, None) + assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_entry_setup_retryable_login_error( + hass: HomeAssistant, mock_asyncsleepiq +) -> None: + """Test when sleepiq client cannot connect during login.""" + mock_asyncsleepiq.login.side_effect = SleepIQLoginException( + "Connection failure: Cannot connect to host" + ) entry = await setup_platform(hass, None) assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_entry_setup_timeout_error( @@ -67,6 +82,7 @@ async def test_entry_setup_timeout_error( mock_asyncsleepiq.login.side_effect = SleepIQTimeoutException entry = await setup_platform(hass, None) assert not await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_update_interval(hass: HomeAssistant, mock_asyncsleepiq) -> None: