diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index ac6659b048e..ae5d2dacf61 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -85,8 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b client = TransmissionClient(hass, config_entry) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client - if not await client.async_setup(): - return False + await client.async_setup() return True @@ -152,15 +151,15 @@ class TransmissionClient: """Return the TransmissionData object.""" return self._tm_data - async def async_setup(self): + async def async_setup(self) -> None: """Set up the Transmission client.""" try: self.tm_api = await get_api(self.hass, self.config_entry.data) except CannotConnect as error: raise ConfigEntryNotReady from error - except (AuthenticationError, UnknownError): - return False + except (AuthenticationError, UnknownError) as error: + raise ConfigEntryAuthFailed from error self._tm_data = TransmissionData(self.hass, self.config_entry, self.tm_api) @@ -262,8 +261,6 @@ class TransmissionClient: self.config_entry.add_update_listener(self.async_options_updated) - return True - def add_options(self): """Add options for entry.""" if not self.config_entry.options: diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index b57279ebbbb..c0bc51b0683 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -1,6 +1,9 @@ """Config flow for Transmission Bittorent Client.""" from __future__ import annotations +from collections.abc import Mapping +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -13,6 +16,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from . import get_api from .const import ( @@ -43,6 +47,7 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle Tansmission config flow.""" VERSION = 1 + _reauth_entry: config_entries.ConfigEntry | None @staticmethod @callback @@ -87,6 +92,48 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_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, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + errors = {} + assert self._reauth_entry + if user_input is not None: + user_input = {**self._reauth_entry.data, **user_input} + try: + await get_api(self.hass, user_input) + + except AuthenticationError: + errors[CONF_PASSWORD] = "invalid_auth" + except (CannotConnect, UnknownError): + errors["base"] = "cannot_connect" + else: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME] + }, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): """Handle Transmission client options.""" diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 81725ad7d16..0194917c416 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -10,6 +10,13 @@ "password": "[%key:common::config_flow::data::password%]", "port": "[%key:common::config_flow::data::port%]" } + }, + "reauth_confirm": { + "description": "The password for {username} is invalid.", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -18,7 +25,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/transmission/translations/en.json b/homeassistant/components/transmission/translations/en.json index e92e307d3bc..3726f6f0a7e 100644 --- a/homeassistant/components/transmission/translations/en.json +++ b/homeassistant/components/transmission/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "name_exists": "Name already exists" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is invalid.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "host": "Host", diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 4e3a7c73d6c..7588736e997 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -257,3 +257,99 @@ async def test_error_on_unknown_error(hass, unknown_error): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_success(hass, api): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_ENTRY, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_ENTRY, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {CONF_USERNAME: USERNAME} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_reauth_failed(hass, auth_error): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_ENTRY, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_ENTRY, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == { + CONF_PASSWORD: "invalid_auth", + } + + +async def test_reauth_failed_conn_error(hass, conn_error): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_ENTRY, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_ENTRY, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index 78fddc5be86..c3dc924c54e 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -6,7 +6,7 @@ import pytest from transmissionrpc.error import TransmissionError from homeassistant.components import transmission -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_coro @@ -105,7 +105,7 @@ async def test_setup_failed(hass): with patch( "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") - ): + ), pytest.raises(ConfigEntryAuthFailed): assert await transmission.async_setup_entry(hass, entry) is False