1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-18 07:56:03 +01:00

Use Tesla Fleet API for Tessie config flow validation (#167021)

This commit is contained in:
Brett Adams
2026-04-01 16:29:07 +10:00
committed by GitHub
parent 899b776e54
commit 2b1c93724f
2 changed files with 64 additions and 65 deletions

View File

@@ -3,15 +3,16 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
from http import HTTPStatus
from typing import Any from typing import Any
from aiohttp import ClientConnectionError, ClientResponseError from aiohttp import ClientConnectionError
from tessie_api import get_state_of_all_vehicles from tesla_fleet_api.exceptions import InvalidToken, MissingToken, TeslaFleetError
from tesla_fleet_api.tessie import Tessie
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .const import DOMAIN
@@ -23,6 +24,24 @@ DESCRIPTION_PLACEHOLDERS = {
} }
async def _async_validate_access_token(
hass: HomeAssistant, access_token: str, *, only_active: bool = False
) -> dict[str, str]:
"""Validate a Tessie access token."""
try:
await Tessie(async_get_clientsession(hass), access_token).list_vehicles(
only_active=only_active
)
except InvalidToken, MissingToken:
return {CONF_ACCESS_TOKEN: "invalid_access_token"}
except ClientConnectionError:
return {"base": "cannot_connect"}
except TeslaFleetError:
return {"base": "unknown"}
return {}
class TessieConfigFlow(ConfigFlow, domain=DOMAIN): class TessieConfigFlow(ConfigFlow, domain=DOMAIN):
"""Config Tessie API connection.""" """Config Tessie API connection."""
@@ -35,20 +54,10 @@ class TessieConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input: if user_input:
self._async_abort_entries_match(dict(user_input)) self._async_abort_entries_match(dict(user_input))
try: errors = await _async_validate_access_token(
await get_state_of_all_vehicles( self.hass, user_input[CONF_ACCESS_TOKEN], only_active=True
session=async_get_clientsession(self.hass), )
api_key=user_input[CONF_ACCESS_TOKEN], if not errors:
only_active=True,
)
except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED:
errors[CONF_ACCESS_TOKEN] = "invalid_access_token"
else:
errors["base"] = "unknown"
except ClientConnectionError:
errors["base"] = "cannot_connect"
else:
return self.async_create_entry( return self.async_create_entry(
title="Tessie", title="Tessie",
data=user_input, data=user_input,
@@ -74,19 +83,10 @@ class TessieConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input: if user_input:
try: errors = await _async_validate_access_token(
await get_state_of_all_vehicles( self.hass, user_input[CONF_ACCESS_TOKEN]
session=async_get_clientsession(self.hass), )
api_key=user_input[CONF_ACCESS_TOKEN], if not errors:
)
except ClientResponseError as e:
if e.status == HTTPStatus.UNAUTHORIZED:
errors[CONF_ACCESS_TOKEN] = "invalid_access_token"
else:
errors["base"] = "unknown"
except ClientConnectionError:
errors["base"] = "cannot_connect"
else:
return self.async_update_reload_and_abort( return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=user_input self._get_reauth_entry(), data=user_input
) )

View File

@@ -1,8 +1,10 @@
"""Test the Tessie config flow.""" """Test the Tessie config flow."""
from unittest.mock import patch from collections.abc import Iterator
from unittest.mock import AsyncMock, patch
import pytest import pytest
from tesla_fleet_api.exceptions import InvalidToken, MissingToken, TeslaFleetError
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.tessie.const import DOMAIN from homeassistant.components.tessie.const import DOMAIN
@@ -10,29 +12,23 @@ from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from .common import ( from .common import ERROR_CONNECTION, TEST_CONFIG, TEST_STATE_OF_ALL_VEHICLES
ERROR_AUTH,
ERROR_CONNECTION,
ERROR_UNKNOWN,
TEST_CONFIG,
TEST_STATE_OF_ALL_VEHICLES,
)
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_config_flow_get_state_of_all_vehicles(): def mock_config_flow_list_vehicles() -> Iterator[AsyncMock]:
"""Mock get_state_of_all_vehicles in config flow.""" """Mock Tessie.list_vehicles in config flow."""
with patch( with patch(
"homeassistant.components.tessie.config_flow.get_state_of_all_vehicles", "homeassistant.components.tessie.config_flow.Tessie.list_vehicles",
return_value=TEST_STATE_OF_ALL_VEHICLES, return_value=TEST_STATE_OF_ALL_VEHICLES,
) as mock_config_flow_get_state_of_all_vehicles: ) as mock_list_vehicles:
yield mock_config_flow_get_state_of_all_vehicles yield mock_list_vehicles
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_async_setup_entry(): def mock_async_setup_entry() -> Iterator[AsyncMock]:
"""Mock async_setup_entry.""" """Mock async_setup_entry."""
with patch( with patch(
"homeassistant.components.tessie.async_setup_entry", "homeassistant.components.tessie.async_setup_entry",
@@ -43,8 +39,8 @@ def mock_async_setup_entry():
async def test_form( async def test_form(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_flow_get_state_of_all_vehicles, mock_config_flow_list_vehicles: AsyncMock,
mock_async_setup_entry, mock_async_setup_entry: AsyncMock,
) -> None: ) -> None:
"""Test we get the form.""" """Test we get the form."""
@@ -60,7 +56,7 @@ async def test_form(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_async_setup_entry.mock_calls) == 1 assert len(mock_async_setup_entry.mock_calls) == 1
assert len(mock_config_flow_get_state_of_all_vehicles.mock_calls) == 1 assert len(mock_config_flow_list_vehicles.mock_calls) == 1
assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == "Tessie" assert result2["title"] == "Tessie"
@@ -69,8 +65,6 @@ async def test_form(
async def test_abort( async def test_abort(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_flow_get_state_of_all_vehicles,
mock_async_setup_entry,
) -> None: ) -> None:
"""Test a duplicate entry aborts.""" """Test a duplicate entry aborts."""
@@ -97,13 +91,17 @@ async def test_abort(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("side_effect", "error"), ("side_effect", "error"),
[ [
(ERROR_AUTH, {CONF_ACCESS_TOKEN: "invalid_access_token"}), (InvalidToken(), {CONF_ACCESS_TOKEN: "invalid_access_token"}),
(ERROR_UNKNOWN, {"base": "unknown"}), (MissingToken(), {CONF_ACCESS_TOKEN: "invalid_access_token"}),
(TeslaFleetError(), {"base": "unknown"}),
(ERROR_CONNECTION, {"base": "cannot_connect"}), (ERROR_CONNECTION, {"base": "cannot_connect"}),
], ],
) )
async def test_form_errors( async def test_form_errors(
hass: HomeAssistant, side_effect, error, mock_config_flow_get_state_of_all_vehicles hass: HomeAssistant,
side_effect: BaseException,
error: dict[str, str],
mock_config_flow_list_vehicles: AsyncMock,
) -> None: ) -> None:
"""Test errors are handled.""" """Test errors are handled."""
@@ -111,7 +109,7 @@ async def test_form_errors(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
mock_config_flow_get_state_of_all_vehicles.side_effect = side_effect mock_config_flow_list_vehicles.side_effect = side_effect
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result1["flow_id"], result1["flow_id"],
TEST_CONFIG, TEST_CONFIG,
@@ -121,7 +119,7 @@ async def test_form_errors(
assert result2["errors"] == error assert result2["errors"] == error
# Complete the flow # Complete the flow
mock_config_flow_get_state_of_all_vehicles.side_effect = None mock_config_flow_list_vehicles.side_effect = None
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
TEST_CONFIG, TEST_CONFIG,
@@ -132,8 +130,8 @@ async def test_form_errors(
async def test_reauth( async def test_reauth(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_flow_get_state_of_all_vehicles, mock_config_flow_list_vehicles: AsyncMock,
mock_async_setup_entry, mock_async_setup_entry: AsyncMock,
) -> None: ) -> None:
"""Test reauth flow.""" """Test reauth flow."""
@@ -155,7 +153,7 @@ async def test_reauth(
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(mock_async_setup_entry.mock_calls) == 1 assert len(mock_async_setup_entry.mock_calls) == 1
assert len(mock_config_flow_get_state_of_all_vehicles.mock_calls) == 1 assert len(mock_config_flow_list_vehicles.mock_calls) == 1
assert result2["type"] is FlowResultType.ABORT assert result2["type"] is FlowResultType.ABORT
assert result2["reason"] == "reauth_successful" assert result2["reason"] == "reauth_successful"
@@ -165,21 +163,22 @@ async def test_reauth(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("side_effect", "error"), ("side_effect", "error"),
[ [
(ERROR_AUTH, {CONF_ACCESS_TOKEN: "invalid_access_token"}), (InvalidToken(), {CONF_ACCESS_TOKEN: "invalid_access_token"}),
(ERROR_UNKNOWN, {"base": "unknown"}), (MissingToken(), {CONF_ACCESS_TOKEN: "invalid_access_token"}),
(TeslaFleetError(), {"base": "unknown"}),
(ERROR_CONNECTION, {"base": "cannot_connect"}), (ERROR_CONNECTION, {"base": "cannot_connect"}),
], ],
) )
async def test_reauth_errors( async def test_reauth_errors(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_flow_get_state_of_all_vehicles, mock_config_flow_list_vehicles: AsyncMock,
mock_async_setup_entry, mock_async_setup_entry: AsyncMock,
side_effect, side_effect: BaseException,
error, error: dict[str, str],
) -> None: ) -> None:
"""Test reauth flows that fail.""" """Test reauth flows that fail."""
mock_config_flow_get_state_of_all_vehicles.side_effect = side_effect mock_config_flow_list_vehicles.side_effect = side_effect
mock_entry = MockConfigEntry( mock_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@@ -199,7 +198,7 @@ async def test_reauth_errors(
assert result2["errors"] == error assert result2["errors"] == error
# Complete the flow # Complete the flow
mock_config_flow_get_state_of_all_vehicles.side_effect = None mock_config_flow_list_vehicles.side_effect = None
result3 = await hass.config_entries.flow.async_configure( result3 = await hass.config_entries.flow.async_configure(
result2["flow_id"], result2["flow_id"],
TEST_CONFIG, TEST_CONFIG,