Add Re-Auth to bmw_connected_drive (#90251)

* Add Re-Auth to bmw_connected_drive

* Always store refresh token to entry

* Fix tests

* Typo

---------

Co-authored-by: rikroe <rikroe@users.noreply.github.com>
This commit is contained in:
rikroe 2023-03-25 18:09:33 +01:00 committed by GitHub
parent 7bceedfc95
commit cc337c4ff6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 89 additions and 11 deletions

View File

@ -1,6 +1,7 @@
"""Config flow for BMW ConnectedDrive integration.""" """Config flow for BMW ConnectedDrive integration."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from typing import Any from typing import Any
from bimmer_connected.api.authentication import MyBMWAuthentication from bimmer_connected.api.authentication import MyBMWAuthentication
@ -55,36 +56,61 @@ class BMWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
_reauth_entry: config_entries.ConfigEntry | None = None
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}" unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}"
if not self._reauth_entry:
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
info = None info = None
try: try:
info = await validate_input(self.hass, user_input) info = await validate_input(self.hass, user_input)
entry_data = {
**user_input,
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
}
except CannotConnect: except CannotConnect:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
if info: if info:
if self._reauth_entry:
self.hass.config_entries.async_update_entry(
self._reauth_entry, data=entry_data
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(
self._reauth_entry.entry_id
)
)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry( return self.async_create_entry(
title=info["title"], title=info["title"],
data={ data=entry_data,
**user_input,
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
},
) )
return self.async_show_form( schema = self.add_suggested_values_to_schema(
step_id="user", data_schema=DATA_SCHEMA, errors=errors DATA_SCHEMA, self._reauth_entry.data if self._reauth_entry else {}
) )
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle configuration by re-auth."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_user()
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow( def async_get_options_flow(

View File

@ -12,6 +12,7 @@ from httpx import HTTPError, HTTPStatusError, TimeoutException
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN from .const import CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN
@ -65,8 +66,9 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
401, 401,
403, 403,
): ):
# Clear refresh token only on issues with authorization # Clear refresh token only and trigger reauth
self._update_config_entry_refresh_token(None) self._update_config_entry_refresh_token(None)
raise ConfigEntryAuthFailed(str(err)) from err
raise UpdateFailed(f"Error communicating with BMW API: {err}") from err raise UpdateFailed(f"Error communicating with BMW API: {err}") from err
if self.account.refresh_token != old_refresh_token: if self.account.refresh_token != old_refresh_token:

View File

@ -14,7 +14,8 @@
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]" "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
}, },
"options": { "options": {

View File

@ -1,4 +1,5 @@
"""Test the for the BMW Connected Drive config flow.""" """Test the for the BMW Connected Drive config flow."""
from copy import deepcopy
from unittest.mock import patch from unittest.mock import patch
from bimmer_connected.api.authentication import MyBMWAuthentication from bimmer_connected.api.authentication import MyBMWAuthentication
@ -10,7 +11,7 @@ from homeassistant.components.bmw_connected_drive.const import (
CONF_READ_ONLY, CONF_READ_ONLY,
CONF_REFRESH_TOKEN, CONF_REFRESH_TOKEN,
) )
from homeassistant.const import CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import FIXTURE_CONFIG_ENTRY, FIXTURE_REFRESH_TOKEN, FIXTURE_USER_INPUT from . import FIXTURE_CONFIG_ENTRY, FIXTURE_REFRESH_TOKEN, FIXTURE_USER_INPUT
@ -110,3 +111,51 @@ async def test_options_flow_implementation(hass: HomeAssistant) -> None:
} }
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
async def test_reauth(hass: HomeAssistant) -> None:
"""Test the reauth form."""
with patch(
"bimmer_connected.api.authentication.MyBMWAuthentication.login",
side_effect=login_sideeffect,
autospec=True,
), patch(
"homeassistant.components.bmw_connected_drive.async_setup_entry",
return_value=True,
) as mock_setup_entry:
wrong_password = "wrong"
config_entry_with_wrong_password = deepcopy(FIXTURE_CONFIG_ENTRY)
config_entry_with_wrong_password["data"][CONF_PASSWORD] = wrong_password
config_entry = MockConfigEntry(**config_entry_with_wrong_password)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.data == config_entry_with_wrong_password["data"]
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"source": config_entries.SOURCE_REAUTH,
"unique_id": config_entry.unique_id,
"entry_id": config_entry.entry_id,
},
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], FIXTURE_USER_INPUT
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.FlowResultType.ABORT
assert result2["reason"] == "reauth_successful"
assert config_entry.data == FIXTURE_COMPLETE_ENTRY
assert len(mock_setup_entry.mock_calls) == 1