"""Test the SMLIGHT SLZB config flow.""" from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError import pytest from homeassistant.components.smlight.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .conftest import MOCK_HOST, MOCK_PASSWORD, MOCK_USERNAME from tests.common import MockConfigEntry DISCOVERY_INFO = ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], hostname="slzb-06.local.", name="mock_name", port=6638, properties={"mac": "AA:BB:CC:DD:EE:FF"}, type="mock_type", ) DISCOVERY_INFO_LEGACY = ZeroconfServiceInfo( ip_address=ip_address("127.0.0.1"), ip_addresses=[ip_address("127.0.0.1")], hostname="slzb-06.local.", name="mock_name", port=6638, properties={}, type="mock_type", ) @pytest.mark.usefixtures("mock_smlight_client") async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test the full manual 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"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOST, }, ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "SLZB-06p7" assert result2["data"] == { CONF_HOST: MOCK_HOST, } assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert len(mock_setup_entry.mock_calls) == 1 async def test_zeroconf_flow( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock, ) -> None: """Test the zeroconf flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO ) assert result["description_placeholders"] == {"host": MOCK_HOST} assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" progress = hass.config_entries.flow.async_progress() assert len(progress) == 1 assert progress[0]["flow_id"] == result["flow_id"] assert progress[0]["context"]["confirm_only"] is True result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["context"]["source"] == "zeroconf" assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert result2["title"] == "slzb-06" assert result2["data"] == { CONF_HOST: MOCK_HOST, } assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smlight_client.get_info.mock_calls) == 1 async def test_zeroconf_flow_auth( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock, ) -> None: """Test the full zeroconf flow including authentication.""" mock_smlight_client.check_auth_needed.return_value = True result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO ) assert result["description_placeholders"] == {"host": MOCK_HOST} assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" progress = hass.config_entries.flow.async_progress() assert len(progress) == 1 assert progress[0]["flow_id"] == result["flow_id"] assert progress[0]["context"]["confirm_only"] is True result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "auth" progress2 = hass.config_entries.flow.async_progress() assert len(progress2) == 1 assert progress2[0]["flow_id"] == result["flow_id"] result3 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["context"]["source"] == "zeroconf" assert result3["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert result3["title"] == "slzb-06" assert result3["data"] == { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, CONF_HOST: MOCK_HOST, } assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smlight_client.get_info.mock_calls) == 1 @pytest.mark.usefixtures("mock_smlight_client") async def test_user_device_exists_abort( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test we abort user flow if device already configured.""" mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={ CONF_HOST: MOCK_HOST, }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @pytest.mark.usefixtures("mock_smlight_client") async def test_zeroconf_device_exists_abort( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test we abort zeroconf flow if device 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" async def test_user_invalid_auth( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock ) -> None: """Test we handle invalid auth.""" mock_smlight_client.check_auth_needed.return_value = True mock_smlight_client.authenticate.side_effect = SmlightAuthError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={ CONF_HOST: MOCK_HOST, }, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: "test", CONF_PASSWORD: "bad", }, ) assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} assert result2["step_id"] == "auth" mock_smlight_client.authenticate.side_effect = None result3 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: "test", CONF_PASSWORD: "good", }, ) assert result3["type"] is FlowResultType.CREATE_ENTRY assert result3["title"] == "SLZB-06p7" assert result3["data"] == { CONF_HOST: MOCK_HOST, CONF_USERNAME: "test", CONF_PASSWORD: "good", } assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smlight_client.get_info.mock_calls) == 1 async def test_user_cannot_connect( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock ) -> None: """Test we handle user cannot connect error.""" mock_smlight_client.check_auth_needed.side_effect = SmlightConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: "unknown.local", }, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "user" mock_smlight_client.check_auth_needed.side_effect = None result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOST, }, ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "SLZB-06p7" assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smlight_client.get_info.mock_calls) == 1 async def test_auth_cannot_connect( hass: HomeAssistant, mock_smlight_client: MagicMock ) -> None: """Test we abort auth step on cannot connect error.""" mock_smlight_client.check_auth_needed.return_value = True result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOST, }, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" mock_smlight_client.check_auth_needed.side_effect = SmlightConnectionError result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "cannot_connect" async def test_zeroconf_cannot_connect( hass: HomeAssistant, mock_smlight_client: MagicMock ) -> None: """Test we abort flow on zeroconf cannot connect error.""" mock_smlight_client.check_auth_needed.side_effect = SmlightConnectionError 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"] == "confirm_discovery" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "cannot_connect" async def test_zeroconf_legacy_cannot_connect( hass: HomeAssistant, mock_smlight_client: MagicMock ) -> None: """Test we abort flow on zeroconf discovery unsupported firmware.""" mock_smlight_client.get_info.side_effect = SmlightConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO_LEGACY, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" @pytest.mark.usefixtures("mock_smlight_client") async def test_zeroconf_legacy_mac( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock ) -> None: """Test we can get unique id MAC address for older firmwares.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO_LEGACY, ) assert result["description_placeholders"] == {"host": MOCK_HOST} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["context"]["source"] == "zeroconf" assert result2["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert result2["title"] == "slzb-06" assert result2["data"] == { CONF_HOST: MOCK_HOST, } assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smlight_client.get_info.mock_calls) == 2 async def test_reauth_flow( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_config_entry: MockConfigEntry, mock_setup_entry: AsyncMock, ) -> None: """Test reauth flow completes successfully.""" mock_smlight_client.check_auth_needed.return_value = True 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" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_config_entry.data == { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, CONF_HOST: MOCK_HOST, } assert len(mock_smlight_client.authenticate.mock_calls) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 async def test_reauth_auth_error( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_config_entry: MockConfigEntry, mock_setup_entry: AsyncMock, ) -> None: """Test reauth flow with authentication error.""" mock_smlight_client.check_auth_needed.return_value = True mock_smlight_client.authenticate.side_effect = SmlightAuthError 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" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: "test-bad", }, ) assert result2["type"] is FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" mock_smlight_client.authenticate.side_effect = None result3 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert mock_config_entry.data == { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, CONF_HOST: MOCK_HOST, } assert len(mock_smlight_client.authenticate.mock_calls) == 2 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 async def test_reauth_connect_error( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test reauth flow with error.""" mock_smlight_client.check_auth_needed.return_value = True mock_smlight_client.authenticate.side_effect = SmlightConnectionError 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" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result2["type"] is FlowResultType.ABORT assert result2["reason"] == "cannot_connect" assert len(mock_smlight_client.authenticate.mock_calls) == 1