"""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