From 194d59df03d95019b39e849ed255a7d47e638286 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 22 Jan 2025 14:23:00 +0100 Subject: [PATCH] Add reauth flow to Overseerr (#136247) --- .../components/overseerr/config_flow.py | 68 ++++++++++++++---- .../components/overseerr/coordinator.py | 4 +- .../components/overseerr/quality_scale.yaml | 2 +- .../components/overseerr/strings.json | 11 ++- .../components/overseerr/test_config_flow.py | 71 +++++++++++++++++++ 5 files changed, 140 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/overseerr/config_flow.py b/homeassistant/components/overseerr/config_flow.py index e2994212bfe..6d765c6449e 100644 --- a/homeassistant/components/overseerr/config_flow.py +++ b/homeassistant/components/overseerr/config_flow.py @@ -1,5 +1,6 @@ """Config flow for Overseerr.""" +from collections.abc import Mapping from typing import Any from python_overseerr import ( @@ -28,6 +29,25 @@ from .const import DOMAIN class OverseerrConfigFlow(ConfigFlow, domain=DOMAIN): """Overseerr config flow.""" + async def _check_connection( + self, host: str, port: int, ssl: bool, api_key: str + ) -> str | None: + """Check if we can connect to the Overseerr instance.""" + client = OverseerrClient( + host, + port, + api_key, + ssl=ssl, + session=async_get_clientsession(self.hass), + ) + try: + await client.get_request_count() + except OverseerrAuthenticationError: + return "invalid_auth" + except OverseerrError: + return "cannot_connect" + return None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -41,19 +61,11 @@ class OverseerrConfigFlow(ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: host}) port = url.port assert port - client = OverseerrClient( - host, - port, - user_input[CONF_API_KEY], - ssl=url.scheme == "https", - session=async_get_clientsession(self.hass), + error = await self._check_connection( + host, port, url.scheme == "https", user_input[CONF_API_KEY] ) - try: - await client.get_request_count() - except OverseerrAuthenticationError: - errors["base"] = "invalid_auth" - except OverseerrError: - errors["base"] = "cannot_connect" + if error: + errors["base"] = error else: return self.async_create_entry( title="Overseerr", @@ -72,3 +84,35 @@ class OverseerrConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle re-auth.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle re-auth confirmation.""" + errors: dict[str, str] = {} + if user_input: + entry = self._get_reauth_entry() + error = await self._check_connection( + entry.data[CONF_HOST], + entry.data[CONF_PORT], + entry.data[CONF_SSL], + user_input[CONF_API_KEY], + ) + if error: + errors["base"] = error + else: + return self.async_update_reload_and_abort( + entry, + data={**entry.data, CONF_API_KEY: user_input[CONF_API_KEY]}, + ) + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) diff --git a/homeassistant/components/overseerr/coordinator.py b/homeassistant/components/overseerr/coordinator.py index 75a7d8d73d7..56002ddf558 100644 --- a/homeassistant/components/overseerr/coordinator.py +++ b/homeassistant/components/overseerr/coordinator.py @@ -13,7 +13,7 @@ from yarl import URL from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -53,7 +53,7 @@ class OverseerrCoordinator(DataUpdateCoordinator[RequestCount]): try: return await self.client.get_request_count() except OverseerrAuthenticationError as err: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="auth_error", ) from err diff --git a/homeassistant/components/overseerr/quality_scale.yaml b/homeassistant/components/overseerr/quality_scale.yaml index dfb794476aa..ffd03ed4a09 100644 --- a/homeassistant/components/overseerr/quality_scale.yaml +++ b/homeassistant/components/overseerr/quality_scale.yaml @@ -37,7 +37,7 @@ rules: status: done comment: Handled by the coordinator parallel-updates: done - reauthentication-flow: todo + reauthentication-flow: done test-coverage: todo # Gold devices: done diff --git a/homeassistant/components/overseerr/strings.json b/homeassistant/components/overseerr/strings.json index 25b53303611..8aa0ff7fe10 100644 --- a/homeassistant/components/overseerr/strings.json +++ b/homeassistant/components/overseerr/strings.json @@ -10,10 +10,19 @@ "url": "The URL of the Overseerr instance.", "api_key": "The API key of the Overseerr instance." } + }, + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "[%key:component::overseerr::config::step::user::data_description::api_key%]" + } } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/tests/components/overseerr/test_config_flow.py b/tests/components/overseerr/test_config_flow.py index 937d697b8cb..3227ffc6862 100644 --- a/tests/components/overseerr/test_config_flow.py +++ b/tests/components/overseerr/test_config_flow.py @@ -155,3 +155,74 @@ async def test_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_reauth_flow( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauth flow.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "new-test-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert mock_config_entry.data[CONF_API_KEY] == "new-test-key" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (OverseerrAuthenticationError, "invalid_auth"), + (OverseerrConnectionError, "cannot_connect"), + ], +) +async def test_reauth_flow_errors( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + exception: Exception, + error: str, +) -> None: + """Test reauth flow.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_overseerr_client.get_request_count.side_effect = exception + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "new-test-key"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_overseerr_client.get_request_count.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "new-test-key"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert mock_config_entry.data[CONF_API_KEY] == "new-test-key"