mirror of
https://github.com/home-assistant/core.git
synced 2026-05-18 14:29:57 +01:00
477 lines
15 KiB
Python
477 lines
15 KiB
Python
"""Tests for the Duco config flow."""
|
|
|
|
from ipaddress import IPv4Address
|
|
from unittest.mock import ANY, AsyncMock, patch
|
|
|
|
from duco_connectivity import BoardInfo, DucoConnectionError, DucoError, LanInfo
|
|
import pytest
|
|
|
|
from homeassistant.components.duco.const import DOMAIN
|
|
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF
|
|
from homeassistant.const import CONF_HOST
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.data_entry_flow import FlowResultType
|
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
|
|
|
from .conftest import TEST_HOST, TEST_MAC, USER_INPUT
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
|
|
ip_address=IPv4Address(TEST_HOST),
|
|
ip_addresses=[IPv4Address(TEST_HOST)],
|
|
port=80,
|
|
hostname="duco_061293.local.",
|
|
type="_http._tcp.local.",
|
|
name="DUCO [a0dd6c061293]._http._tcp.local.",
|
|
properties={},
|
|
)
|
|
|
|
DHCP_DISCOVERY = DhcpServiceInfo(
|
|
ip=TEST_HOST,
|
|
hostname="duco_ddeeff",
|
|
macaddress="aabbccddeeff",
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_setup_entry")
|
|
async def test_user_flow_success(
|
|
hass: HomeAssistant, mock_duco_client: AsyncMock
|
|
) -> None:
|
|
"""Test a 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"] == {}
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], USER_INPUT
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "SILENT_CONNECT"
|
|
assert result["data"] == USER_INPUT
|
|
assert result["result"].unique_id == TEST_MAC
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "expected_error"),
|
|
[
|
|
(DucoConnectionError("Connection refused"), "cannot_connect"),
|
|
(DucoError("Unexpected error"), "unknown"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("mock_setup_entry")
|
|
async def test_user_flow_error(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
exception: Exception,
|
|
expected_error: str,
|
|
) -> None:
|
|
"""Test handling of connection and unknown errors in the user flow."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
|
|
mock_duco_client.async_get_board_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_duco_client.async_get_board_info.side_effect = None
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], USER_INPUT
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
|
|
|
|
async def test_user_flow_duplicate(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test that a duplicate config entry is aborted."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
# Second attempt for the same device
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], USER_INPUT
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_setup_entry")
|
|
async def test_zeroconf_discovery_new_device(
|
|
hass: HomeAssistant, mock_duco_client: AsyncMock
|
|
) -> None:
|
|
"""Test zeroconf discovery shows confirmation form and creates entry."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_ZEROCONF},
|
|
data=ZEROCONF_DISCOVERY,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "discovery_confirm"
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], user_input={}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "SILENT_CONNECT"
|
|
assert result["data"] == USER_INPUT
|
|
assert result["result"].unique_id == TEST_MAC
|
|
|
|
|
|
async def test_zeroconf_discovery_updates_host(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test zeroconf discovery updates the host of an existing entry."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
new_ip = "192.168.1.200"
|
|
discovery = ZeroconfServiceInfo(
|
|
ip_address=IPv4Address(new_ip),
|
|
ip_addresses=[IPv4Address(new_ip)],
|
|
port=80,
|
|
hostname="duco_061293.local.",
|
|
type="_http._tcp.local.",
|
|
name="DUCO [a0dd6c061293]._http._tcp.local.",
|
|
properties={},
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_ZEROCONF},
|
|
data=discovery,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
assert mock_config_entry.data[CONF_HOST] == new_ip
|
|
|
|
|
|
async def test_zeroconf_discovery_already_configured_same_ip(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test zeroconf discovery with unchanged IP aborts as already_configured."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_ZEROCONF},
|
|
data=ZEROCONF_DISCOVERY,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "expected_reason"),
|
|
[
|
|
(DucoConnectionError("Connection refused"), "cannot_connect"),
|
|
(DucoError("Unexpected error"), "unknown"),
|
|
],
|
|
)
|
|
async def test_zeroconf_discovery_exceptions(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
exception: Exception,
|
|
expected_reason: str,
|
|
) -> None:
|
|
"""Test zeroconf discovery aborts on connection and unknown errors."""
|
|
mock_duco_client.async_get_board_info.side_effect = exception
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_ZEROCONF},
|
|
data=ZEROCONF_DISCOVERY,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == expected_reason
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_setup_entry")
|
|
async def test_reconfigure_flow_success(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test a successful reconfigure flow updates host and reloads."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
result = await mock_config_entry.start_reconfigure_flow(hass)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "reconfigure"
|
|
|
|
mock_duco_client.async_get_board_info.side_effect = DucoConnectionError(
|
|
"Connection refused"
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {CONF_HOST: "192.168.1.50"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "reconfigure"
|
|
assert result["errors"] == {"base": "cannot_connect"}
|
|
|
|
mock_duco_client.async_get_board_info.side_effect = None
|
|
new_host = "192.168.1.200"
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {CONF_HOST: new_host}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "reconfigure_successful"
|
|
assert mock_config_entry.data[CONF_HOST] == new_host
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_setup_entry")
|
|
async def test_reconfigure_flow_wrong_device(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test reconfigure flow aborts when pointing to a different device."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
result = await mock_config_entry.start_reconfigure_flow(hass)
|
|
|
|
# Simulate a different MAC returned by the new host
|
|
different_mac = "11:22:33:44:55:66"
|
|
mock_duco_client.async_get_lan_info.return_value = LanInfo(
|
|
mode="WIFI_CLIENT",
|
|
ip="192.168.1.200",
|
|
net_mask="255.255.255.0",
|
|
default_gateway="192.168.1.1",
|
|
dns="8.8.8.8",
|
|
mac=different_mac,
|
|
host_name="duco-other",
|
|
rssi_wifi=-60,
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {CONF_HOST: "192.168.1.200"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "unique_id_mismatch"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_setup_entry")
|
|
@pytest.mark.parametrize(
|
|
("exception", "expected_error"),
|
|
[
|
|
(DucoConnectionError("Connection refused"), "cannot_connect"),
|
|
(DucoError("Unexpected error"), "unknown"),
|
|
],
|
|
)
|
|
async def test_reconfigure_flow_error(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
exception: Exception,
|
|
expected_error: str,
|
|
) -> None:
|
|
"""Test reconfigure flow shows error on connection failure."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
result = await mock_config_entry.start_reconfigure_flow(hass)
|
|
|
|
mock_duco_client.async_get_board_info.side_effect = exception
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {CONF_HOST: "192.168.1.200"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "reconfigure"
|
|
assert result["errors"] == {"base": expected_error}
|
|
|
|
mock_duco_client.async_get_board_info.side_effect = None
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {CONF_HOST: "192.168.1.200"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "reconfigure_successful"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_setup_entry")
|
|
async def test_dhcp_discovery_new_device(
|
|
hass: HomeAssistant, mock_duco_client: AsyncMock
|
|
) -> None:
|
|
"""Test DHCP discovery of a new device shows confirmation form and creates entry."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_DHCP},
|
|
data=DHCP_DISCOVERY,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "discovery_confirm"
|
|
assert result["description_placeholders"] == {"name": "SILENT_CONNECT"}
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], user_input={}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "SILENT_CONNECT"
|
|
assert result["data"] == USER_INPUT
|
|
assert result["result"].unique_id == TEST_MAC
|
|
|
|
|
|
async def test_dhcp_discovery_updates_host(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test DHCP discovery updates the host of an existing entry."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
new_ip = "192.168.1.200"
|
|
discovery = DhcpServiceInfo(
|
|
ip=new_ip,
|
|
hostname="duco_ddeeff",
|
|
macaddress="aabbccddeeff",
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_DHCP},
|
|
data=discovery,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
assert mock_config_entry.data[CONF_HOST] == new_ip
|
|
|
|
|
|
async def test_dhcp_discovery_already_configured_same_ip(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test DHCP discovery with unchanged IP aborts as already_configured."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_DHCP},
|
|
data=DHCP_DISCOVERY,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "expected_reason"),
|
|
[
|
|
(DucoConnectionError("Connection refused"), "cannot_connect"),
|
|
(DucoError("Unexpected error"), "unknown"),
|
|
],
|
|
)
|
|
async def test_dhcp_discovery_exceptions(
|
|
hass: HomeAssistant,
|
|
mock_duco_client: AsyncMock,
|
|
exception: Exception,
|
|
expected_reason: str,
|
|
) -> None:
|
|
"""Test DHCP discovery aborts on connection and unknown errors."""
|
|
mock_duco_client.async_get_board_info.side_effect = exception
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_DHCP},
|
|
data=DHCP_DISCOVERY,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == expected_reason
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_setup_entry")
|
|
async def test_dhcp_discovery_exception_recovery(
|
|
hass: HomeAssistant, mock_duco_client: AsyncMock
|
|
) -> None:
|
|
"""Test DHCP discovery recovers after an initial exception and creates the entry."""
|
|
mock_duco_client.async_get_board_info.side_effect = DucoConnectionError(
|
|
"Connection refused"
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_DHCP},
|
|
data=DHCP_DISCOVERY,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "cannot_connect"
|
|
|
|
mock_duco_client.async_get_board_info.side_effect = None
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_DHCP},
|
|
data=DHCP_DISCOVERY,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "discovery_confirm"
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], user_input={}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["result"].unique_id == TEST_MAC
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_setup_entry")
|
|
async def test_user_flow_initializes_client_with_host(
|
|
hass: HomeAssistant, mock_board_info: BoardInfo, mock_lan_info: LanInfo
|
|
) -> None:
|
|
"""Test that the config flow initializes the Duco client with the host."""
|
|
with patch(
|
|
"homeassistant.components.duco.config_flow.DucoClient",
|
|
autospec=True,
|
|
) as mock_client_class:
|
|
mock_client_class.return_value.async_get_board_info.return_value = (
|
|
mock_board_info
|
|
)
|
|
mock_client_class.return_value.async_get_lan_info.return_value = mock_lan_info
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], USER_INPUT
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
mock_client_class.assert_called_once_with(
|
|
session=ANY,
|
|
host=TEST_HOST,
|
|
)
|