Support reauth for octoprint (#77213)

* Add reauth flow to octoprint

* Add unit tests around octoprint reauth

* Add missing strings

* Fix unit test mocks
This commit is contained in:
Ryan Fleming 2022-10-02 02:08:45 -04:00 committed by GitHub
parent 28809fc7fd
commit 2ea9732419
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 124 additions and 2 deletions

View File

@ -6,6 +6,7 @@ import logging
from typing import cast from typing import cast
from pyoctoprintapi import ApiError, OctoprintClient, PrinterOffline from pyoctoprintapi import ApiError, OctoprintClient, PrinterOffline
from pyoctoprintapi.exceptions import UnauthorizedException
import voluptuous as vol import voluptuous as vol
from yarl import URL from yarl import URL
@ -24,6 +25,7 @@ from homeassistant.const import (
Platform, Platform,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
@ -226,6 +228,8 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator):
printer = None printer = None
try: try:
job = await self._octoprint.get_job_info() job = await self._octoprint.get_job_info()
except UnauthorizedException as err:
raise ConfigEntryAuthFailed from err
except ApiError as err: except ApiError as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err
@ -238,6 +242,8 @@ class OctoprintDataUpdateCoordinator(DataUpdateCoordinator):
if not self._printer_offline: if not self._printer_offline:
_LOGGER.debug("Unable to retrieve printer information: Printer offline") _LOGGER.debug("Unable to retrieve printer information: Printer offline")
self._printer_offline = True self._printer_offline = True
except UnauthorizedException as err:
raise ConfigEntryAuthFailed from err
except ApiError as err: except ApiError as err:
raise UpdateFailed(err) from err raise UpdateFailed(err) from err
else: else:

View File

@ -1,5 +1,9 @@
"""Config flow for OctoPrint integration.""" """Config flow for OctoPrint integration."""
from __future__ import annotations
from collections.abc import Mapping
import logging import logging
from typing import Any
from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException from pyoctoprintapi import ApiError, OctoprintClient, OctoprintException
import voluptuous as vol import voluptuous as vol
@ -16,6 +20,7 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
) )
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
@ -46,6 +51,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
api_key_task = None api_key_task = None
_reauth_data = None
def __init__(self) -> None: def __init__(self) -> None:
"""Handle a config flow for OctoPrint.""" """Handle a config flow for OctoPrint."""
@ -114,8 +120,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._user_input = user_input self._user_input = user_input
return self.async_show_progress_done(next_step_id="user") return self.async_show_progress_done(next_step_id="user")
async def _finish_config(self, user_input): async def _finish_config(self, user_input: dict):
"""Finish the configuration setup.""" """Finish the configuration setup."""
existing_entry = await self.async_set_unique_id(self.unique_id)
if existing_entry is not None:
self.hass.config_entries.async_update_entry(existing_entry, data=user_input)
# Reload the config entry otherwise devices will remain unavailable
self.hass.async_create_task(
self.hass.config_entries.async_reload(existing_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
octoprint = self._get_octoprint_client(user_input) octoprint = self._get_octoprint_client(user_input)
octoprint.set_api_key(user_input[CONF_API_KEY]) octoprint.set_api_key(user_input[CONF_API_KEY])
@ -127,6 +143,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(discovery.upnp_uuid, raise_on_progress=False) await self.async_set_unique_id(discovery.upnp_uuid, raise_on_progress=False)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)
async def async_step_auth_failed(self, user_input): async def async_step_auth_failed(self, user_input):
@ -188,6 +205,41 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_user() return await self.async_step_user()
async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult:
"""Handle reauthorization request from Octoprint."""
self._reauth_data = dict(config)
self.context.update(
{
"title_placeholders": {CONF_HOST: config[CONF_HOST]},
}
)
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."""
assert self._reauth_data is not None
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(
CONF_USERNAME, default=self._reauth_data[CONF_USERNAME]
): str,
}
),
)
self.api_key_task = None
self._reauth_data[CONF_USERNAME] = user_input[CONF_USERNAME]
return await self.async_step_get_api_key(self._reauth_data)
async def _async_get_auth_key(self, user_input: dict): async def _async_get_auth_key(self, user_input: dict):
"""Get application api key.""" """Get application api key."""
octoprint = self._get_octoprint_client(user_input) octoprint = self._get_octoprint_client(user_input)

View File

@ -11,6 +11,11 @@
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
"username": "[%key:common::config_flow::data::username%]" "username": "[%key:common::config_flow::data::username%]"
} }
},
"reauth_confirm": {
"data": {
"username": "[%key:common::config_flow::data::username%]"
}
} }
}, },
"error": { "error": {
@ -21,7 +26,8 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"unknown": "[%key:common::config_flow::error::unknown%]", "unknown": "[%key:common::config_flow::error::unknown%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"auth_failed": "Failed to retrieve application api key" "auth_failed": "Failed to retrieve application api key",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}, },
"progress": { "progress": {
"get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'." "get_api_key": "Open the OctoPrint UI and click 'Allow' on the Access Request for 'Home Assistant'."

View File

@ -4,6 +4,7 @@
"already_configured": "Device is already configured", "already_configured": "Device is already configured",
"auth_failed": "Failed to retrieve application api key", "auth_failed": "Failed to retrieve application api key",
"cannot_connect": "Failed to connect", "cannot_connect": "Failed to connect",
"reauth_successful": "Re-authentication was successful",
"unknown": "Unexpected error" "unknown": "Unexpected error"
}, },
"error": { "error": {
@ -24,6 +25,11 @@
"username": "Username", "username": "Username",
"verify_ssl": "Verify SSL certificate" "verify_ssl": "Verify SSL certificate"
} }
},
"reauth_confirm": {
"data": {
"username": "Username"
}
} }
} }
} }

View File

@ -533,3 +533,55 @@ async def test_duplicate_ssdp_ignored(hass: HomeAssistant) -> None:
) )
assert result["type"] == "abort" assert result["type"] == "abort"
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_reauth_form(hass):
"""Test we get the form."""
entry = MockConfigEntry(
domain=DOMAIN,
data={
"username": "testuser",
"host": "1.1.1.1",
"name": "Printer",
"port": 81,
"ssl": True,
"path": "/",
},
unique_id="1234",
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={
"entry_id": entry.entry_id,
"source": config_entries.SOURCE_REAUTH,
"unique_id": entry.unique_id,
},
data=entry.data,
)
assert result["type"] == "form"
assert not result["errors"]
with patch(
"pyoctoprintapi.OctoprintClient.request_app_key", return_value="test-key"
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"username": "testuser",
},
)
await hass.async_block_till_done()
assert result["type"] == "progress"
with patch(
"homeassistant.components.octoprint.async_setup_entry",
return_value=True,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
)
await hass.async_block_till_done()
assert result2["type"] == "abort"
assert result2["reason"] == "reauth_successful"