diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 60489b3e30d..65829f713ef 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -1,7 +1,12 @@ """The Nextcloud integration.""" import logging -from nextcloudmonitor import NextcloudMonitor, NextcloudMonitorError +from nextcloudmonitor import ( + NextcloudMonitor, + NextcloudMonitorAuthorizationError, + NextcloudMonitorConnectionError, + NextcloudMonitorRequestError, +) import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -14,6 +19,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType @@ -82,9 +88,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: ncm = await hass.async_add_executor_job(_connect_nc) - except NextcloudMonitorError: - _LOGGER.error("Nextcloud setup failed - Check configuration") - return False + except NextcloudMonitorAuthorizationError as ex: + raise ConfigEntryAuthFailed from ex + except (NextcloudMonitorConnectionError, NextcloudMonitorRequestError) as ex: + raise ConfigEntryNotReady from ex coordinator = NextcloudDataUpdateCoordinator( hass, diff --git a/homeassistant/components/nextcloud/config_flow.py b/homeassistant/components/nextcloud/config_flow.py index f22d0a01a55..c5019603c09 100644 --- a/homeassistant/components/nextcloud/config_flow.py +++ b/homeassistant/components/nextcloud/config_flow.py @@ -1,13 +1,20 @@ """Config flow to configure the Nextcloud integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any -from nextcloudmonitor import NextcloudMonitor, NextcloudMonitorError +from nextcloudmonitor import ( + NextcloudMonitor, + NextcloudMonitorAuthorizationError, + NextcloudMonitorConnectionError, + NextcloudMonitorError, + NextcloudMonitorRequestError, +) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.data_entry_flow import FlowResult @@ -21,6 +28,13 @@ DATA_SCHEMA_USER = vol.Schema( vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, } ) +DATA_SCHEMA_REAUTH = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + _LOGGER = logging.getLogger(__name__) @@ -29,6 +43,8 @@ class NextcloudConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + _entry: ConfigEntry | None = None + def _try_connect_nc(self, user_input: dict) -> NextcloudMonitor: """Try to connect to nextcloud server.""" return NextcloudMonitor( @@ -67,7 +83,9 @@ class NextcloudConfigFlow(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_URL: user_input.get(CONF_URL)}) try: await self.hass.async_add_executor_job(self._try_connect_nc, user_input) - except NextcloudMonitorError: + except NextcloudMonitorAuthorizationError: + errors["base"] = "invalid_auth" + except (NextcloudMonitorConnectionError, NextcloudMonitorRequestError): errors["base"] = "connection_error" else: return self.async_create_entry( @@ -79,3 +97,43 @@ class NextcloudConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle flow upon an API authentication error.""" + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauthorization flow.""" + errors = {} + assert self._entry is not None + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + self._try_connect_nc, {**self._entry.data, **user_input} + ) + except NextcloudMonitorAuthorizationError: + errors["base"] = "invalid_auth" + except (NextcloudMonitorConnectionError, NextcloudMonitorRequestError): + errors["base"] = "connection_error" + else: + self.hass.config_entries.async_update_entry( + self._entry, + data={**self._entry.data, **user_input}, + ) + await self.hass.config_entries.async_reload(self._entry.entry_id) + return self.async_abort(reason="reauth_successful") + + data_schema = self.add_suggested_values_to_schema( + DATA_SCHEMA_REAUTH, + {CONF_USERNAME: self._entry.data[CONF_USERNAME], **(user_input or {})}, + ) + return self.async_show_form( + step_id="reauth_confirm", + data_schema=data_schema, + description_placeholders={"url": self._entry.data[CONF_URL]}, + errors=errors, + ) diff --git a/homeassistant/components/nextcloud/strings.json b/homeassistant/components/nextcloud/strings.json index dc0175ea8e8..782865032af 100644 --- a/homeassistant/components/nextcloud/strings.json +++ b/homeassistant/components/nextcloud/strings.json @@ -10,14 +10,23 @@ "password": "[%key:common::config_flow::data::password%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } + }, + "reauth_confirm": { + "description": "Update your login information for {url}.", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "connection_error_during_import": "Connection error occured during yaml configuration import" + "connection_error_during_import": "Connection error occured during yaml configuration import", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "connection_error": "[%key:common::config_flow::error::cannot_connect%]" + "connection_error": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" } }, "issues": { diff --git a/tests/components/nextcloud/snapshots/test_config_flow.ambr b/tests/components/nextcloud/snapshots/test_config_flow.ambr index caa95285074..3334478ba24 100644 --- a/tests/components/nextcloud/snapshots/test_config_flow.ambr +++ b/tests/components/nextcloud/snapshots/test_config_flow.ambr @@ -7,6 +7,14 @@ 'verify_ssl': True, }) # --- +# name: test_reauth + dict({ + 'password': 'other_password', + 'url': 'nc_url', + 'username': 'other_user', + 'verify_ssl': True, + }) +# --- # name: test_user_create_entry dict({ 'password': 'nc_pass', diff --git a/tests/components/nextcloud/test_config_flow.py b/tests/components/nextcloud/test_config_flow.py index 582ad3e77a3..ba465c5f8a7 100644 --- a/tests/components/nextcloud/test_config_flow.py +++ b/tests/components/nextcloud/test_config_flow.py @@ -1,12 +1,17 @@ """Tests for the Nextcloud config flow.""" from unittest.mock import Mock, patch -from nextcloudmonitor import NextcloudMonitorError +from nextcloudmonitor import ( + NextcloudMonitorAuthorizationError, + NextcloudMonitorConnectionError, + NextcloudMonitorError, + NextcloudMonitorRequestError, +) import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.nextcloud import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -27,6 +32,7 @@ async def test_user_create_entry( hass: HomeAssistant, mock_nextcloud_monitor: Mock, snapshot: SnapshotAssertion ) -> None: """Test that the user step works.""" + # start user flow result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -34,9 +40,24 @@ async def test_user_create_entry( assert result["step_id"] == "user" assert result["errors"] == {} + # test NextcloudMonitorAuthorizationError with patch( "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", - side_effect=NextcloudMonitorError, + side_effect=NextcloudMonitorAuthorizationError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + # test NextcloudMonitorConnectionError + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + side_effect=NextcloudMonitorConnectionError, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -47,6 +68,21 @@ async def test_user_create_entry( assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} + # test NextcloudMonitorRequestError + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + side_effect=NextcloudMonitorRequestError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "connection_error"} + + # test success with patch( "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", return_value=mock_nextcloud_monitor, @@ -154,3 +190,94 @@ async def test_import_connection_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result["type"] == FlowResultType.ABORT assert result["reason"] == "connection_error_during_import" + + +async def test_reauth( + hass: HomeAssistant, mock_nextcloud_monitor: Mock, snapshot: SnapshotAssertion +) -> None: + """Test that the re-auth flow works.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="nc_url", + unique_id="nc_url", + data=VALID_CONFIG, + ) + entry.add_to_hass(hass) + + # start reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + # test NextcloudMonitorAuthorizationError + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + side_effect=NextcloudMonitorAuthorizationError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "other_user", + CONF_PASSWORD: "other_password", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} + + # test NextcloudMonitorConnectionError + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + side_effect=NextcloudMonitorConnectionError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "other_user", + CONF_PASSWORD: "other_password", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "connection_error"} + + # test NextcloudMonitorRequestError + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + side_effect=NextcloudMonitorRequestError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "other_user", + CONF_PASSWORD: "other_password", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "connection_error"} + + # test success + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + return_value=mock_nextcloud_monitor, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "other_user", + CONF_PASSWORD: "other_password", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data == snapshot