1
0
mirror of https://github.com/home-assistant/core.git synced 2026-06-01 05:04:21 +01:00
Files
core/tests/components/homevolt/test_config_flow.py
T
2026-05-17 13:09:56 +02:00

472 lines
14 KiB
Python

"""Tests for the Homevolt config flow."""
from ipaddress import IPv4Address
from unittest.mock import AsyncMock, MagicMock
from homevolt import HomevoltAuthenticationError, HomevoltConnectionError
import pytest
from homeassistant.components.homevolt.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from tests.common import MockConfigEntry
DISCOVERY_INFO = ZeroconfServiceInfo(
ip_address=IPv4Address("192.168.1.123"),
ip_addresses=[IPv4Address("192.168.1.123")],
port=80,
hostname="homevolt.local.",
type="_http._tcp.local.",
name="homevolt._http._tcp.local.",
properties={},
)
async def test_full_flow_success(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_homevolt_client: MagicMock
) -> None:
"""Test a complete successful user flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
user_input = {
CONF_HOST: "192.168.1.100",
}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Homevolt"
assert result["data"] == {CONF_HOST: "192.168.1.100", CONF_PASSWORD: None}
assert result["result"].unique_id == "40580137858664"
assert len(mock_setup_entry.mock_calls) == 1
async def test_flow_auth_error_then_password_success(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_homevolt_client: MagicMock
) -> None:
"""Test flow when authentication is required."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
user_input = {
CONF_HOST: "192.168.1.100",
}
mock_homevolt_client.update_info.side_effect = HomevoltAuthenticationError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "credentials"
assert result["errors"] == {}
# Now provide password - should succeed
mock_homevolt_client.update_info.side_effect = None
password_input = {
CONF_PASSWORD: "test-password",
}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], password_input
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Homevolt"
assert result["data"] == {
CONF_HOST: "192.168.1.100",
CONF_PASSWORD: "test-password",
}
assert result["result"].unique_id == "40580137858664"
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
("exception", "expected_error"),
[
(HomevoltConnectionError, "cannot_connect"),
(Exception, "unknown"),
],
)
async def test_step_user_errors(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_homevolt_client: MagicMock,
exception: Exception,
expected_error: str,
) -> None:
"""Test error cases for the user step with recovery."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
user_input = {
CONF_HOST: "192.168.1.100",
}
mock_homevolt_client.update_info.side_effect = exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {"base": expected_error}
mock_homevolt_client.update_info.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input,
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Homevolt"
assert result["data"] == {CONF_HOST: "192.168.1.100", CONF_PASSWORD: None}
assert result["result"].unique_id == "40580137858664"
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_setup_entry")
async def test_duplicate_entry(
hass: HomeAssistant,
mock_homevolt_client: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that a duplicate device_id aborts the flow."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
user_input = {
CONF_HOST: "192.168.1.200",
}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_credentials_step_invalid_password(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_homevolt_client: MagicMock
) -> None:
"""Test invalid password in credentials step shows error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
user_input = {
CONF_HOST: "192.168.1.100",
}
mock_homevolt_client.update_info.side_effect = HomevoltAuthenticationError
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "credentials"
# Provide wrong password - should show error
password_input = {
CONF_PASSWORD: "wrong-password",
}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], password_input
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "credentials"
assert result["errors"] == {"base": "invalid_auth"}
mock_homevolt_client.update_info.side_effect = None
password_input = {
CONF_PASSWORD: "correct-password",
}
result = await hass.config_entries.flow.async_configure(
result["flow_id"], password_input
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Homevolt"
assert result["data"] == {
CONF_HOST: "192.168.1.100",
CONF_PASSWORD: "correct-password",
}
assert result["result"].unique_id == "40580137858664"
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_setup_entry")
async def test_reauth_success(
hass: HomeAssistant,
mock_homevolt_client: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test successful reauthentication flow."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["description_placeholders"] == {
"host": "127.0.0.1",
"name": "Homevolt",
}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_PASSWORD: "new-password"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.unique_id == "40580137858664"
assert mock_config_entry.data == {
CONF_HOST: "127.0.0.1",
CONF_PASSWORD: "new-password",
}
@pytest.mark.parametrize(
("exception", "expected_error"),
[
(HomevoltAuthenticationError, "invalid_auth"),
(HomevoltConnectionError, "cannot_connect"),
(Exception, "unknown"),
],
)
@pytest.mark.usefixtures("mock_setup_entry")
async def test_reauth_flow_errors(
hass: HomeAssistant,
mock_homevolt_client: MagicMock,
mock_config_entry: MockConfigEntry,
exception: Exception,
expected_error: str,
) -> None:
"""Test reauthentication flow with errors and recovery."""
mock_config_entry.add_to_hass(hass)
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
mock_homevolt_client.update_info.side_effect = exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_PASSWORD: "wrong-password"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"] == {"base": expected_error}
mock_homevolt_client.update_info.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_PASSWORD: "correct-password"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data == {
CONF_HOST: "127.0.0.1",
CONF_PASSWORD: "correct-password",
}
async def test_zeroconf_confirm_flow_success(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_homevolt_client: MagicMock
) -> None:
"""Test zeroconf flow shows confirm step before creating entry."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
assert result["description_placeholders"] == {"host": "192.168.1.123"}
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Homevolt"
assert result["data"] == {CONF_HOST: "192.168.1.123", CONF_PASSWORD: None}
assert result["result"].unique_id == "40580137858664"
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("mock_setup_entry")
async def test_zeroconf_duplicate_aborts(
hass: HomeAssistant,
mock_homevolt_client: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test zeroconf flow aborts when unique id is already configured."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
assert mock_config_entry.data[CONF_HOST] == "192.168.1.123"
async def test_zeroconf_confirm_with_password_success(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_homevolt_client: MagicMock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test zeroconf confirm collects password when auth is required."""
mock_homevolt_client.update_info.side_effect = HomevoltAuthenticationError
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
assert result["description_placeholders"] == {"host": "192.168.1.123"}
mock_homevolt_client.update_info.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_PASSWORD: "test-password"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Homevolt"
assert result["data"] == {
CONF_HOST: "192.168.1.123",
CONF_PASSWORD: "test-password",
}
assert result["result"].unique_id == "40580137858664"
assert len(mock_setup_entry.mock_calls) == 1
async def test_zeroconf_confirm_with_password_invalid_then_success(
hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_homevolt_client: MagicMock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test zeroconf confirm shows error on invalid password, then succeeds."""
mock_homevolt_client.update_info.side_effect = HomevoltAuthenticationError
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_PASSWORD: "wrong-password"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "zeroconf_confirm"
assert result["errors"] == {"base": "invalid_auth"}
mock_homevolt_client.update_info.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_PASSWORD: "correct-password"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Homevolt"
assert result["data"] == {
CONF_HOST: "192.168.1.123",
CONF_PASSWORD: "correct-password",
}
assert result["result"].unique_id == "40580137858664"
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
("exception", "expected_reason"),
[
(HomevoltConnectionError, "cannot_connect"),
(Exception("Unexpected error"), "unknown"),
],
ids=["connection_error", "unknown_error"],
)
@pytest.mark.usefixtures("mock_setup_entry")
async def test_zeroconf_error_aborts(
hass: HomeAssistant,
mock_homevolt_client: MagicMock,
exception: Exception,
expected_reason: str,
) -> None:
"""Test zeroconf flow aborts on error during discovery."""
mock_homevolt_client.update_info.side_effect = exception
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=DISCOVERY_INFO,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == expected_reason