Add re-authentication for transmission (#73124)

* Add reauth flow to transmission

* fix async_setup

* add strings

* fix test coverage
This commit is contained in:
Rami Mosleh 2022-06-20 17:09:58 +03:00 committed by GitHub
parent 3824703a64
commit 81e3ed790d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 168 additions and 12 deletions

View File

@ -20,7 +20,7 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, ServiceCall 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 import config_validation as cv
from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.dispatcher import dispatcher_send
from homeassistant.helpers.event import async_track_time_interval 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) client = TransmissionClient(hass, config_entry)
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client
if not await client.async_setup(): await client.async_setup()
return False
return True return True
@ -152,15 +151,15 @@ class TransmissionClient:
"""Return the TransmissionData object.""" """Return the TransmissionData object."""
return self._tm_data return self._tm_data
async def async_setup(self): async def async_setup(self) -> None:
"""Set up the Transmission client.""" """Set up the Transmission client."""
try: try:
self.tm_api = await get_api(self.hass, self.config_entry.data) self.tm_api = await get_api(self.hass, self.config_entry.data)
except CannotConnect as error: except CannotConnect as error:
raise ConfigEntryNotReady from error raise ConfigEntryNotReady from error
except (AuthenticationError, UnknownError): except (AuthenticationError, UnknownError) as error:
return False raise ConfigEntryAuthFailed from error
self._tm_data = TransmissionData(self.hass, self.config_entry, self.tm_api) 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) self.config_entry.add_update_listener(self.async_options_updated)
return True
def add_options(self): def add_options(self):
"""Add options for entry.""" """Add options for entry."""
if not self.config_entry.options: if not self.config_entry.options:

View File

@ -1,6 +1,9 @@
"""Config flow for Transmission Bittorent Client.""" """Config flow for Transmission Bittorent Client."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -13,6 +16,7 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from . import get_api from . import get_api
from .const import ( from .const import (
@ -43,6 +47,7 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle Tansmission config flow.""" """Handle Tansmission config flow."""
VERSION = 1 VERSION = 1
_reauth_entry: config_entries.ConfigEntry | None
@staticmethod @staticmethod
@callback @callback
@ -87,6 +92,48 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, 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): class TransmissionOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Transmission client options.""" """Handle Transmission client options."""

View File

@ -10,6 +10,13 @@
"password": "[%key:common::config_flow::data::password%]", "password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]" "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": { "error": {
@ -18,7 +25,8 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}, },
"abort": { "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": { "options": {

View File

@ -1,7 +1,8 @@
{ {
"config": { "config": {
"abort": { "abort": {
"already_configured": "Device is already configured" "already_configured": "Device is already configured",
"reauth_successful": "Re-authentication was successful"
}, },
"error": { "error": {
"cannot_connect": "Failed to connect", "cannot_connect": "Failed to connect",
@ -9,6 +10,13 @@
"name_exists": "Name already exists" "name_exists": "Name already exists"
}, },
"step": { "step": {
"reauth_confirm": {
"data": {
"password": "Password"
},
"description": "The password for {username} is invalid.",
"title": "Reauthenticate Integration"
},
"user": { "user": {
"data": { "data": {
"host": "Host", "host": "Host",

View File

@ -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["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["errors"] == {"base": "cannot_connect"} 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"}

View File

@ -6,7 +6,7 @@ import pytest
from transmissionrpc.error import TransmissionError from transmissionrpc.error import TransmissionError
from homeassistant.components import transmission from homeassistant.components import transmission
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, mock_coro from tests.common import MockConfigEntry, mock_coro
@ -105,7 +105,7 @@ async def test_setup_failed(hass):
with patch( with patch(
"transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized")
): ), pytest.raises(ConfigEntryAuthFailed):
assert await transmission.async_setup_entry(hass, entry) is False assert await transmission.async_setup_entry(hass, entry) is False