"""Test the RabbitAir config flow.""" from __future__ import annotations from collections.abc import Generator from ipaddress import ip_address from unittest.mock import Mock, patch import pytest from rabbitair import Mode, Model, Speed from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.rabbitair.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MAC from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.device_registry import format_mac TEST_HOST = "1.1.1.1" TEST_NAME = "abcdef1234_123456789012345678" TEST_TOKEN = "0123456789abcdef0123456789abcdef" TEST_MAC = "01:23:45:67:89:AB" TEST_FIRMWARE = "2.3.17" TEST_HARDWARE = "1.0.0.4" TEST_UNIQUE_ID = format_mac(TEST_MAC) TEST_TITLE = "Rabbit Air" ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo( ip_address=ip_address(TEST_HOST), ip_addresses=[ip_address(TEST_HOST)], port=9009, hostname=f"{TEST_NAME}.local.", type="_rabbitair._udp.local.", name=f"{TEST_NAME}._rabbitair._udp.local.", properties={"id": TEST_MAC.replace(":", "")}, ) @pytest.fixture(autouse=True) def use_mocked_zeroconf(mock_async_zeroconf): """Mock zeroconf in all tests.""" @pytest.fixture def rabbitair_connect() -> Generator[None, None, None]: """Mock connection.""" with ( patch("rabbitair.UdpClient.get_info", return_value=get_mock_info()), patch("rabbitair.UdpClient.get_state", return_value=get_mock_state()), ): yield def get_mock_info(mac: str = TEST_MAC) -> Mock: """Return a mock device info instance.""" mock_info = Mock() mock_info.mac = mac return mock_info def get_mock_state( model: Model | None = Model.A3, main_firmware: str | None = TEST_HARDWARE, power: bool | None = True, mode: Mode | None = Mode.Auto, speed: Speed | None = Speed.Low, wifi_firmware: str | None = TEST_FIRMWARE, ) -> Mock: """Return a mock device state instance.""" mock_state = Mock() mock_state.model = model mock_state.main_firmware = main_firmware mock_state.power = power mock_state.mode = mode mock_state.speed = speed mock_state.wifi_firmware = wifi_firmware return mock_state @pytest.mark.usefixtures("rabbitair_connect") async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( "homeassistant.components.rabbitair.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: TEST_HOST, CONF_ACCESS_TOKEN: TEST_TOKEN, }, ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_TITLE assert result2["data"] == { CONF_HOST: TEST_HOST, CONF_ACCESS_TOKEN: TEST_TOKEN, CONF_MAC: TEST_MAC, } assert result2["result"].unique_id == TEST_UNIQUE_ID assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( ("error_type", "base_value"), [ (ValueError, "invalid_access_token"), (OSError, "invalid_host"), (TimeoutError, "timeout_connect"), (Exception, "cannot_connect"), ], ) async def test_form_cannot_connect( hass: HomeAssistant, error_type: type[Exception], base_value: str ) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( "rabbitair.UdpClient.get_info", side_effect=error_type, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: TEST_HOST, CONF_ACCESS_TOKEN: TEST_TOKEN, }, ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": base_value} async def test_form_unknown_error(hass: HomeAssistant) -> None: """Test we handle unknown error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( "homeassistant.components.rabbitair.config_flow.validate_input", side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: TEST_HOST, CONF_ACCESS_TOKEN: TEST_TOKEN, }, ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @pytest.mark.usefixtures("rabbitair_connect") async def test_zeroconf_discovery(hass: HomeAssistant) -> None: """Test zeroconf discovery setup flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZEROCONF_DATA ) assert result["type"] is FlowResultType.FORM assert not result["errors"] with patch( "homeassistant.components.rabbitair.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: TEST_NAME + ".local", CONF_ACCESS_TOKEN: TEST_TOKEN, }, ) await hass.async_block_till_done() assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_TITLE assert result2["data"] == { CONF_HOST: TEST_NAME + ".local", CONF_ACCESS_TOKEN: TEST_TOKEN, CONF_MAC: TEST_MAC, } assert result2["result"].unique_id == TEST_UNIQUE_ID assert len(mock_setup_entry.mock_calls) == 1 result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZEROCONF_DATA ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured"