Add reconfiguration to onedrive_for_business (#163054)

This commit is contained in:
Josef Zweck
2026-02-15 13:32:05 +01:00
committed by GitHub
parent 40890419bb
commit 11fe11cc03
5 changed files with 166 additions and 16 deletions
@@ -11,7 +11,11 @@ from onedrive_personal_sdk.exceptions import OneDriveException
from onedrive_personal_sdk.models.items import AppRoot
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlowResult,
)
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
@@ -116,10 +120,16 @@ class OneDriveForBusinessConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
data_updates=data,
)
self._abort_if_unique_id_configured()
if self.source == SOURCE_RECONFIGURE:
self._abort_if_unique_id_mismatch(reason="wrong_drive")
else:
self._abort_if_unique_id_configured()
self._data.update(data)
if self.source == SOURCE_RECONFIGURE:
return await self.async_step_reconfigure_folder()
return await self.async_step_select_folder()
async def async_step_select_folder(
@@ -157,6 +167,47 @@ class OneDriveForBusinessConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Reconfigure the entry."""
self._data[CONF_TENANT_ID] = self._get_reconfigure_entry().data[CONF_TENANT_ID]
with tenant_id_context(self._data[CONF_TENANT_ID]):
return await self.async_step_pick_implementation()
async def async_step_reconfigure_folder(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step to ask for new folder path during reconfiguration."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
path = str(user_input[CONF_FOLDER_PATH]).lstrip("/")
try:
folder = await self.client.create_folder("root", path)
except OneDriveException:
self.logger.debug("Failed to create folder", exc_info=True)
errors["base"] = "folder_creation_error"
if not errors:
return self.async_update_reload_and_abort(
reconfigure_entry,
data={
**self._data,
CONF_FOLDER_ID: folder.id,
CONF_FOLDER_PATH: user_input[CONF_FOLDER_PATH],
},
)
return self.async_show_form(
step_id="reconfigure_folder",
data_schema=self.add_suggested_values_to_schema(
FOLDER_NAME_SCHEMA,
{CONF_FOLDER_PATH: reconfigure_entry.data[CONF_FOLDER_PATH]},
),
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
@@ -116,7 +116,7 @@ rules:
status: exempt
comment: |
This integration does not create entities.
reconfiguration-flow: todo
reconfiguration-flow: done
repair-issues:
status: exempt
comment: |
@@ -12,6 +12,7 @@
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"wrong_drive": "[%key:component::onedrive::config::abort::wrong_drive%]"
@@ -46,6 +47,16 @@
"description": "The OneDrive for Business integration needs to re-authenticate your account",
"title": "[%key:common::config_flow::title::reauth%]"
},
"reconfigure_folder": {
"data": {
"folder_path": "[%key:component::onedrive_for_business::config::step::select_folder::data::folder_path%]"
},
"data_description": {
"folder_path": "[%key:component::onedrive_for_business::config::step::select_folder::data_description::folder_path%]"
},
"description": "[%key:component::onedrive_for_business::config::step::select_folder::description%]",
"title": "[%key:component::onedrive_for_business::config::step::select_folder::title%]"
},
"select_folder": {
"data": {
"folder_path": "Folder path"
@@ -33,7 +33,7 @@ from homeassistant.components.onedrive_for_business.const import (
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .const import BACKUP_METADATA, CLIENT_ID, CLIENT_SECRET, IDENTITY_SET
from .const import BACKUP_METADATA, CLIENT_ID, CLIENT_SECRET, IDENTITY_SET, TENANT_ID
from tests.common import MockConfigEntry
@@ -77,7 +77,7 @@ def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
},
CONF_FOLDER_PATH: "backups/home_assistant",
CONF_FOLDER_ID: "my_folder_id",
CONF_TENANT_ID: "mock-tenant-id",
CONF_TENANT_ID: TENANT_ID,
},
unique_id="mock_drive_id",
)
@@ -35,7 +35,6 @@ async def _do_get_token(
result: ConfigFlowResult,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
tenant_id: str = TENANT_ID,
) -> None:
state = config_entry_oauth2_flow._encode_jwt(
hass,
@@ -46,8 +45,8 @@ async def _do_get_token(
)
scope = "Files.ReadWrite.All+offline_access+openid"
authorize_url = OAUTH2_AUTHORIZE.format(tenant_id=tenant_id)
token_url = OAUTH2_TOKEN.format(tenant_id=tenant_id)
authorize_url = OAUTH2_AUTHORIZE.format(tenant_id=TENANT_ID)
token_url = OAUTH2_TOKEN.format(tenant_id=TENANT_ID)
assert result["url"] == (
f"{authorize_url}?response_type=code&client_id={CLIENT_ID}"
@@ -266,6 +265,99 @@ async def test_already_configured(
assert result["reason"] == "already_configured"
@pytest.mark.usefixtures("current_request_with_host")
async def test_reconfigure_flow(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_onedrive_client: MagicMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Test reconfigure flow."""
await setup_integration(hass, mock_config_entry)
result = await mock_config_entry.start_reconfigure_flow(hass)
await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure_folder"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_PATH: "new/folder/path"}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert mock_config_entry.data[CONF_FOLDER_PATH] == "new/folder/path"
assert mock_config_entry.data[CONF_FOLDER_ID] == "my_folder_id"
assert mock_config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token"
assert mock_config_entry.data[CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
@pytest.mark.usefixtures("current_request_with_host")
async def test_reconfigure_flow_error(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_onedrive_client: MagicMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Test reconfigure flow errors."""
mock_config_entry.add_to_hass(hass)
await hass.async_block_till_done()
result = await mock_config_entry.start_reconfigure_flow(hass)
await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure_folder"
mock_onedrive_client.create_folder.side_effect = OneDriveException()
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_PATH: "new/folder/path"}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure_folder"
assert result["errors"] == {"base": "folder_creation_error"}
# clear side effect and try again
mock_onedrive_client.create_folder.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_PATH: "new/folder/path"}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert mock_config_entry.data[CONF_FOLDER_PATH] == "new/folder/path"
assert mock_config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token"
assert mock_config_entry.data[CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
@pytest.mark.usefixtures("current_request_with_host")
async def test_reconfigure_flow_wrong_drive(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_drive: Drive,
) -> None:
"""Test that the reconfigure flow fails on a different drive id."""
mock_drive.id = "other_drive_id"
mock_config_entry.add_to_hass(hass)
await hass.async_block_till_done()
result = await mock_config_entry.start_reconfigure_flow(hass)
await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock)
@pytest.mark.usefixtures("current_request_with_host")
async def test_reauth_flow(
hass: HomeAssistant,
@@ -284,9 +376,7 @@ async def test_reauth_flow(
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
await _do_get_token(
hass, result, hass_client_no_auth, aioclient_mock, "mock-tenant-id"
)
await _do_get_token(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
@@ -295,7 +385,7 @@ async def test_reauth_flow(
assert mock_config_entry.data[CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
assert mock_config_entry.data[CONF_FOLDER_PATH] == "backups/home_assistant"
assert mock_config_entry.data[CONF_FOLDER_ID] == "my_folder_id"
assert mock_config_entry.data[CONF_TENANT_ID] == "mock-tenant-id"
assert mock_config_entry.data[CONF_TENANT_ID] == TENANT_ID
@pytest.mark.usefixtures("current_request_with_host")
@@ -319,9 +409,7 @@ async def test_reauth_flow_id_changed(
assert result["step_id"] == "reauth_confirm"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
await _do_get_token(
hass, result, hass_client_no_auth, aioclient_mock, "mock-tenant-id"
)
await _do_get_token(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
@@ -330,4 +418,4 @@ async def test_reauth_flow_id_changed(
assert mock_config_entry.data[CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
assert mock_config_entry.data[CONF_FOLDER_PATH] == "backups/home_assistant"
assert mock_config_entry.data[CONF_FOLDER_ID] == "my_folder_id"
assert mock_config_entry.data[CONF_TENANT_ID] == "mock-tenant-id"
assert mock_config_entry.data[CONF_TENANT_ID] == TENANT_ID