"""Tests for the Rain Bird config flow.""" from collections.abc import Generator from http import HTTPStatus from typing import Any from unittest.mock import Mock, patch import pytest from homeassistant import config_entries from homeassistant.components.rainbird import DOMAIN from homeassistant.components.rainbird.const import ATTR_DURATION from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult, FlowResultType from .conftest import ( CONFIG_ENTRY_DATA, HOST, MAC_ADDRESS_UNIQUE_ID, PASSWORD, SERIAL_NUMBER, SERIAL_RESPONSE, URL, WIFI_PARAMS_RESPONSE, ZERO_SERIAL_RESPONSE, mock_json_response, mock_response, ) from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse @pytest.fixture(name="responses") def mock_responses() -> list[AiohttpClientMockResponse]: """Set up fake serial number response when testing the connection.""" return [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)] @pytest.fixture(autouse=True) async def config_entry_data() -> None: """Fixture to disable config entry setup for exercising config flow.""" return None @pytest.fixture(autouse=True) async def mock_setup() -> Generator[Mock, None, None]: """Fixture for patching out integration setup.""" with patch( "homeassistant.components.rainbird.async_setup_entry", return_value=True, ) as mock_setup: yield mock_setup async def complete_flow(hass: HomeAssistant) -> FlowResult: """Start the config flow and enter the host and password.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert not result.get("errors") assert "flow_id" in result return await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: HOST, CONF_PASSWORD: PASSWORD}, ) @pytest.mark.parametrize( ("responses", "expected_config_entry", "expected_unique_id"), [ ( [ mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE), ], CONFIG_ENTRY_DATA, MAC_ADDRESS_UNIQUE_ID, ), ( [ mock_response(ZERO_SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE), ], {**CONFIG_ENTRY_DATA, "serial_number": 0}, MAC_ADDRESS_UNIQUE_ID, ), ], ) async def test_controller_flow( hass: HomeAssistant, mock_setup: Mock, expected_config_entry: dict[str, str], expected_unique_id: int | None, ) -> None: """Test the controller is setup correctly.""" result = await complete_flow(hass) assert result.get("type") == "create_entry" assert result.get("title") == HOST assert "result" in result assert dict(result["result"].data) == expected_config_entry assert result["result"].options == {ATTR_DURATION: 6} assert result["result"].unique_id == expected_unique_id assert len(mock_setup.mock_calls) == 1 @pytest.mark.parametrize( ( "config_entry_unique_id", "config_entry_data", "config_flow_responses", "expected_config_entry", ), [ ( "other-serial-number", {**CONFIG_ENTRY_DATA, "host": "other-host"}, [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)], CONFIG_ENTRY_DATA, ), ( "11:22:33:44:55:66", { **CONFIG_ENTRY_DATA, "host": "other-host", }, [ mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE), ], CONFIG_ENTRY_DATA, ), ( None, {**CONFIG_ENTRY_DATA, "serial_number": 0, "host": "other-host"}, [ mock_response(ZERO_SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE), ], {**CONFIG_ENTRY_DATA, "serial_number": 0}, ), ], ids=["with-serial", "with-mac-address", "zero-serial"], ) async def test_multiple_config_entries( hass: HomeAssistant, config_entry: MockConfigEntry, responses: list[AiohttpClientMockResponse], config_flow_responses: list[AiohttpClientMockResponse], expected_config_entry: dict[str, Any] | None, ) -> None: """Test setting up multiple config entries that refer to different devices.""" await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED responses.clear() responses.extend(config_flow_responses) result = await complete_flow(hass) assert result.get("type") == FlowResultType.CREATE_ENTRY assert dict(result.get("result").data) == expected_config_entry entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 2 @pytest.mark.parametrize( ( "config_entry_unique_id", "config_entry_data", "config_flow_responses", "expected_config_entry_data", ), [ # Config entry is a pure duplicate with the same mac address unique id ( MAC_ADDRESS_UNIQUE_ID, CONFIG_ENTRY_DATA, [ mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE), ], CONFIG_ENTRY_DATA, ), # Old unique id with serial, but same host ( SERIAL_NUMBER, CONFIG_ENTRY_DATA, [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)], CONFIG_ENTRY_DATA, ), # Old unique id with no serial, but same host ( None, {**CONFIG_ENTRY_DATA, "serial_number": 0}, [ mock_response(ZERO_SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE), ], {**CONFIG_ENTRY_DATA, "serial_number": 0}, ), # Enters a different hostname that points to the same mac address ( MAC_ADDRESS_UNIQUE_ID, { **CONFIG_ENTRY_DATA, "host": f"other-{HOST}", }, [mock_response(SERIAL_RESPONSE), mock_json_response(WIFI_PARAMS_RESPONSE)], CONFIG_ENTRY_DATA, # Updated the host ), ], ids=[ "duplicate-mac-unique-id", "duplicate-host-legacy-serial-number", "duplicate-host-port-no-serial", "duplicate-duplicate-hostname", ], ) async def test_duplicate_config_entries( hass: HomeAssistant, config_entry: MockConfigEntry, responses: list[AiohttpClientMockResponse], config_flow_responses: list[AiohttpClientMockResponse], expected_config_entry_data: dict[str, Any], ) -> None: """Test that a device can not be registered twice.""" await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state == ConfigEntryState.LOADED responses.clear() responses.extend(config_flow_responses) result = await complete_flow(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert dict(config_entry.data) == expected_config_entry_data async def test_controller_cannot_connect( hass: HomeAssistant, mock_setup: Mock, responses: list[AiohttpClientMockResponse], aioclient_mock: AiohttpClientMocker, ) -> None: """Test an error talking to the controller.""" # Controller response with a failure responses.clear() responses.append( AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE) ) result = await complete_flow(hass) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} assert not mock_setup.mock_calls async def test_controller_timeout( hass: HomeAssistant, mock_setup: Mock, ) -> None: """Test an error talking to the controller.""" with patch( "homeassistant.components.rainbird.config_flow.asyncio.timeout", side_effect=TimeoutError, ): result = await complete_flow(hass) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "timeout_connect"} assert not mock_setup.mock_calls async def test_options_flow(hass: HomeAssistant, mock_setup: Mock) -> None: """Test config flow options.""" # Setup config flow result = await complete_flow(hass) assert result.get("type") == "create_entry" assert result.get("title") == HOST assert "result" in result assert result["result"].data == CONFIG_ENTRY_DATA assert result["result"].options == {ATTR_DURATION: 6} # Assert single config entry is loaded config_entry = next(iter(hass.config_entries.async_entries(DOMAIN))) assert config_entry.state == ConfigEntryState.LOADED # Initiate the options flow result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" # Change the default duration result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ATTR_DURATION: 5} ) assert result.get("type") == FlowResultType.CREATE_ENTRY assert config_entry.options == { ATTR_DURATION: 5, }