mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Raise ConfigEntryAuthFailed
at Home Connect update auth error (#136953)
* Raise `ConfigEntryAuthFailed` on `UnauthorizedError` handling * Implement reauth flow * Add tests * Remove unnecessary code from tests
This commit is contained in:
parent
80cff85c14
commit
4a8c96471b
@ -1,7 +1,12 @@
|
||||
"""Config flow for Home Connect."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import DOMAIN
|
||||
@ -20,3 +25,29 @@ class OAuth2FlowHandler(
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({}),
|
||||
)
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Create an oauth config entry or update existing entry for reauth."""
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
data_updates=data,
|
||||
)
|
||||
return await super().async_oauth_create_entry(data)
|
||||
|
@ -26,12 +26,14 @@ from aiohomeconnect.model.error import (
|
||||
HomeConnectApiError,
|
||||
HomeConnectError,
|
||||
HomeConnectRequestError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from aiohomeconnect.model.program import EnumerateProgram
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@ -270,6 +272,12 @@ class HomeConnectCoordinator(
|
||||
"""Fetch data from Home Connect."""
|
||||
try:
|
||||
appliances = await self.client.get_home_appliances()
|
||||
except UnauthorizedError as error:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
translation_placeholders=get_dict_from_home_connect_error(error),
|
||||
) from error
|
||||
except HomeConnectError as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
@ -7,9 +7,14 @@
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The Home Connect integration needs to re-authenticate your account"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
@ -22,6 +27,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"auth_error": {
|
||||
"message": "Authentication error: {error}. Please, re-authenticate your account"
|
||||
},
|
||||
"appliance_not_found": {
|
||||
"message": "Appliance for device ID {device_id} not found"
|
||||
},
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""Test the Home Connect config flow."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
import pytest
|
||||
@ -93,3 +94,57 @@ async def test_prevent_multiple_config_entries(
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
async def test_reauth_flow(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test reauth flow."""
|
||||
result = await config_entry.start_reauth_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
"flow_id": result["flow_id"],
|
||||
"redirect_uri": "https://example.com/auth/external/callback",
|
||||
},
|
||||
)
|
||||
|
||||
_client = await hass_client_no_auth()
|
||||
resp = await _client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
assert resp.status == HTTPStatus.OK
|
||||
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||
|
||||
aioclient_mock.post(
|
||||
OAUTH2_TOKEN,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.home_connect.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
from aiohomeconnect.const import OAUTH2_TOKEN
|
||||
from aiohomeconnect.model import OptionKey, ProgramKey, SettingKey, StatusKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
from aiohomeconnect.model.error import HomeConnectError, UnauthorizedError
|
||||
import pytest
|
||||
import requests_mock
|
||||
import respx
|
||||
@ -216,7 +216,16 @@ async def test_token_refresh_success(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_state"),
|
||||
[
|
||||
(HomeConnectError(), ConfigEntryState.SETUP_RETRY),
|
||||
(UnauthorizedError("error.key"), ConfigEntryState.SETUP_ERROR),
|
||||
],
|
||||
)
|
||||
async def test_client_error(
|
||||
exception: HomeConnectError,
|
||||
expected_state: ConfigEntryState,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
@ -224,10 +233,10 @@ async def test_client_error(
|
||||
) -> None:
|
||||
"""Test client errors during setup integration."""
|
||||
client_with_exception.get_home_appliances.return_value = None
|
||||
client_with_exception.get_home_appliances.side_effect = HomeConnectError()
|
||||
client_with_exception.get_home_appliances.side_effect = exception
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert not await integration_setup(client_with_exception)
|
||||
assert config_entry.state == ConfigEntryState.SETUP_RETRY
|
||||
assert config_entry.state == expected_state
|
||||
assert client_with_exception.get_home_appliances.call_count == 1
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user