Add reauth flow to bosch_alarm (#142251)

* add reauth flow

* fix tests

* move not happy flow to its own test

* reference existing strings

* Update test_config_flow.py
This commit is contained in:
Sanjay Govind 2025-04-06 01:26:35 +13:00 committed by GitHub
parent 904265bca7
commit 9692d637ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 172 additions and 6 deletions

View File

@ -9,7 +9,7 @@ from bosch_alarm_mode2 import Panel
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
@ -34,10 +34,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
await panel.connect() await panel.connect()
except (PermissionError, ValueError) as err: except (PermissionError, ValueError) as err:
await panel.disconnect() await panel.disconnect()
raise ConfigEntryNotReady from err raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from err
except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err: except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err:
await panel.disconnect() await panel.disconnect()
raise ConfigEntryNotReady("Connection failed") from err raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
entry.runtime_data = panel entry.runtime_data = panel

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Mapping
import logging import logging
import ssl import ssl
from typing import Any from typing import Any
@ -163,3 +164,55 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=self.add_suggested_values_to_schema(schema, user_input), data_schema=self.add_suggested_values_to_schema(schema, user_input),
errors=errors, errors=errors,
) )
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an authentication error."""
self._data = dict(entry_data)
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the reauth step."""
errors: dict[str, str] = {}
# Each model variant requires a different authentication flow
if "Solution" in self._data[CONF_MODEL]:
schema = STEP_AUTH_DATA_SCHEMA_SOLUTION
elif "AMAX" in self._data[CONF_MODEL]:
schema = STEP_AUTH_DATA_SCHEMA_AMAX
else:
schema = STEP_AUTH_DATA_SCHEMA_BG
if user_input is not None:
reauth_entry = self._get_reauth_entry()
self._data.update(user_input)
try:
(_, _) = await try_connect(self._data, Panel.LOAD_EXTENDED_INFO)
except (PermissionError, ValueError) as e:
errors["base"] = "invalid_auth"
_LOGGER.error("Authentication Error: %s", e)
except (
OSError,
ConnectionRefusedError,
ssl.SSLError,
TimeoutError,
) as e:
_LOGGER.error("Connection Error: %s", e)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(schema, user_input),
errors=errors,
)

View File

@ -40,7 +40,7 @@ rules:
integration-owner: done integration-owner: done
log-when-unavailable: todo log-when-unavailable: todo
parallel-updates: todo parallel-updates: todo
reauthentication-flow: todo reauthentication-flow: done
test-coverage: done test-coverage: done
# Gold # Gold

View File

@ -22,6 +22,18 @@
"installer_code": "The installer code from your panel", "installer_code": "The installer code from your panel",
"user_code": "The user code from your panel" "user_code": "The user code from your panel"
} }
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data::installer_code%]",
"user_code": "[%key:component::bosch_alarm::config::step::auth::data::user_code%]"
},
"data_description": {
"password": "[%key:component::bosch_alarm::config::step::auth::data_description::password%]",
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data_description::installer_code%]",
"user_code": "[%key:component::bosch_alarm::config::step::auth::data_description::user_code%]"
}
} }
}, },
"error": { "error": {
@ -30,7 +42,16 @@
"unknown": "[%key:common::config_flow::error::unknown%]" "unknown": "[%key:common::config_flow::error::unknown%]"
}, },
"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%]"
}
},
"exceptions": {
"cannot_connect": {
"message": "Could not connect to panel."
},
"authentication_failed": {
"message": "Incorrect credentials for panel."
} }
} }
} }

View File

@ -13,6 +13,8 @@ from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_PORT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from . import setup_integration
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@ -210,3 +212,74 @@ async def test_entry_already_configured_serial(
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
async def test_reauth_flow_success(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
serial_number: str,
config_flow_data: dict[str, Any],
) -> None:
"""Test reauth flow."""
await setup_integration(hass, mock_config_entry)
result = await mock_config_entry.start_reauth_flow(hass)
config_flow_data = {k: f"{v}2" for k, v in config_flow_data.items()}
assert result["step_id"] == "reauth_confirm"
# Now check it works when there are no errors
mock_panel.connect.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=config_flow_data,
)
assert result["reason"] == "reauth_successful"
compare = {**mock_config_entry.data, **config_flow_data}
assert compare == mock_config_entry.data
@pytest.mark.parametrize(
("exception", "message"),
[
(OSError(), "cannot_connect"),
(PermissionError(), "invalid_auth"),
(Exception(), "unknown"),
],
)
async def test_reauth_flow_error(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_panel: AsyncMock,
model_name: str,
serial_number: str,
config_flow_data: dict[str, Any],
exception: Exception,
message: str,
) -> None:
"""Test reauth flow."""
await setup_integration(hass, mock_config_entry)
result = await mock_config_entry.start_reauth_flow(hass)
config_flow_data = {k: f"{v}2" for k, v in config_flow_data.items()}
assert result["step_id"] == "reauth_confirm"
mock_panel.connect.side_effect = exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=config_flow_data,
)
assert result["step_id"] == "reauth_confirm"
assert result["errors"]["base"] == message
mock_panel.connect.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input=config_flow_data,
)
assert result["reason"] == "reauth_successful"
compare = {**mock_config_entry.data, **config_flow_data}
assert compare == mock_config_entry.data

View File

@ -20,12 +20,26 @@ def disable_platform_only():
@pytest.mark.parametrize("model", ["solution_3000"]) @pytest.mark.parametrize("model", ["solution_3000"])
@pytest.mark.parametrize("exception", [PermissionError(), TimeoutError()]) @pytest.mark.parametrize("exception", [PermissionError()])
async def test_incorrect_auth( async def test_incorrect_auth(
hass: HomeAssistant, hass: HomeAssistant,
mock_panel: AsyncMock, mock_panel: AsyncMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
exception: Exception, exception: Exception,
) -> None:
"""Test errors with incorrect auth."""
mock_panel.connect.side_effect = exception
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
@pytest.mark.parametrize("model", ["solution_3000"])
@pytest.mark.parametrize("exception", [TimeoutError()])
async def test_connection_error(
hass: HomeAssistant,
mock_panel: AsyncMock,
mock_config_entry: MockConfigEntry,
exception: Exception,
) -> None: ) -> None:
"""Test errors with incorrect auth.""" """Test errors with incorrect auth."""
mock_panel.connect.side_effect = exception mock_panel.connect.side_effect = exception