From e39b3796f34523d29ee82575d7d3b2f602618dad Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Wed, 28 Aug 2024 22:58:16 +1000 Subject: [PATCH] Fix OAuth reauth in Tesla Fleet (#124744) * Fix auth failure * Test * Fix test * Only reauth on 401 * Cover 401 and others * Update homeassistant/components/tesla_fleet/strings.json Co-authored-by: Jan Bouwhuis --------- Co-authored-by: Jan Bouwhuis --- .../components/tesla_fleet/__init__.py | 8 ++- .../components/tesla_fleet/config_flow.py | 5 +- .../components/tesla_fleet/strings.json | 2 +- tests/components/tesla_fleet/conftest.py | 2 +- tests/components/tesla_fleet/test_init.py | 49 ++++++++++++++++++- 5 files changed, 61 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tesla_fleet/__init__.py b/homeassistant/components/tesla_fleet/__init__.py index 47a2a9173a5..183e7e753b5 100644 --- a/homeassistant/components/tesla_fleet/__init__.py +++ b/homeassistant/components/tesla_fleet/__init__.py @@ -3,6 +3,7 @@ import asyncio from typing import Final +from aiohttp.client_exceptions import ClientResponseError import jwt from tesla_fleet_api import EnergySpecific, TeslaFleetApi, VehicleSpecific from tesla_fleet_api.const import Scope @@ -66,7 +67,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) - async def _refresh_token() -> str: async with refresh_lock: - await oauth_session.async_ensure_token_valid() + try: + await oauth_session.async_ensure_token_valid() + except ClientResponseError as e: + if e.status == 401: + raise ConfigEntryAuthFailed from e + raise ConfigEntryNotReady from e token: str = oauth_session.token[CONF_ACCESS_TOKEN] return token diff --git a/homeassistant/components/tesla_fleet/config_flow.py b/homeassistant/components/tesla_fleet/config_flow.py index 0ffdca1aec6..64b88792387 100644 --- a/homeassistant/components/tesla_fleet/config_flow.py +++ b/homeassistant/components/tesla_fleet/config_flow.py @@ -83,5 +83,8 @@ class OAuth2FlowHandler( ) -> ConfigFlowResult: """Confirm reauth dialog.""" if user_input is None: - return self.async_show_form(step_id="reauth_confirm") + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={"name": "Tesla Fleet"}, + ) return await self.async_step_user() diff --git a/homeassistant/components/tesla_fleet/strings.json b/homeassistant/components/tesla_fleet/strings.json index 6e74714ddd5..d4848836689 100644 --- a/homeassistant/components/tesla_fleet/strings.json +++ b/homeassistant/components/tesla_fleet/strings.json @@ -19,7 +19,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The Withings integration needs to re-authenticate your account" + "description": "The {name} integration needs to re-authenticate your account" } }, "create_entry": { diff --git a/tests/components/tesla_fleet/conftest.py b/tests/components/tesla_fleet/conftest.py index 49f0be9cca7..615c62fe16e 100644 --- a/tests/components/tesla_fleet/conftest.py +++ b/tests/components/tesla_fleet/conftest.py @@ -124,7 +124,7 @@ def mock_site_info() -> Generator[AsyncMock]: yield mock_live_status -@pytest.fixture(autouse=True) +@pytest.fixture def mock_find_server() -> Generator[AsyncMock]: """Mock Tesla Fleet find server method.""" with patch( diff --git a/tests/components/tesla_fleet/test_init.py b/tests/components/tesla_fleet/test_init.py index b5eb21d1cdd..9dcac4ec388 100644 --- a/tests/components/tesla_fleet/test_init.py +++ b/tests/components/tesla_fleet/test_init.py @@ -1,7 +1,9 @@ """Test the Tesla Fleet init.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch +from aiohttp import RequestInfo +from aiohttp.client_exceptions import ClientResponseError from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion @@ -16,6 +18,7 @@ from tesla_fleet_api.exceptions import ( VehicleOffline, ) +from homeassistant.components.tesla_fleet.const import AUTHORIZE_URL from homeassistant.components.tesla_fleet.coordinator import ( ENERGY_INTERVAL, ENERGY_INTERVAL_SECONDS, @@ -72,6 +75,50 @@ async def test_init_error( assert normal_config_entry.state is state +async def test_oauth_refresh_expired( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + mock_products: AsyncMock, +) -> None: + """Test init with expired Oauth token.""" + + # Patch the token refresh to raise an error + with patch( + "homeassistant.components.tesla_fleet.OAuth2Session.async_ensure_token_valid", + side_effect=ClientResponseError( + RequestInfo(AUTHORIZE_URL, "POST", {}, AUTHORIZE_URL), None, status=401 + ), + ) as mock_async_ensure_token_valid: + # Trigger an unmocked function call + mock_products.side_effect = InvalidRegion + await setup_platform(hass, normal_config_entry) + + mock_async_ensure_token_valid.assert_called_once() + assert normal_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_oauth_refresh_error( + hass: HomeAssistant, + normal_config_entry: MockConfigEntry, + mock_products: AsyncMock, +) -> None: + """Test init with Oauth refresh failure.""" + + # Patch the token refresh to raise an error + with patch( + "homeassistant.components.tesla_fleet.OAuth2Session.async_ensure_token_valid", + side_effect=ClientResponseError( + RequestInfo(AUTHORIZE_URL, "POST", {}, AUTHORIZE_URL), None, status=400 + ), + ) as mock_async_ensure_token_valid: + # Trigger an unmocked function call + mock_products.side_effect = InvalidRegion + await setup_platform(hass, normal_config_entry) + + mock_async_ensure_token_valid.assert_called_once() + assert normal_config_entry.state is ConfigEntryState.SETUP_RETRY + + # Test devices async def test_devices( hass: HomeAssistant,