mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +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."""
|
"""Config flow for Home Connect."""
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
import logging
|
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 homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -20,3 +25,29 @@ class OAuth2FlowHandler(
|
|||||||
def logger(self) -> logging.Logger:
|
def logger(self) -> logging.Logger:
|
||||||
"""Return logger."""
|
"""Return logger."""
|
||||||
return logging.getLogger(__name__)
|
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,
|
HomeConnectApiError,
|
||||||
HomeConnectError,
|
HomeConnectError,
|
||||||
HomeConnectRequestError,
|
HomeConnectRequestError,
|
||||||
|
UnauthorizedError,
|
||||||
)
|
)
|
||||||
from aiohomeconnect.model.program import EnumerateProgram
|
from aiohomeconnect.model.program import EnumerateProgram
|
||||||
from propcache.api import cached_property
|
from propcache.api import cached_property
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@ -270,6 +272,12 @@ class HomeConnectCoordinator(
|
|||||||
"""Fetch data from Home Connect."""
|
"""Fetch data from Home Connect."""
|
||||||
try:
|
try:
|
||||||
appliances = await self.client.get_home_appliances()
|
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:
|
except HomeConnectError as error:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
|
@ -7,9 +7,14 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"pick_implementation": {
|
"pick_implementation": {
|
||||||
"title": "[%key:common::config_flow::title::oauth2_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": {
|
"abort": {
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||||
@ -22,6 +27,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
"auth_error": {
|
||||||
|
"message": "Authentication error: {error}. Please, re-authenticate your account"
|
||||||
|
},
|
||||||
"appliance_not_found": {
|
"appliance_not_found": {
|
||||||
"message": "Appliance for device ID {device_id} not found"
|
"message": "Appliance for device ID {device_id} not found"
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""Test the Home Connect config flow."""
|
"""Test the Home Connect config flow."""
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from unittest.mock import patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||||
import pytest
|
import pytest
|
||||||
@ -93,3 +94,57 @@ async def test_prevent_multiple_config_entries(
|
|||||||
|
|
||||||
assert result["type"] == "abort"
|
assert result["type"] == "abort"
|
||||||
assert result["reason"] == "single_instance_allowed"
|
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.const import OAUTH2_TOKEN
|
||||||
from aiohomeconnect.model import OptionKey, ProgramKey, SettingKey, StatusKey
|
from aiohomeconnect.model import OptionKey, ProgramKey, SettingKey, StatusKey
|
||||||
from aiohomeconnect.model.error import HomeConnectError
|
from aiohomeconnect.model.error import HomeConnectError, UnauthorizedError
|
||||||
import pytest
|
import pytest
|
||||||
import requests_mock
|
import requests_mock
|
||||||
import respx
|
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(
|
async def test_client_error(
|
||||||
|
exception: HomeConnectError,
|
||||||
|
expected_state: ConfigEntryState,
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||||
setup_credentials: None,
|
setup_credentials: None,
|
||||||
@ -224,10 +233,10 @@ async def test_client_error(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test client errors during setup integration."""
|
"""Test client errors during setup integration."""
|
||||||
client_with_exception.get_home_appliances.return_value = None
|
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 config_entry.state == ConfigEntryState.NOT_LOADED
|
||||||
assert not await integration_setup(client_with_exception)
|
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
|
assert client_with_exception.get_home_appliances.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user