From 36aefce9e1088f8ffb86c1a3f73827abcb6168fd Mon Sep 17 00:00:00 2001 From: ryanjones-gentex Date: Tue, 16 Dec 2025 11:14:49 -0500 Subject: [PATCH] Store unique user configurations for HomeLink integration (#159111) --- .../components/gentex_homelink/config_flow.py | 10 +++-- .../components/gentex_homelink/coordinator.py | 5 +-- tests/components/gentex_homelink/__init__.py | 7 ++++ tests/components/gentex_homelink/conftest.py | 6 ++- .../gentex_homelink/test_config_flow.py | 38 ++++++++++++++++--- 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/gentex_homelink/config_flow.py b/homeassistant/components/gentex_homelink/config_flow.py index dceb79143e8..512f9df5278 100644 --- a/homeassistant/components/gentex_homelink/config_flow.py +++ b/homeassistant/components/gentex_homelink/config_flow.py @@ -5,6 +5,7 @@ from typing import Any import botocore.exceptions from homelink.auth.srp_auth import SRPAuth +import jwt import voluptuous as vol from homeassistant.config_entries import ConfigFlowResult @@ -38,8 +39,6 @@ class SRPFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Ask for username and password.""" errors: dict[str, str] = {} if user_input is not None: - self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]}) - srp_auth = SRPAuth() try: tokens = await self.hass.async_add_executor_job( @@ -48,12 +47,17 @@ class SRPFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): user_input[CONF_PASSWORD], ) except botocore.exceptions.ClientError: - _LOGGER.exception("Error authenticating homelink account") errors["base"] = "srp_auth_failed" except Exception: _LOGGER.exception("An unexpected error occurred") errors["base"] = "unknown" else: + access_token = jwt.decode( + tokens["AuthenticationResult"]["AccessToken"], + options={"verify_signature": False}, + ) + await self.async_set_unique_id(access_token["sub"]) + self._abort_if_unique_id_configured() self.external_data = {"tokens": tokens} return await self.async_step_creation() diff --git a/homeassistant/components/gentex_homelink/coordinator.py b/homeassistant/components/gentex_homelink/coordinator.py index 61daf71ba0e..9e03b16fc79 100644 --- a/homeassistant/components/gentex_homelink/coordinator.py +++ b/homeassistant/components/gentex_homelink/coordinator.py @@ -1,10 +1,9 @@ -"""Makes requests to the state server and stores the resulting data so that the buttons can access it.""" +"""Establish MQTT connection and listen for event data.""" from __future__ import annotations from collections.abc import Callable from functools import partial -import logging from typing import TypedDict from homelink.model.device import Device @@ -14,8 +13,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.util.ssl import get_default_context -_LOGGER = logging.getLogger(__name__) - type HomeLinkConfigEntry = ConfigEntry[HomeLinkCoordinator] type EventCallback = Callable[[HomeLinkEventData], None] diff --git a/tests/components/gentex_homelink/__init__.py b/tests/components/gentex_homelink/__init__.py index fb5f94f953d..d887d88772f 100644 --- a/tests/components/gentex_homelink/__init__.py +++ b/tests/components/gentex_homelink/__init__.py @@ -3,10 +3,17 @@ from typing import Any from unittest.mock import AsyncMock +import jwt + +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +TEST_CREDENTIALS = {CONF_EMAIL: "test@test.com", CONF_PASSWORD: "SomePassword"} + +TEST_ACCESS_JWT = jwt.encode({"sub": "some-uuid"}, key="secret") + async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None: """Set up the homelink integration for testing.""" diff --git a/tests/components/gentex_homelink/conftest.py b/tests/components/gentex_homelink/conftest.py index 0fc9e9f0943..c9adcc6081d 100644 --- a/tests/components/gentex_homelink/conftest.py +++ b/tests/components/gentex_homelink/conftest.py @@ -9,6 +9,8 @@ import pytest from homeassistant.components.gentex_homelink import DOMAIN +from . import TEST_ACCESS_JWT + from tests.common import MockConfigEntry @@ -21,7 +23,7 @@ def mock_srp_auth() -> Generator[AsyncMock]: instance = mock_srp_auth.return_value instance.async_get_access_token.return_value = { "AuthenticationResult": { - "AccessToken": "access", + "AccessToken": TEST_ACCESS_JWT, "RefreshToken": "refresh", "TokenType": "bearer", "ExpiresIn": 3600, @@ -60,6 +62,8 @@ def mock_device() -> AsyncMock: def mock_config_entry() -> MockConfigEntry: """Mock setup entry.""" return MockConfigEntry( + unique_id="some-uuid", + version=1, domain=DOMAIN, data={ "auth_implementation": "gentex_homelink", diff --git a/tests/components/gentex_homelink/test_config_flow.py b/tests/components/gentex_homelink/test_config_flow.py index b8c8ce361f4..4e2bffd3d25 100644 --- a/tests/components/gentex_homelink/test_config_flow.py +++ b/tests/components/gentex_homelink/test_config_flow.py @@ -7,10 +7,13 @@ import pytest from homeassistant.components.gentex_homelink.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import TEST_ACCESS_JWT, TEST_CREDENTIALS, setup_integration + +from tests.common import MockConfigEntry + async def test_full_flow( hass: HomeAssistant, mock_srp_auth: AsyncMock, mock_setup_entry: AsyncMock @@ -26,13 +29,13 @@ async def test_full_flow( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_EMAIL: "test@test.com", CONF_PASSWORD: "SomePassword"}, + user_input=TEST_CREDENTIALS, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "auth_implementation": "gentex_homelink", "token": { - "access_token": "access", + "access_token": TEST_ACCESS_JWT, "refresh_token": "refresh", "expires_in": 3600, "token_type": "bearer", @@ -40,6 +43,31 @@ async def test_full_flow( }, } assert result["title"] == "SRPAuth" + assert result["result"].unique_id == "some-uuid" + + +async def test_unique_configurations( + hass: HomeAssistant, + mock_srp_auth: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Check full flow.""" + await setup_integration(hass, mock_config_entry) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert not result["errors"] + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=TEST_CREDENTIALS, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" @pytest.mark.parametrize( @@ -69,7 +97,7 @@ async def test_exceptions( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_EMAIL: "test@test.com", CONF_PASSWORD: "SomePassword"}, + user_input=TEST_CREDENTIALS, ) assert result["type"] is FlowResultType.FORM @@ -79,6 +107,6 @@ async def test_exceptions( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_EMAIL: "test@test.com", CONF_PASSWORD: "SomePassword"}, + user_input=TEST_CREDENTIALS, ) assert result["type"] is FlowResultType.CREATE_ENTRY