From 2ac5f8db06e6e68ce04ac10e885e6b3e1aa745b4 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 4 Jun 2024 16:00:53 +0200 Subject: [PATCH] Set unique id in aladdin connect config flow (#118798) --- .../components/aladdin_connect/config_flow.py | 28 ++- tests/components/aladdin_connect/conftest.py | 31 ++++ .../aladdin_connect/test_config_flow.py | 171 ++++++++++++++++-- 3 files changed, 208 insertions(+), 22 deletions(-) create mode 100644 tests/components/aladdin_connect/conftest.py diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index e1a7b44830d..507085fa27f 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -4,9 +4,10 @@ from collections.abc import Mapping import logging from typing import Any -import voluptuous as vol +import jwt from homeassistant.config_entries import ConfigEntry, ConfigFlowResult +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler from .const import DOMAIN @@ -35,20 +36,33 @@ class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): ) -> 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 self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an oauth config entry or update existing entry for reauth.""" - if self.reauth_entry: + token_payload = jwt.decode( + data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False} + ) + if not self.reauth_entry: + await self.async_set_unique_id(token_payload["sub"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=token_payload["username"], + data=data, + ) + + if self.reauth_entry.unique_id == token_payload["username"]: return self.async_update_reload_and_abort( self.reauth_entry, data=data, + unique_id=token_payload["sub"], ) - return await super().async_oauth_create_entry(data) + if self.reauth_entry.unique_id == token_payload["sub"]: + return self.async_update_reload_and_abort(self.reauth_entry, data=data) + + return self.async_abort(reason="wrong_account") @property def logger(self) -> logging.Logger: diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py new file mode 100644 index 00000000000..a3f8ae417e1 --- /dev/null +++ b/tests/components/aladdin_connect/conftest.py @@ -0,0 +1,31 @@ +"""Test fixtures for the Aladdin Connect Garage Door integration.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.aladdin_connect import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return an Aladdin Connect config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={}, + title="test@test.com", + unique_id="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + version=2, + ) diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 0fca87487dd..02244420925 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,10 +1,9 @@ """Test the Aladdin Connect Garage Door config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock import pytest -from homeassistant import config_entries from homeassistant.components.aladdin_connect.const import ( DOMAIN, OAUTH2_AUTHORIZE, @@ -14,16 +13,25 @@ from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigFlowResult from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator CLIENT_ID = "1234" CLIENT_SECRET = "5678" +EXAMPLE_TOKEN = ( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYWFhYWFhYS1iYmJiLWNjY2MtZGRk" + "ZC1lZWVlZWVlZWVlZWUiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW" + "1lIjoidGVzdEB0ZXN0LmNvbSJ9.CTU1YItIrUl8nSM3koJxlFJr5CjLghgc9gS6h45D8dE" +) + @pytest.fixture async def setup_credentials(hass: HomeAssistant) -> None: @@ -36,17 +44,13 @@ async def setup_credentials(hass: HomeAssistant) -> None: ) -@pytest.mark.usefixtures("current_request_with_host", "setup_credentials") -async def test_full_flow( +async def _oauth_actions( hass: HomeAssistant, + result: ConfigFlowResult, hass_client_no_auth: ClientSessionGenerator, aioclient_mock: AiohttpClientMocker, ) -> None: - """Check full flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - state = config_entry_oauth2_flow._encode_jwt( # noqa: SLF001 + state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], @@ -69,16 +73,153 @@ async def test_full_flow( OAUTH2_TOKEN, json={ "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", + "access_token": EXAMPLE_TOKEN, "type": "Bearer", "expires_in": 60, }, ) - with patch( - "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True - ) as mock_setup: - await hass.config_entries.flow.async_configure(result["flow_id"]) + +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_setup_entry: AsyncMock, +) -> None: + """Check full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test@test.com" + assert result["data"]["token"]["access_token"] == EXAMPLE_TOKEN + assert result["data"]["token"]["refresh_token"] == "mock-refresh-token" + assert result["result"].unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_entry( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we abort with duplicate entry.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_reauth( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_config_entry: MockConfigEntry, + mock_setup_entry: AsyncMock, +) -> None: + """Test reauthentication.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reauth_wrong_account( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_setup_entry: AsyncMock, +) -> None: + """Test reauthentication with wrong account.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + title="test@test.com", + unique_id="aaaaaaaa-bbbb-ffff-dddd-eeeeeeeeeeee", + version=2, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "wrong_account" + + +async def test_reauth_old_account( + hass: HomeAssistant, + hass_client_no_auth: ClientSessionGenerator, + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + setup_credentials: None, + mock_setup_entry: AsyncMock, +) -> None: + """Test reauthentication with old account.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + title="test@test.com", + unique_id="test@test.com", + version=2, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await _oauth_actions(hass, result, hass_client_no_auth, aioclient_mock) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.unique_id == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"