mirror of
https://github.com/home-assistant/core.git
synced 2025-12-24 21:06:19 +00:00
Use new method to get the access token in the Volvo integration (#151625)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -4,9 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from volvocarsapi.api import VolvoCarsApi
|
||||
from volvocarsapi.models import VolvoAuthException, VolvoCarsVehicle
|
||||
from volvocarsapi.models import VolvoApiException, VolvoAuthException, VolvoCarsVehicle
|
||||
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -69,22 +68,22 @@ async def _async_auth_and_create_api(
|
||||
oauth_session = OAuth2Session(hass, entry, implementation)
|
||||
web_session = async_get_clientsession(hass)
|
||||
auth = VolvoAuth(web_session, oauth_session)
|
||||
|
||||
try:
|
||||
await auth.async_get_access_token()
|
||||
except ClientResponseError as err:
|
||||
if err.status in (400, 401):
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
return VolvoCarsApi(
|
||||
api = VolvoCarsApi(
|
||||
web_session,
|
||||
auth,
|
||||
entry.data[CONF_API_KEY],
|
||||
entry.data[CONF_VIN],
|
||||
)
|
||||
|
||||
try:
|
||||
await api.async_get_access_token()
|
||||
except VolvoAuthException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except VolvoApiException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
return api
|
||||
|
||||
|
||||
async def _async_load_vehicle(api: VolvoCarsApi) -> VolvoCarsVehicle:
|
||||
try:
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
"""API for Volvo bound to Home Assistant OAuth."""
|
||||
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from volvocarsapi.auth import AccessTokenManager
|
||||
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.helpers.redact import async_redact_data
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_TO_REDACT = ["access_token", "id_token", "refresh_token"]
|
||||
|
||||
|
||||
class VolvoAuth(AccessTokenManager):
|
||||
@@ -18,7 +23,20 @@ class VolvoAuth(AccessTokenManager):
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return a valid access token."""
|
||||
current_access_token = self._oauth_session.token["access_token"]
|
||||
current_refresh_token = self._oauth_session.token["refresh_token"]
|
||||
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
_LOGGER.debug(
|
||||
"Token: %s", async_redact_data(self._oauth_session.token, _TO_REDACT)
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Token changed: access %s, refresh %s",
|
||||
current_access_token != self._oauth_session.token["access_token"],
|
||||
current_refresh_token != self._oauth_session.token["refresh_token"],
|
||||
)
|
||||
|
||||
return cast(str, self._oauth_session.token["access_token"])
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Define fixtures for Volvo unit tests."""
|
||||
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -9,6 +10,7 @@ from volvocarsapi.auth import TOKEN_URL
|
||||
from volvocarsapi.models import (
|
||||
VolvoCarsAvailableCommand,
|
||||
VolvoCarsLocation,
|
||||
VolvoCarsValueField,
|
||||
VolvoCarsValueStatusField,
|
||||
VolvoCarsVehicle,
|
||||
)
|
||||
@@ -17,10 +19,16 @@ from homeassistant.components.application_credentials import (
|
||||
ClientCredential,
|
||||
async_import_client_credential,
|
||||
)
|
||||
from homeassistant.components.volvo.api import VolvoAuth
|
||||
from homeassistant.components.volvo.const import CONF_VIN, DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.json import JsonObjectType
|
||||
|
||||
from . import async_load_fixture_as_json, async_load_fixture_as_value_field
|
||||
from .const import (
|
||||
@@ -37,6 +45,30 @@ from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockApiData:
|
||||
"""Container for mock API data."""
|
||||
|
||||
vehicle: VolvoCarsVehicle
|
||||
commands: list[VolvoCarsAvailableCommand]
|
||||
location: dict[str, VolvoCarsLocation]
|
||||
availability: dict[str, VolvoCarsValueField]
|
||||
brakes: dict[str, VolvoCarsValueField]
|
||||
diagnostics: dict[str, VolvoCarsValueField]
|
||||
doors: dict[str, VolvoCarsValueField]
|
||||
energy_capabilities: JsonObjectType
|
||||
energy_state: dict[str, VolvoCarsValueStatusField]
|
||||
engine_status: dict[str, VolvoCarsValueField]
|
||||
engine_warnings: dict[str, VolvoCarsValueField]
|
||||
fuel_status: dict[str, VolvoCarsValueField]
|
||||
odometer: dict[str, VolvoCarsValueField]
|
||||
recharge_status: dict[str, VolvoCarsValueField]
|
||||
statistics: dict[str, VolvoCarsValueField]
|
||||
tyres: dict[str, VolvoCarsValueField]
|
||||
warnings: dict[str, VolvoCarsValueField]
|
||||
windows: dict[str, VolvoCarsValueField]
|
||||
|
||||
|
||||
@pytest.fixture(params=[DEFAULT_MODEL])
|
||||
def full_model(request: pytest.FixtureRequest) -> str:
|
||||
"""Define which model to use when running the test. Use as a decorator."""
|
||||
@@ -65,81 +97,62 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
return config_entry
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def mock_api(hass: HomeAssistant, full_model: str) -> AsyncGenerator[AsyncMock]:
|
||||
@pytest.fixture
|
||||
async def mock_api(
|
||||
hass: HomeAssistant,
|
||||
full_model: str,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_credentials,
|
||||
) -> AsyncGenerator[VolvoCarsApi]:
|
||||
"""Mock the Volvo API."""
|
||||
|
||||
mock_api_data = await _async_load_mock_api_data(hass, full_model)
|
||||
|
||||
implementation = await async_get_config_entry_implementation(
|
||||
hass, mock_config_entry
|
||||
)
|
||||
oauth_session = OAuth2Session(hass, mock_config_entry, implementation)
|
||||
auth = VolvoAuth(aioclient_mock, oauth_session)
|
||||
api = VolvoCarsApi(
|
||||
aioclient_mock,
|
||||
auth,
|
||||
mock_config_entry.data[CONF_API_KEY],
|
||||
mock_config_entry.data[CONF_VIN],
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.volvo.VolvoCarsApi",
|
||||
autospec=True,
|
||||
) as mock_api:
|
||||
vehicle_data = await async_load_fixture_as_json(hass, "vehicle", full_model)
|
||||
vehicle = VolvoCarsVehicle.from_dict(vehicle_data)
|
||||
|
||||
commands_data = (
|
||||
await async_load_fixture_as_json(hass, "commands", full_model)
|
||||
).get("data")
|
||||
commands = [VolvoCarsAvailableCommand.from_dict(item) for item in commands_data]
|
||||
|
||||
location_data = await async_load_fixture_as_json(hass, "location", full_model)
|
||||
location = {"location": VolvoCarsLocation.from_dict(location_data)}
|
||||
|
||||
availability = await async_load_fixture_as_value_field(
|
||||
hass, "availability", full_model
|
||||
return_value=api,
|
||||
):
|
||||
api.async_get_brakes_status = AsyncMock(return_value=mock_api_data.brakes)
|
||||
api.async_get_command_accessibility = AsyncMock(
|
||||
return_value=mock_api_data.availability
|
||||
)
|
||||
brakes = await async_load_fixture_as_value_field(hass, "brakes", full_model)
|
||||
diagnostics = await async_load_fixture_as_value_field(
|
||||
hass, "diagnostics", full_model
|
||||
api.async_get_commands = AsyncMock(return_value=mock_api_data.commands)
|
||||
api.async_get_diagnostics = AsyncMock(return_value=mock_api_data.diagnostics)
|
||||
api.async_get_doors_status = AsyncMock(return_value=mock_api_data.doors)
|
||||
api.async_get_energy_capabilities = AsyncMock(
|
||||
return_value=mock_api_data.energy_capabilities
|
||||
)
|
||||
doors = await async_load_fixture_as_value_field(hass, "doors", full_model)
|
||||
energy_capabilities = await async_load_fixture_as_json(
|
||||
hass, "energy_capabilities", full_model
|
||||
api.async_get_energy_state = AsyncMock(return_value=mock_api_data.energy_state)
|
||||
api.async_get_engine_status = AsyncMock(
|
||||
return_value=mock_api_data.engine_status
|
||||
)
|
||||
energy_state_data = await async_load_fixture_as_json(
|
||||
hass, "energy_state", full_model
|
||||
api.async_get_engine_warnings = AsyncMock(
|
||||
return_value=mock_api_data.engine_warnings
|
||||
)
|
||||
energy_state = {
|
||||
key: VolvoCarsValueStatusField.from_dict(value)
|
||||
for key, value in energy_state_data.items()
|
||||
}
|
||||
engine_status = await async_load_fixture_as_value_field(
|
||||
hass, "engine_status", full_model
|
||||
api.async_get_fuel_status = AsyncMock(return_value=mock_api_data.fuel_status)
|
||||
api.async_get_location = AsyncMock(return_value=mock_api_data.location)
|
||||
api.async_get_odometer = AsyncMock(return_value=mock_api_data.odometer)
|
||||
api.async_get_recharge_status = AsyncMock(
|
||||
return_value=mock_api_data.recharge_status
|
||||
)
|
||||
engine_warnings = await async_load_fixture_as_value_field(
|
||||
hass, "engine_warnings", full_model
|
||||
)
|
||||
fuel_status = await async_load_fixture_as_value_field(
|
||||
hass, "fuel_status", full_model
|
||||
)
|
||||
odometer = await async_load_fixture_as_value_field(hass, "odometer", full_model)
|
||||
recharge_status = await async_load_fixture_as_value_field(
|
||||
hass, "recharge_status", full_model
|
||||
)
|
||||
statistics = await async_load_fixture_as_value_field(
|
||||
hass, "statistics", full_model
|
||||
)
|
||||
tyres = await async_load_fixture_as_value_field(hass, "tyres", full_model)
|
||||
warnings = await async_load_fixture_as_value_field(hass, "warnings", full_model)
|
||||
windows = await async_load_fixture_as_value_field(hass, "windows", full_model)
|
||||
|
||||
api: VolvoCarsApi = mock_api.return_value
|
||||
api.async_get_brakes_status = AsyncMock(return_value=brakes)
|
||||
api.async_get_command_accessibility = AsyncMock(return_value=availability)
|
||||
api.async_get_commands = AsyncMock(return_value=commands)
|
||||
api.async_get_diagnostics = AsyncMock(return_value=diagnostics)
|
||||
api.async_get_doors_status = AsyncMock(return_value=doors)
|
||||
api.async_get_energy_capabilities = AsyncMock(return_value=energy_capabilities)
|
||||
api.async_get_energy_state = AsyncMock(return_value=energy_state)
|
||||
api.async_get_engine_status = AsyncMock(return_value=engine_status)
|
||||
api.async_get_engine_warnings = AsyncMock(return_value=engine_warnings)
|
||||
api.async_get_fuel_status = AsyncMock(return_value=fuel_status)
|
||||
api.async_get_location = AsyncMock(return_value=location)
|
||||
api.async_get_odometer = AsyncMock(return_value=odometer)
|
||||
api.async_get_recharge_status = AsyncMock(return_value=recharge_status)
|
||||
api.async_get_statistics = AsyncMock(return_value=statistics)
|
||||
api.async_get_tyre_states = AsyncMock(return_value=tyres)
|
||||
api.async_get_vehicle_details = AsyncMock(return_value=vehicle)
|
||||
api.async_get_warnings = AsyncMock(return_value=warnings)
|
||||
api.async_get_window_states = AsyncMock(return_value=windows)
|
||||
api.async_get_statistics = AsyncMock(return_value=mock_api_data.statistics)
|
||||
api.async_get_tyre_states = AsyncMock(return_value=mock_api_data.tyres)
|
||||
api.async_get_vehicle_details = AsyncMock(return_value=mock_api_data.vehicle)
|
||||
api.async_get_warnings = AsyncMock(return_value=mock_api_data.warnings)
|
||||
api.async_get_window_states = AsyncMock(return_value=mock_api_data.windows)
|
||||
|
||||
yield api
|
||||
|
||||
@@ -183,3 +196,76 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"homeassistant.components.volvo.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
async def _async_load_mock_api_data(
|
||||
hass: HomeAssistant, full_model: str
|
||||
) -> MockApiData:
|
||||
"""Load all mock API data from fixtures."""
|
||||
vehicle_data = await async_load_fixture_as_json(hass, "vehicle", full_model)
|
||||
vehicle = VolvoCarsVehicle.from_dict(vehicle_data)
|
||||
|
||||
commands_data = (
|
||||
await async_load_fixture_as_json(hass, "commands", full_model)
|
||||
).get("data")
|
||||
commands = [VolvoCarsAvailableCommand.from_dict(item) for item in commands_data]
|
||||
|
||||
location_data = await async_load_fixture_as_json(hass, "location", full_model)
|
||||
location = {"location": VolvoCarsLocation.from_dict(location_data)}
|
||||
|
||||
availability = await async_load_fixture_as_value_field(
|
||||
hass, "availability", full_model
|
||||
)
|
||||
brakes = await async_load_fixture_as_value_field(hass, "brakes", full_model)
|
||||
diagnostics = await async_load_fixture_as_value_field(
|
||||
hass, "diagnostics", full_model
|
||||
)
|
||||
doors = await async_load_fixture_as_value_field(hass, "doors", full_model)
|
||||
energy_capabilities = await async_load_fixture_as_json(
|
||||
hass, "energy_capabilities", full_model
|
||||
)
|
||||
energy_state_data = await async_load_fixture_as_json(
|
||||
hass, "energy_state", full_model
|
||||
)
|
||||
energy_state = {
|
||||
key: VolvoCarsValueStatusField.from_dict(value)
|
||||
for key, value in energy_state_data.items()
|
||||
}
|
||||
engine_status = await async_load_fixture_as_value_field(
|
||||
hass, "engine_status", full_model
|
||||
)
|
||||
engine_warnings = await async_load_fixture_as_value_field(
|
||||
hass, "engine_warnings", full_model
|
||||
)
|
||||
fuel_status = await async_load_fixture_as_value_field(
|
||||
hass, "fuel_status", full_model
|
||||
)
|
||||
odometer = await async_load_fixture_as_value_field(hass, "odometer", full_model)
|
||||
recharge_status = await async_load_fixture_as_value_field(
|
||||
hass, "recharge_status", full_model
|
||||
)
|
||||
statistics = await async_load_fixture_as_value_field(hass, "statistics", full_model)
|
||||
tyres = await async_load_fixture_as_value_field(hass, "tyres", full_model)
|
||||
warnings = await async_load_fixture_as_value_field(hass, "warnings", full_model)
|
||||
windows = await async_load_fixture_as_value_field(hass, "windows", full_model)
|
||||
|
||||
return MockApiData(
|
||||
vehicle=vehicle,
|
||||
commands=commands,
|
||||
location=location,
|
||||
availability=availability,
|
||||
brakes=brakes,
|
||||
diagnostics=diagnostics,
|
||||
doors=doors,
|
||||
energy_capabilities=energy_capabilities,
|
||||
energy_state=energy_state,
|
||||
engine_status=engine_status,
|
||||
engine_warnings=engine_warnings,
|
||||
fuel_status=fuel_status,
|
||||
odometer=odometer,
|
||||
recharge_status=recharge_status,
|
||||
statistics=statistics,
|
||||
tyres=tyres,
|
||||
warnings=warnings,
|
||||
windows=windows,
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.helpers import entity_registry as er
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_api", "full_model")
|
||||
@pytest.mark.parametrize(
|
||||
"full_model",
|
||||
["ex30_2024", "s90_diesel_2018", "xc40_electric_2024", "xc90_petrol_2019"],
|
||||
|
||||
@@ -21,6 +21,7 @@ from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_api")
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
@@ -38,6 +39,7 @@ async def test_setup(
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_api")
|
||||
async def test_token_refresh_success(
|
||||
mock_config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
@@ -61,7 +63,6 @@ async def test_token_refresh_success(
|
||||
@pytest.mark.parametrize(
|
||||
("token_response"),
|
||||
[
|
||||
(HTTPStatus.FORBIDDEN),
|
||||
(HTTPStatus.INTERNAL_SERVER_ERROR),
|
||||
(HTTPStatus.NOT_FOUND),
|
||||
],
|
||||
@@ -80,15 +81,23 @@ async def test_token_refresh_fail(
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("token_response"),
|
||||
[
|
||||
(HTTPStatus.BAD_REQUEST),
|
||||
(HTTPStatus.FORBIDDEN),
|
||||
],
|
||||
)
|
||||
async def test_token_refresh_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
setup_integration: Callable[[], Awaitable[bool]],
|
||||
token_response: HTTPStatus,
|
||||
) -> None:
|
||||
"""Test where token refresh indicates unauthorized."""
|
||||
|
||||
aioclient_mock.post(TOKEN_URL, status=HTTPStatus.UNAUTHORIZED)
|
||||
aioclient_mock.post(TOKEN_URL, status=token_response)
|
||||
|
||||
assert not await setup_integration()
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.helpers import entity_registry as er
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_api", "full_model")
|
||||
@pytest.mark.parametrize(
|
||||
"full_model",
|
||||
[
|
||||
@@ -38,6 +39,7 @@ async def test_sensor(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_api", "full_model")
|
||||
@pytest.mark.parametrize(
|
||||
"full_model",
|
||||
["xc40_electric_2024"],
|
||||
@@ -54,6 +56,7 @@ async def test_distance_to_empty_battery(
|
||||
assert hass.states.get("sensor.volvo_xc40_distance_to_empty_battery").state == "250"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_api", "full_model")
|
||||
@pytest.mark.parametrize(
|
||||
("full_model", "short_model"),
|
||||
[("ex30_2024", "ex30"), ("xc60_phev_2020", "xc60")],
|
||||
@@ -71,6 +74,7 @@ async def test_skip_invalid_api_fields(
|
||||
assert not hass.states.get(f"sensor.volvo_{short_model}_charging_current_limit")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_api", "full_model")
|
||||
@pytest.mark.parametrize(
|
||||
"full_model",
|
||||
["ex30_2024"],
|
||||
|
||||
Reference in New Issue
Block a user