From fd10fa1fba8f8ab16a5ad96eb356e7716c978e27 Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:49:08 +0200 Subject: [PATCH] Add reauthentication flow to Uptime Kuma (#148772) --- .../components/uptime_kuma/config_flow.py | 47 +++++++++++++ .../components/uptime_kuma/coordinator.py | 4 +- .../components/uptime_kuma/quality_scale.yaml | 2 +- .../components/uptime_kuma/strings.json | 13 +++- .../uptime_kuma/test_config_flow.py | 70 +++++++++++++++++++ tests/components/uptime_kuma/test_init.py | 29 +++++++- 6 files changed, 160 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/uptime_kuma/config_flow.py b/homeassistant/components/uptime_kuma/config_flow.py index 9866f08bef3..30f9d7ae9ba 100644 --- a/homeassistant/components/uptime_kuma/config_flow.py +++ b/homeassistant/components/uptime_kuma/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -38,6 +39,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( vol.Optional(CONF_API_KEY, default=""): str, } ) +STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Optional(CONF_API_KEY, default=""): str}) class UptimeKumaConfigFlow(ConfigFlow, domain=DOMAIN): @@ -77,3 +79,48 @@ class UptimeKumaConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm reauthentication dialog.""" + errors: dict[str, str] = {} + + entry = self._get_reauth_entry() + + if user_input is not None: + session = async_get_clientsession(self.hass, entry.data[CONF_VERIFY_SSL]) + uptime_kuma = UptimeKuma( + session, + entry.data[CONF_URL], + user_input[CONF_API_KEY], + ) + + try: + await uptime_kuma.metrics() + except UptimeKumaAuthenticationException: + errors["base"] = "invalid_auth" + except UptimeKumaException: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + entry, + data_updates=user_input, + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_REAUTH_DATA_SCHEMA, suggested_values=user_input + ), + errors=errors, + ) diff --git a/homeassistant/components/uptime_kuma/coordinator.py b/homeassistant/components/uptime_kuma/coordinator.py index 788d37cfb84..297bd83e7c8 100644 --- a/homeassistant/components/uptime_kuma/coordinator.py +++ b/homeassistant/components/uptime_kuma/coordinator.py @@ -16,7 +16,7 @@ from pythonkuma import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -59,7 +59,7 @@ class UptimeKumaDataUpdateCoordinator( try: metrics = await self.api.metrics() except UptimeKumaAuthenticationException as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="auth_failed_exception", ) from e diff --git a/homeassistant/components/uptime_kuma/quality_scale.yaml b/homeassistant/components/uptime_kuma/quality_scale.yaml index 145cbf58448..c3d88f7e3c8 100644 --- a/homeassistant/components/uptime_kuma/quality_scale.yaml +++ b/homeassistant/components/uptime_kuma/quality_scale.yaml @@ -38,7 +38,7 @@ rules: integration-owner: done log-when-unavailable: done parallel-updates: done - reauthentication-flow: todo + reauthentication-flow: done test-coverage: done # Gold diff --git a/homeassistant/components/uptime_kuma/strings.json b/homeassistant/components/uptime_kuma/strings.json index 8cd361cccea..0321db1c221 100644 --- a/homeassistant/components/uptime_kuma/strings.json +++ b/homeassistant/components/uptime_kuma/strings.json @@ -13,6 +13,16 @@ "verify_ssl": "Enable SSL certificate verification for secure connections. Disable only if connecting to an Uptime Kuma instance using a self-signed certificate or via IP address", "api_key": "Enter an API key. To create a new API key navigate to **Settings → API Keys** and select **Add API Key**" } + }, + "reauth_confirm": { + "title": "Re-authenticate with Uptime Kuma: {name}", + "description": "The API key for **{name}** is invalid. To re-authenticate with Uptime Kuma provide a new API key below", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "api_key": "[%key:component::uptime_kuma::config::step::user::data_description::api_key%]" + } } }, "error": { @@ -21,7 +31,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "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%]" } }, "entity": { diff --git a/tests/components/uptime_kuma/test_config_flow.py b/tests/components/uptime_kuma/test_config_flow.py index b70cb9d353c..3c1bf902ce8 100644 --- a/tests/components/uptime_kuma/test_config_flow.py +++ b/tests/components/uptime_kuma/test_config_flow.py @@ -120,3 +120,73 @@ async def test_form_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_flow_reauth( + hass: HomeAssistant, + config_entry: MockConfigEntry, +) -> None: + """Test reauth flow.""" + config_entry.add_to_hass(hass) + result = await 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: "newapikey"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.data[CONF_API_KEY] == "newapikey" + + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (UptimeKumaConnectionException, "cannot_connect"), + (UptimeKumaAuthenticationException, "invalid_auth"), + (ValueError, "unknown"), + ], +) +@pytest.mark.usefixtures("mock_pythonkuma") +async def test_flow_reauth_errors( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pythonkuma: AsyncMock, + raise_error: Exception, + text_error: str, +) -> None: + """Test reauth flow errors and recover.""" + config_entry.add_to_hass(hass) + result = await config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_pythonkuma.metrics.side_effect = raise_error + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "newapikey"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": text_error} + + mock_pythonkuma.metrics.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "newapikey"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert config_entry.data[CONF_API_KEY] == "newapikey" + + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/uptime_kuma/test_init.py b/tests/components/uptime_kuma/test_init.py index 57390da60d5..6e2ef43b14d 100644 --- a/tests/components/uptime_kuma/test_init.py +++ b/tests/components/uptime_kuma/test_init.py @@ -5,7 +5,8 @@ from unittest.mock import AsyncMock import pytest from pythonkuma import UptimeKumaAuthenticationException, UptimeKumaException -from homeassistant.config_entries import ConfigEntryState +from homeassistant.components.uptime_kuma.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -50,3 +51,29 @@ async def test_config_entry_not_ready( await hass.async_block_till_done() assert config_entry.state is state + + +async def test_config_reauth_flow( + hass: HomeAssistant, + config_entry: MockConfigEntry, + mock_pythonkuma: AsyncMock, +) -> None: + """Test config entry auth error starts reauth flow.""" + + mock_pythonkuma.metrics.side_effect = UptimeKumaAuthenticationException + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == config_entry.entry_id