"""Test the SMLIGHT SLZB config flow.""" from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock from pysmlight import Info from pysmlight.exceptions import SmlightAuthError, SmlightConnectionError import pytest from homeassistant.components.smlight.const import DOMAIN from homeassistant.config_entries import ( SOURCE_DHCP, SOURCE_RECONFIGURE, 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.dhcp import DhcpServiceInfo from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .conftest import ( MOCK_DEVICE_NAME, MOCK_HOST, MOCK_HOSTNAME, MOCK_PASSWORD, MOCK_USERNAME, ) from tests.common import MockConfigEntry DISCOVERY_INFO = ZeroconfServiceInfo( ip_address=ip_address("192.168.1.161"), ip_addresses=[ip_address("192.168.1.161")], 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("192.168.1.161"), ip_addresses=[ip_address("192.168.1.161")], 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"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOSTNAME, }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SLZB-06p7" assert result["data"] == { CONF_HOST: MOCK_HOSTNAME, } assert result["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert len(mock_setup_entry.mock_calls) == 1 async def test_user_flow_auth( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_setup_entry: AsyncMock ) -> None: """Test the full manual user flow with authentication.""" mock_smlight_client.check_auth_needed.return_value = True 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"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOSTNAME, }, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SLZB-06p7" assert result["data"] == { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, CONF_HOST: MOCK_HOSTNAME, } assert result["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_DEVICE_NAME} assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" progress = [ flow for flow in hass.config_entries.flow.async_progress() if flow["handler"] == DOMAIN ] assert len(progress) == 1 assert progress[0]["flow_id"] == result["flow_id"] assert progress[0]["context"]["confirm_only"] is True result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["context"]["source"] == "zeroconf" assert result["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert result["title"] == "slzb-06" assert result["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_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_DEVICE_NAME} assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" progress = [ flow for flow in hass.config_entries.flow.async_progress() if flow["handler"] == DOMAIN ] assert len(progress) == 1 assert progress[0]["flow_id"] == result["flow_id"] assert progress[0]["context"]["confirm_only"] is True result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth" progress2 = [ flow for flow in hass.config_entries.flow.async_progress() if flow["handler"] == DOMAIN ] assert len(progress2) == 1 assert progress2[0]["flow_id"] == result["flow_id"] result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["context"]["source"] == "zeroconf" assert result["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert result["title"] == "SLZB-06p7" assert result["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) == 2 async def test_zeroconf_unsupported_abort( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_smlight_client: MagicMock, ) -> None: """Test we abort zeroconf flow if device unsupported.""" mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = Info(model="SLZB-X") result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=DISCOVERY_INFO ) assert result["description_placeholders"] == {"host": MOCK_DEVICE_NAME} assert result["type"] is FlowResultType.FORM assert result["step_id"] == "confirm_discovery" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_device" async def test_user_unsupported_abort( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_smlight_client: MagicMock, ) -> None: """Test we abort user flow if unsupported device.""" mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = Info(model="SLZB-X") 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"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOST, }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_device" async def test_user_unsupported_device_abort_auth( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_smlight_client: MagicMock, ) -> None: """Test we abort user flow if unsupported device (with auth).""" mock_smlight_client.check_auth_needed.return_value = True 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"] == {} 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.get_info.side_effect = None mock_smlight_client.get_info.return_value = Info(model="SLZB-X") result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "unsupported_device" @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}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: MOCK_HOST}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @pytest.mark.usefixtures("mock_smlight_client") async def test_user_flow_can_override_discovery( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_setup_entry: AsyncMock, ) -> None: """Test manual user flow can override discovery in progress.""" 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" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOST, }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["context"]["source"] == SOURCE_USER assert result["data"] == { CONF_HOST: MOCK_HOST, } assert result["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert len(mock_setup_entry.mock_calls) == 1 @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}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} 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" result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: "test", CONF_PASSWORD: "bad", }, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} assert result["step_id"] == "auth" mock_smlight_client.authenticate.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: "test", CONF_PASSWORD: "good", }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SLZB-06p7" assert result["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) == 2 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 result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOST, }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "SLZB-06p7" assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smlight_client.get_info.mock_calls) == 2 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 result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result["type"] is FlowResultType.ABORT assert result["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" result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) assert result["type"] is FlowResultType.ABORT assert result["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_DEVICE_NAME} result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["context"]["source"] == "zeroconf" assert result["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff" assert result["title"] == "slzb-06" assert result["data"] == { CONF_HOST: MOCK_HOST, } assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smlight_client.get_info.mock_calls) == 3 @pytest.mark.usefixtures("mock_smlight_client", "mock_setup_entry") async def test_zeroconf_updates_host( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test zeroconf discovery updates host ip.""" mock_config_entry.add_to_hass(hass) service_info = DISCOVERY_INFO service_info.ip_address = ip_address("192.168.1.164") result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_HOST] == "192.168.1.164" @pytest.mark.usefixtures("mock_smlight_client", "mock_setup_entry") async def test_dhcp_discovery_updates_host( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test dhcp discovery updates host ip.""" mock_config_entry.add_to_hass(hass) service_info = DhcpServiceInfo( ip="192.168.1.164", hostname="slzb-06", macaddress="aabbccddeeff", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=service_info ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_HOST] == "192.168.1.164" @pytest.mark.usefixtures("mock_smlight_client", "mock_setup_entry") async def test_dhcp_discovery_aborts( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> None: """Test dhcp discovery updates host ip.""" mock_config_entry.add_to_hass(hass) service_info = DhcpServiceInfo( ip="192.168.1.161", hostname="slzb-06", macaddress="000000000000", ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=service_info ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config_entry.data[CONF_HOST] == "192.168.1.161" @pytest.mark.usefixtures("mock_setup_entry") async def test_reauth_flow( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_config_entry: MockConfigEntry, ) -> 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" result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result["type"] is FlowResultType.ABORT assert result["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 @pytest.mark.usefixtures("mock_setup_entry") async def test_reauth_auth_error( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_config_entry: MockConfigEntry, ) -> 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" result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: "test-bad", }, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" mock_smlight_client.authenticate.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result["type"] is FlowResultType.ABORT assert result["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" result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(mock_smlight_client.authenticate.mock_calls) == 1 @pytest.mark.usefixtures("mock_smlight_client") @pytest.mark.usefixtures("mock_setup_entry") async def test_reconfigure_flow( hass: HomeAssistant, mock_config_entry: MockConfigEntry, ) -> None: """Test reconfigure flow completes successfully.""" mock_config_entry.add_to_hass(hass) result = await mock_config_entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == SOURCE_RECONFIGURE result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOST, }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.data[CONF_HOST] == MOCK_HOST assert mock_config_entry.unique_id == "aa:bb:cc:dd:ee:ff" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @pytest.mark.parametrize( "mock_method", ["check_auth_needed", "get_info"], ) async def test_reconfigure_connect_error( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_config_entry: MockConfigEntry, mock_method: str, ) -> None: """Test reconfigure flow handles connection errors.""" getattr(mock_smlight_client, mock_method).side_effect = SmlightConnectionError mock_config_entry.add_to_hass(hass) result = await mock_config_entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == SOURCE_RECONFIGURE 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"] == SOURCE_RECONFIGURE assert result["errors"] == {"base": "cannot_connect"} @pytest.mark.parametrize( ("get_info_return", "expected_reason"), [ (Info(MAC="AA:BB:CC:DD:EE:FF", model="SLZB-X"), "unsupported_device"), (Info(MAC="11:22:33:44:55:66", model="SLZB-06p7"), "unique_id_mismatch"), ], ) async def test_reconfigure_abort_cases( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_config_entry: MockConfigEntry, get_info_return: Info, expected_reason: str, ) -> None: """Test reconfigure flow aborts on wrong or unsupported device.""" mock_config_entry.add_to_hass(hass) result = await mock_config_entry.start_reconfigure_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == SOURCE_RECONFIGURE mock_smlight_client.get_info.side_effect = None mock_smlight_client.get_info.return_value = get_info_return result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_HOST: MOCK_HOST, }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == expected_reason @pytest.mark.usefixtures("mock_setup_entry") async def test_reconfigure_auth_error( hass: HomeAssistant, mock_smlight_client: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test reconfigure flow handles auth error and redirects to auth step.""" 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_reconfigure_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == SOURCE_RECONFIGURE 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.authenticate.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert mock_config_entry.data == { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, CONF_HOST: MOCK_HOST, } assert mock_config_entry.unique_id == "aa:bb:cc:dd:ee:ff" assert len(mock_smlight_client.authenticate.mock_calls) == 2 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 async def test_options_flow( hass: HomeAssistant, mock_ultima_client: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test options flow does not call the hardware API when switching between non-disabled modes.""" mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ "ble_scanner_mode": "passive", }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "ble_scanner_mode": "passive", } mock_ultima_client.set_ble_proxy.assert_not_called() async def test_options_flow_enable_from_disabled( hass: HomeAssistant, mock_ultima_client: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test options flow toggles the remote adapter on when transitioning from disabled.""" mock_config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( mock_config_entry, options={"ble_scanner_mode": "disabled"} ) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ "ble_scanner_mode": "passive", }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "ble_scanner_mode": "passive", } mock_ultima_client.set_ble_proxy.assert_called_once_with(True) async def test_options_flow_error( hass: HomeAssistant, mock_ultima_client: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test options flow error handling when disabling set_ble_proxy fails and succeeds on retry.""" mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) assert result["type"] is FlowResultType.FORM mock_ultima_client.set_ble_proxy.side_effect = SmlightConnectionError result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ "ble_scanner_mode": "disabled", }, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} mock_ultima_client.set_ble_proxy.side_effect = None result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ "ble_scanner_mode": "disabled", }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "ble_scanner_mode": "disabled", } mock_ultima_client.set_ble_proxy.assert_called_with(False) @pytest.mark.usefixtures("mock_smlight_client") async def test_options_flow_no_ble( hass: HomeAssistant, mock_config_entry: MockConfigEntry, ) -> None: """Test options flow for device without BLE support redirects to no_settings step.""" mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "no_settings" assert result["description_placeholders"] == {"model": "SLZB-06p7"} result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={}, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {} async def test_options_flow_not_loaded( hass: HomeAssistant, mock_config_entry: MockConfigEntry, ) -> None: """Test options flow returns cannot_connect error when config entry has not finished loading.""" mock_config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cannot_connect"} async def test_options_flow_auth_error( hass: HomeAssistant, mock_ultima_client: MagicMock, mock_config_entry: MockConfigEntry, ) -> None: """Test options flow auth error handling when disabling set_ble_proxy raises SmlightAuthError and reauth completes.""" mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) assert result["type"] is FlowResultType.FORM mock_ultima_client.set_ble_proxy.side_effect = SmlightAuthError result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ "ble_scanner_mode": "disabled", }, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} progress_flows = [ flow for flow in hass.config_entries.flow.async_progress() if flow["handler"] == DOMAIN and flow["context"].get("source") == "reauth" ] assert len(progress_flows) == 1 reauth_flow = progress_flows[0] mock_ultima_client.authenticate.side_effect = None mock_ultima_client.check_auth_needed.return_value = True reauth_result = await hass.config_entries.flow.async_configure( reauth_flow["flow_id"], { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, }, ) assert reauth_result["type"] is FlowResultType.ABORT assert reauth_result["reason"] == "reauth_successful" assert mock_config_entry.data == { CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, CONF_HOST: MOCK_HOST, }