mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 00:20:30 +01:00
881 lines
27 KiB
Python
881 lines
27 KiB
Python
"""Test the Ubiquiti airOS config flow."""
|
|
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from airos.exceptions import (
|
|
AirOSConnectionAuthenticationError,
|
|
AirOSConnectionSetupError,
|
|
AirOSDeviceConnectionError,
|
|
AirOSEndpointError,
|
|
AirOSKeyDataMissingError,
|
|
AirOSListenerError,
|
|
)
|
|
from airos.helpers import DetectDeviceData
|
|
import pytest
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.airos.const import (
|
|
DEFAULT_USERNAME,
|
|
DOMAIN,
|
|
HOSTNAME,
|
|
IP_ADDRESS,
|
|
MAC_ADDRESS,
|
|
SECTION_ADVANCED_SETTINGS,
|
|
)
|
|
from homeassistant.config_entries import (
|
|
SOURCE_DHCP,
|
|
SOURCE_REAUTH,
|
|
SOURCE_RECONFIGURE,
|
|
SOURCE_USER,
|
|
)
|
|
from homeassistant.const import (
|
|
CONF_HOST,
|
|
CONF_PASSWORD,
|
|
CONF_SSL,
|
|
CONF_USERNAME,
|
|
CONF_VERIFY_SSL,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.data_entry_flow import FlowResultType
|
|
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
|
|
|
from . import AirOSData
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
NEW_PASSWORD = "new_password"
|
|
REAUTH_STEP = "reauth_confirm"
|
|
RECONFIGURE_STEP = "reconfigure"
|
|
|
|
MOCK_ADVANCED_SETTINGS = {
|
|
CONF_SSL: True,
|
|
CONF_VERIFY_SSL: False,
|
|
}
|
|
|
|
MOCK_CONFIG = {
|
|
CONF_HOST: "1.1.1.1",
|
|
CONF_USERNAME: DEFAULT_USERNAME,
|
|
CONF_PASSWORD: "test-password",
|
|
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
|
}
|
|
MOCK_CONFIG_REAUTH = {
|
|
CONF_HOST: "1.1.1.1",
|
|
CONF_USERNAME: DEFAULT_USERNAME,
|
|
CONF_PASSWORD: "wrong-password",
|
|
}
|
|
|
|
MOCK_DISC_DEV1 = {
|
|
MAC_ADDRESS: "00:11:22:33:44:55",
|
|
IP_ADDRESS: "192.168.1.100",
|
|
HOSTNAME: "Test-Device-1",
|
|
}
|
|
MOCK_DISC_DEV2 = {
|
|
MAC_ADDRESS: "AA:BB:CC:DD:EE:FF",
|
|
IP_ADDRESS: "192.168.1.101",
|
|
HOSTNAME: "Test-Device-2",
|
|
}
|
|
MOCK_DISC_EXISTS = {
|
|
MAC_ADDRESS: "01:23:45:67:89:AB",
|
|
IP_ADDRESS: "192.168.1.102",
|
|
HOSTNAME: "Existing-Device",
|
|
}
|
|
|
|
|
|
async def test_manual_flow_creates_entry(
|
|
hass: HomeAssistant,
|
|
ap_fixture: dict[str, Any],
|
|
mock_airos_client: AsyncMock,
|
|
mock_async_get_firmware_data: AsyncMock,
|
|
mock_setup_entry: AsyncMock,
|
|
) -> None:
|
|
"""Test we get the user form and create the appropriate entry."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_USER},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.MENU
|
|
assert "manual" in result["menu_options"]
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "manual"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "manual"
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], MOCK_CONFIG
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "NanoStation 5AC ap name"
|
|
assert result["result"].unique_id == "01:23:45:67:89:AB"
|
|
assert result["data"] == MOCK_CONFIG
|
|
assert len(mock_setup_entry.mock_calls) == 1
|
|
|
|
|
|
async def test_form_duplicate_entry(
|
|
hass: HomeAssistant,
|
|
mock_airos_client: AsyncMock,
|
|
mock_async_get_firmware_data: AsyncMock,
|
|
) -> None:
|
|
"""Test the form does not allow duplicate entries."""
|
|
mock_entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="01:23:45:67:89:AB",
|
|
data=MOCK_CONFIG,
|
|
)
|
|
mock_entry.add_to_hass(hass)
|
|
|
|
flow_start = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_USER},
|
|
)
|
|
|
|
menu = await hass.config_entries.flow.async_configure(
|
|
flow_start["flow_id"], {"next_step_id": "manual"}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
menu["flow_id"], MOCK_CONFIG
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "error"),
|
|
[
|
|
(AirOSConnectionAuthenticationError, "invalid_auth"),
|
|
(AirOSConnectionSetupError, "cannot_connect"),
|
|
(AirOSDeviceConnectionError, "cannot_connect"),
|
|
(AirOSKeyDataMissingError, "key_data_missing"),
|
|
(Exception, "unknown"),
|
|
],
|
|
)
|
|
async def test_form_exception_handling(
|
|
hass: HomeAssistant,
|
|
mock_setup_entry: AsyncMock,
|
|
ap_fixture: dict[str, Any],
|
|
mock_airos_client: AsyncMock,
|
|
mock_async_get_firmware_data: AsyncMock,
|
|
exception: Exception,
|
|
error: str,
|
|
) -> None:
|
|
"""Test we handle exceptions."""
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
side_effect=exception,
|
|
):
|
|
flow_start = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_USER},
|
|
)
|
|
|
|
menu = await hass.config_entries.flow.async_configure(
|
|
flow_start["flow_id"], {"next_step_id": "manual"}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
menu["flow_id"], MOCK_CONFIG
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": error}
|
|
|
|
fw_major = int(ap_fixture.host.fwversion.lstrip("v").split(".", 1)[0])
|
|
valid_data = DetectDeviceData(
|
|
fw_major=fw_major,
|
|
mac=ap_fixture.derived.mac,
|
|
hostname=ap_fixture.host.hostname,
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
return_value=valid_data,
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
MOCK_CONFIG,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "NanoStation 5AC ap name"
|
|
assert result["data"] == MOCK_CONFIG
|
|
assert len(mock_setup_entry.mock_calls) == 1
|
|
|
|
|
|
async def test_reauth_flow_scenario(
|
|
hass: HomeAssistant,
|
|
ap_fixture: AirOSData,
|
|
mock_airos_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
mock_setup_entry: AsyncMock,
|
|
) -> None:
|
|
"""Test successful reauthentication."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
mock_airos_client.login.side_effect = AirOSConnectionAuthenticationError
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
side_effect=AirOSConnectionAuthenticationError,
|
|
):
|
|
flow = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_REAUTH, "entry_id": mock_config_entry.entry_id},
|
|
data=mock_config_entry.data,
|
|
)
|
|
|
|
assert flow["type"] == FlowResultType.FORM
|
|
assert flow["step_id"] == REAUTH_STEP
|
|
|
|
fw_major = int(ap_fixture.host.fwversion.lstrip("v").split(".", 1)[0])
|
|
valid_data = DetectDeviceData(
|
|
fw_major=fw_major,
|
|
mac=ap_fixture.derived.mac,
|
|
hostname=ap_fixture.host.hostname,
|
|
)
|
|
|
|
mock_firmware = AsyncMock(return_value=valid_data)
|
|
with (
|
|
patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
new=mock_firmware,
|
|
),
|
|
patch(
|
|
"homeassistant.components.airos.async_get_firmware_data",
|
|
new=mock_firmware,
|
|
),
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
flow["flow_id"],
|
|
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
|
)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
# Always test resolution
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "reauth_successful"
|
|
|
|
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
|
assert updated_entry.data[CONF_PASSWORD] == NEW_PASSWORD
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("reauth_exception", "expected_error"),
|
|
[
|
|
(AirOSConnectionAuthenticationError, "invalid_auth"),
|
|
(AirOSDeviceConnectionError, "cannot_connect"),
|
|
(AirOSKeyDataMissingError, "key_data_missing"),
|
|
(Exception, "unknown"),
|
|
],
|
|
ids=[
|
|
"invalid_auth",
|
|
"cannot_connect",
|
|
"key_data_missing",
|
|
"unknown",
|
|
],
|
|
)
|
|
async def test_reauth_flow_scenarios(
|
|
hass: HomeAssistant,
|
|
ap_fixture: AirOSData,
|
|
expected_error: str,
|
|
mock_airos_client: AsyncMock,
|
|
mock_async_get_firmware_data: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
reauth_exception: Exception,
|
|
) -> None:
|
|
"""Test reauthentication from start (failure) to finish (success)."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
side_effect=AirOSConnectionAuthenticationError,
|
|
):
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
|
|
flow = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_REAUTH, "entry_id": mock_config_entry.entry_id},
|
|
data=mock_config_entry.data,
|
|
)
|
|
|
|
assert flow["type"] == FlowResultType.FORM
|
|
assert flow["step_id"] == REAUTH_STEP
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
side_effect=reauth_exception,
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
flow["flow_id"],
|
|
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == REAUTH_STEP
|
|
assert result["errors"] == {"base": expected_error}
|
|
|
|
fw_major = int(ap_fixture.host.fwversion.lstrip("v").split(".", 1)[0])
|
|
valid_data = DetectDeviceData(
|
|
fw_major=fw_major,
|
|
mac=ap_fixture.derived.mac,
|
|
hostname=ap_fixture.host.hostname,
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
new=AsyncMock(return_value=valid_data),
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
flow["flow_id"],
|
|
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
|
)
|
|
|
|
assert result["type"] == FlowResultType.ABORT
|
|
assert result["reason"] == "reauth_successful"
|
|
|
|
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
|
assert updated_entry.data[CONF_PASSWORD] == NEW_PASSWORD
|
|
|
|
|
|
async def test_reauth_unique_id_mismatch(
|
|
hass: HomeAssistant,
|
|
ap_fixture: AirOSData,
|
|
mock_airos_client: AsyncMock,
|
|
mock_async_get_firmware_data: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test reauthentication failure when the unique ID changes."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
side_effect=AirOSConnectionAuthenticationError,
|
|
):
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
|
|
flow = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_REAUTH, "entry_id": mock_config_entry.entry_id},
|
|
data=mock_config_entry.data,
|
|
)
|
|
|
|
fw_major = int(ap_fixture.host.fwversion.lstrip("v").split(".", 1)[0])
|
|
valid_data = DetectDeviceData(
|
|
fw_major=fw_major,
|
|
mac="FF:23:45:67:89:AB",
|
|
hostname=ap_fixture.host.hostname,
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
new=AsyncMock(return_value=valid_data),
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
flow["flow_id"],
|
|
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "unique_id_mismatch"
|
|
|
|
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
|
assert updated_entry.data[CONF_PASSWORD] != NEW_PASSWORD
|
|
|
|
|
|
async def test_successful_reconfigure(
|
|
hass: HomeAssistant,
|
|
mock_airos_client: AsyncMock,
|
|
mock_async_get_firmware_data: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test successful reconfigure."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == RECONFIGURE_STEP
|
|
|
|
user_input = {
|
|
CONF_PASSWORD: NEW_PASSWORD,
|
|
SECTION_ADVANCED_SETTINGS: {
|
|
CONF_SSL: True,
|
|
CONF_VERIFY_SSL: True,
|
|
},
|
|
}
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
user_input=user_input,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "reconfigure_successful"
|
|
|
|
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
|
assert updated_entry.data[CONF_PASSWORD] == NEW_PASSWORD
|
|
assert updated_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL] is True
|
|
assert updated_entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL] is True
|
|
|
|
assert updated_entry.data[CONF_HOST] == MOCK_CONFIG[CONF_HOST]
|
|
assert updated_entry.data[CONF_USERNAME] == MOCK_CONFIG[CONF_USERNAME]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("reconfigure_exception", "expected_error"),
|
|
[
|
|
(AirOSConnectionAuthenticationError, "invalid_auth"),
|
|
(AirOSDeviceConnectionError, "cannot_connect"),
|
|
(AirOSKeyDataMissingError, "key_data_missing"),
|
|
(Exception, "unknown"),
|
|
],
|
|
ids=[
|
|
"invalid_auth",
|
|
"cannot_connect",
|
|
"key_data_missing",
|
|
"unknown",
|
|
],
|
|
)
|
|
async def test_reconfigure_flow_failure(
|
|
hass: HomeAssistant,
|
|
expected_error: str,
|
|
mock_airos_client: AsyncMock,
|
|
mock_async_get_firmware_data: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
reconfigure_exception: Exception,
|
|
) -> None:
|
|
"""Test reconfigure from start (failure) to finish (success)."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
|
)
|
|
|
|
user_input = {
|
|
CONF_PASSWORD: NEW_PASSWORD,
|
|
SECTION_ADVANCED_SETTINGS: {
|
|
CONF_SSL: True,
|
|
CONF_VERIFY_SSL: True,
|
|
},
|
|
}
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
side_effect=reconfigure_exception,
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
user_input=user_input,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == RECONFIGURE_STEP
|
|
assert result["errors"] == {"base": expected_error}
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
user_input=user_input,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "reconfigure_successful"
|
|
|
|
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
|
assert updated_entry.data[CONF_PASSWORD] == NEW_PASSWORD
|
|
|
|
|
|
async def test_reconfigure_unique_id_mismatch(
|
|
hass: HomeAssistant,
|
|
ap_fixture: AirOSData,
|
|
mock_airos_client: AsyncMock,
|
|
mock_async_get_firmware_data: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test reconfiguration failure when the unique ID changes."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
|
)
|
|
flow_id = result["flow_id"]
|
|
|
|
fw_major = int(ap_fixture.host.fwversion.lstrip("v").split(".", 1)[0])
|
|
mismatched_data = DetectDeviceData(
|
|
fw_major=fw_major,
|
|
mac="FF:23:45:67:89:AB",
|
|
hostname=ap_fixture.host.hostname,
|
|
)
|
|
|
|
user_input = {
|
|
CONF_PASSWORD: NEW_PASSWORD,
|
|
SECTION_ADVANCED_SETTINGS: {
|
|
CONF_SSL: True,
|
|
CONF_VERIFY_SSL: True,
|
|
},
|
|
}
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
new=AsyncMock(return_value=mismatched_data),
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
flow_id,
|
|
user_input=user_input,
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "unique_id_mismatch"
|
|
|
|
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
|
assert updated_entry.data[CONF_PASSWORD] == MOCK_CONFIG[CONF_PASSWORD]
|
|
assert (
|
|
updated_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL]
|
|
== MOCK_CONFIG[SECTION_ADVANCED_SETTINGS][CONF_SSL]
|
|
)
|
|
|
|
|
|
async def test_discover_flow_no_devices_found(
|
|
hass: HomeAssistant,
|
|
mock_discovery_method: AsyncMock,
|
|
) -> None:
|
|
"""Test discovery flow aborts when no devices are found."""
|
|
mock_discovery_method.return_value = {}
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "discovery"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
|
assert result["step_id"] == "discovery"
|
|
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
async def test_discover_flow_one_device_found(
|
|
hass: HomeAssistant,
|
|
mock_airos_client: AsyncMock,
|
|
mock_discovery_method: AsyncMock,
|
|
mock_setup_entry: AsyncMock,
|
|
) -> None:
|
|
"""Test discovery flow goes straight to credentials when one device is found."""
|
|
mock_discovery_method.return_value = {MOCK_DISC_DEV1[MAC_ADDRESS]: MOCK_DISC_DEV1}
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "discovery"}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
|
|
|
# With only one device, the flow should skip the select step and
|
|
# go directly to configure_device.
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "configure_device"
|
|
assert result["description_placeholders"]["device_name"] == MOCK_DISC_DEV1[HOSTNAME]
|
|
|
|
valid_data = DetectDeviceData(
|
|
fw_major=8,
|
|
mac=MOCK_DISC_DEV1[MAC_ADDRESS],
|
|
hostname=MOCK_DISC_DEV1[HOSTNAME],
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
new=AsyncMock(return_value=valid_data),
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{
|
|
CONF_USERNAME: DEFAULT_USERNAME,
|
|
CONF_PASSWORD: "test-password",
|
|
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
|
},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == MOCK_DISC_DEV1[HOSTNAME]
|
|
assert result["data"][CONF_HOST] == MOCK_DISC_DEV1[IP_ADDRESS]
|
|
|
|
|
|
async def test_discover_flow_multiple_devices_found(
|
|
hass: HomeAssistant,
|
|
mock_airos_client: AsyncMock,
|
|
mock_async_get_firmware_data: AsyncMock,
|
|
mock_discovery_method: AsyncMock,
|
|
mock_setup_entry: AsyncMock,
|
|
) -> None:
|
|
"""Test discovery flow with multiple devices found, requiring a selection step."""
|
|
mock_discovery_method.return_value = {
|
|
MOCK_DISC_DEV1[MAC_ADDRESS]: MOCK_DISC_DEV1,
|
|
MOCK_DISC_DEV2[MAC_ADDRESS]: MOCK_DISC_DEV2,
|
|
}
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.MENU
|
|
assert "discovery" in result["menu_options"]
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "discovery"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
|
assert result["step_id"] == "discovery"
|
|
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "select_device"
|
|
|
|
expected_options = {
|
|
MOCK_DISC_DEV1[MAC_ADDRESS]: (
|
|
f"{MOCK_DISC_DEV1[HOSTNAME]} ({MOCK_DISC_DEV1[IP_ADDRESS]})"
|
|
),
|
|
MOCK_DISC_DEV2[MAC_ADDRESS]: (
|
|
f"{MOCK_DISC_DEV2[HOSTNAME]} ({MOCK_DISC_DEV2[IP_ADDRESS]})"
|
|
),
|
|
}
|
|
actual_options = result["data_schema"].schema[vol.Required(MAC_ADDRESS)].container
|
|
assert actual_options == expected_options
|
|
|
|
# Select one of the devices
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {MAC_ADDRESS: MOCK_DISC_DEV1[MAC_ADDRESS]}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "configure_device"
|
|
assert result["description_placeholders"]["device_name"] == MOCK_DISC_DEV1[HOSTNAME]
|
|
|
|
valid_data = DetectDeviceData(
|
|
fw_major=8,
|
|
mac=MOCK_DISC_DEV1[MAC_ADDRESS],
|
|
hostname=MOCK_DISC_DEV1[HOSTNAME],
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
new=AsyncMock(return_value=valid_data),
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{
|
|
CONF_USERNAME: DEFAULT_USERNAME,
|
|
CONF_PASSWORD: "test-password",
|
|
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
|
},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == MOCK_DISC_DEV1[HOSTNAME]
|
|
assert result["data"][CONF_HOST] == MOCK_DISC_DEV1[IP_ADDRESS]
|
|
|
|
|
|
async def test_discover_flow_with_existing_device(
|
|
hass: HomeAssistant,
|
|
mock_discovery_method: AsyncMock,
|
|
mock_airos_client: AsyncMock,
|
|
) -> None:
|
|
"""Test that discovery ignores devices that are already configured."""
|
|
# Add a mock config entry for an existing device
|
|
mock_entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id=MOCK_DISC_EXISTS[MAC_ADDRESS],
|
|
data=MOCK_CONFIG,
|
|
)
|
|
mock_entry.add_to_hass(hass)
|
|
|
|
# Mock discovery to find both a new device and the existing one
|
|
mock_discovery_method.return_value = {
|
|
MOCK_DISC_DEV1[MAC_ADDRESS]: MOCK_DISC_DEV1,
|
|
MOCK_DISC_EXISTS[MAC_ADDRESS]: MOCK_DISC_EXISTS,
|
|
}
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "discovery"}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
|
|
|
# The flow should proceed with only the new device
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "configure_device"
|
|
assert result["description_placeholders"]["device_name"] == MOCK_DISC_DEV1[HOSTNAME]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "reason"),
|
|
[
|
|
(AirOSEndpointError, "detect_error"),
|
|
(AirOSListenerError, "listen_error"),
|
|
(Exception, "discovery_failed"),
|
|
],
|
|
)
|
|
async def test_discover_flow_discovery_exceptions(
|
|
hass: HomeAssistant,
|
|
mock_discovery_method,
|
|
exception: Exception,
|
|
reason: str,
|
|
) -> None:
|
|
"""Test discovery flow aborts on various discovery exceptions."""
|
|
mock_discovery_method.side_effect = exception
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "discovery"}
|
|
)
|
|
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == reason
|
|
|
|
|
|
async def test_configure_device_flow_exceptions(
|
|
hass: HomeAssistant,
|
|
mock_discovery_method: AsyncMock,
|
|
mock_airos_client: AsyncMock,
|
|
) -> None:
|
|
"""Test configure_device step handles authentication and connection exceptions."""
|
|
mock_discovery_method.return_value = {MOCK_DISC_DEV1[MAC_ADDRESS]: MOCK_DISC_DEV1}
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": SOURCE_USER}
|
|
)
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"], {"next_step_id": "discovery"}
|
|
)
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
side_effect=AirOSConnectionAuthenticationError,
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{
|
|
CONF_USERNAME: "wrong-user",
|
|
CONF_PASSWORD: "wrong-password",
|
|
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
|
},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": "invalid_auth"}
|
|
|
|
with patch(
|
|
"homeassistant.components.airos.config_flow.async_get_firmware_data",
|
|
side_effect=AirOSDeviceConnectionError,
|
|
):
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{
|
|
CONF_USERNAME: DEFAULT_USERNAME,
|
|
CONF_PASSWORD: "some-password",
|
|
SECTION_ADVANCED_SETTINGS: MOCK_ADVANCED_SETTINGS,
|
|
},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["errors"] == {"base": "cannot_connect"}
|
|
|
|
|
|
async def test_dhcp_ip_changed_updates_entry(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""DHCP event with new IP should update the config entry and reload."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
macaddress = mock_config_entry.unique_id.lower().replace(":", "").replace("-", "")
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_DHCP},
|
|
data=DhcpServiceInfo(
|
|
ip="1.1.1.2",
|
|
hostname="airos",
|
|
macaddress=macaddress,
|
|
),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
assert mock_config_entry.data[CONF_HOST] == "1.1.1.2"
|
|
|
|
|
|
async def test_dhcp_mac_mismatch(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""DHCP event with non-matching MAC should abort."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_DHCP},
|
|
data=DhcpServiceInfo(
|
|
ip="1.1.1.2",
|
|
hostname="airos",
|
|
macaddress="aabbccddeeff",
|
|
),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "unreachable"
|
|
|
|
|
|
async def test_dhcp_ip_unchanged(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""DHCP event with same IP should abort."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": SOURCE_DHCP},
|
|
data=DhcpServiceInfo(
|
|
ip=mock_config_entry.data[CONF_HOST],
|
|
hostname="airos",
|
|
macaddress=mock_config_entry.unique_id.lower()
|
|
.replace(":", "")
|
|
.replace("-", ""),
|
|
),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|