diff --git a/homeassistant/components/onedrive_for_business/config_flow.py b/homeassistant/components/onedrive_for_business/config_flow.py index 92457ad834e7..ae1d9f6b681d 100644 --- a/homeassistant/components/onedrive_for_business/config_flow.py +++ b/homeassistant/components/onedrive_for_business/config_flow.py @@ -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: diff --git a/homeassistant/components/onedrive_for_business/quality_scale.yaml b/homeassistant/components/onedrive_for_business/quality_scale.yaml index 4632db092edb..91917eb4af00 100644 --- a/homeassistant/components/onedrive_for_business/quality_scale.yaml +++ b/homeassistant/components/onedrive_for_business/quality_scale.yaml @@ -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: | diff --git a/homeassistant/components/onedrive_for_business/strings.json b/homeassistant/components/onedrive_for_business/strings.json index c7c45e56ff9e..10d5936016e9 100644 --- a/homeassistant/components/onedrive_for_business/strings.json +++ b/homeassistant/components/onedrive_for_business/strings.json @@ -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" diff --git a/tests/components/onedrive_for_business/conftest.py b/tests/components/onedrive_for_business/conftest.py index f9c2325e679a..9ad609c8cc41 100644 --- a/tests/components/onedrive_for_business/conftest.py +++ b/tests/components/onedrive_for_business/conftest.py @@ -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", ) diff --git a/tests/components/onedrive_for_business/test_config_flow.py b/tests/components/onedrive_for_business/test_config_flow.py index 5e06af5a8d34..ce42892a6793 100644 --- a/tests/components/onedrive_for_business/test_config_flow.py +++ b/tests/components/onedrive_for_business/test_config_flow.py @@ -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