mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add Images scope to Home Connect (#178485)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any, override
|
||||
from typing import Any, Final, override
|
||||
|
||||
import jwt
|
||||
import voluptuous as vol
|
||||
@@ -13,6 +13,8 @@ from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
INPUT_IMAGES_SCOPE: Final = "images_scope"
|
||||
|
||||
|
||||
class OAuth2FlowHandler(
|
||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
||||
@@ -23,12 +25,49 @@ class OAuth2FlowHandler(
|
||||
|
||||
MINOR_VERSION = 3
|
||||
|
||||
images_scope: bool | None = None
|
||||
|
||||
@property
|
||||
@override
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
@override
|
||||
def extra_authorize_data(self) -> dict[str, str]:
|
||||
return {
|
||||
"scope": (
|
||||
"Control Monitor Settings"
|
||||
f" IdentifyAppliance{' Images' if self.images_scope else ''}"
|
||||
),
|
||||
}
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow start."""
|
||||
return await self.async_step_scopes(user_input)
|
||||
|
||||
async def async_step_scopes(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Ask for the scopes to use."""
|
||||
if user_input is not None:
|
||||
self.images_scope = user_input[INPUT_IMAGES_SCOPE]
|
||||
if self.images_scope is not None:
|
||||
return await self.async_step_pick_implementation(None)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="scopes",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(INPUT_IMAGES_SCOPE): bool,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"wrong_account": "Please ensure you reconfigure against the same account."
|
||||
},
|
||||
"create_entry": {
|
||||
@@ -38,6 +39,16 @@
|
||||
"reauth_confirm": {
|
||||
"description": "The Home Connect integration needs to re-authenticate your account",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"scopes": {
|
||||
"data": {
|
||||
"images_scope": "Images scope"
|
||||
},
|
||||
"data_description": {
|
||||
"images_scope": "Allows Home Assistant to access images from your Home Connect devices."
|
||||
},
|
||||
"description": "Select the optional scopes you want to enable for Home Connect authentication.",
|
||||
"title": "Scopes"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import MagicMock, patch
|
||||
from urllib.parse import parse_qsl, urlsplit
|
||||
|
||||
from aiohomeconnect.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
from aiohomeconnect.model import HomeAppliance
|
||||
@@ -25,6 +26,23 @@ from tests.typing import ClientSessionGenerator
|
||||
CLIENT_ID = "1234"
|
||||
CLIENT_SECRET = "5678"
|
||||
|
||||
|
||||
def assert_authorize_url(url: str, state: str, images_scope: bool | None) -> None:
|
||||
"""Assert the generated OAuth authorize URL."""
|
||||
split_url = urlsplit(url)
|
||||
|
||||
assert (
|
||||
f"{split_url.scheme}://{split_url.netloc}{split_url.path}" == OAUTH2_AUTHORIZE
|
||||
)
|
||||
assert dict(parse_qsl(split_url.query)) == {
|
||||
"response_type": "code",
|
||||
"client_id": CLIENT_ID,
|
||||
"redirect_uri": "https://example.com/auth/external/callback",
|
||||
"state": state,
|
||||
"scope": f"Control Monitor Settings IdentifyAppliance{' Images' if images_scope else ''}",
|
||||
}
|
||||
|
||||
|
||||
DHCP_DISCOVERY = (
|
||||
DhcpServiceInfo(
|
||||
ip="1.1.1.1",
|
||||
@@ -95,10 +113,14 @@ DHCP_DISCOVERY = (
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
@pytest.mark.parametrize(
|
||||
"images_scope", [True, False], ids=["images_scope", "no_images_scope"]
|
||||
)
|
||||
async def test_full_flow(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
images_scope: bool,
|
||||
) -> None:
|
||||
"""Check full flow."""
|
||||
assert await setup.async_setup_component(hass, "home_connect", {})
|
||||
@@ -106,6 +128,13 @@ async def test_full_flow(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context=ConfigFlowContext(source=config_entries.SOURCE_USER)
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "scopes"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"images_scope": images_scope}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
@@ -115,11 +144,7 @@ async def test_full_flow(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.EXTERNAL_STEP
|
||||
assert result["url"] == (
|
||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}"
|
||||
)
|
||||
assert_authorize_url(result["url"], state, images_scope)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
@@ -161,6 +186,13 @@ async def test_prevent_reconfiguring_same_account(
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context=ConfigFlowContext(source=config_entries.SOURCE_USER)
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "scopes"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"images_scope": True}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
@@ -170,11 +202,7 @@ async def test_prevent_reconfiguring_same_account(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.EXTERNAL_STEP
|
||||
assert result["url"] == (
|
||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}"
|
||||
)
|
||||
assert_authorize_url(result["url"], state, True)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
@@ -214,6 +242,13 @@ async def test_reauth_flow(
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "scopes"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"images_scope": False}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
@@ -268,6 +303,13 @@ async def test_reauth_flow_with_different_account(
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "scopes"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"images_scope": True}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
@@ -323,6 +365,13 @@ async def test_zeroconf_flow(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "scopes"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"images_scope": True}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
@@ -332,11 +381,7 @@ async def test_zeroconf_flow(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.EXTERNAL_STEP
|
||||
assert result["url"] == (
|
||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}"
|
||||
)
|
||||
assert_authorize_url(result["url"], state, True)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
@@ -406,6 +451,13 @@ async def test_dhcp_flow(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "scopes"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={"images_scope": True}
|
||||
)
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
@@ -414,11 +466,7 @@ async def test_dhcp_flow(
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.EXTERNAL_STEP
|
||||
assert result["url"] == (
|
||||
f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}"
|
||||
)
|
||||
assert_authorize_url(result["url"], state, True)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
|
||||
Reference in New Issue
Block a user