"""Define tests for the Nettigo Air Monitor config flow.""" from ipaddress import ip_address from unittest.mock import patch from nettigo_air_monitor import ApiError, AuthFailedError, CannotGetMacError import pytest from homeassistant.components import zeroconf from homeassistant.components.nam.const import DOMAIN from homeassistant.config_entries import ( SOURCE_REAUTH, 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 tests.common import MockConfigEntry DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( ip_address=ip_address("10.10.2.3"), ip_addresses=[ip_address("10.10.2.3")], hostname="mock_hostname", name="mock_name", port=None, properties={}, type="mock_type", ) VALID_CONFIG = {"host": "10.10.2.3"} VALID_AUTH = {"username": "fake_username", "password": "fake_password"} DEVICE_CONFIG = {"www_basicauth_enabled": False} DEVICE_CONFIG_AUTH = {"www_basicauth_enabled": True} async def test_form_create_entry_without_auth(hass: HomeAssistant) -> None: """Test that the user step without auth works.""" 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"] == {} with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), patch( "homeassistant.components.nam.async_setup_entry", return_value=True ) as mock_setup_entry, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert len(mock_setup_entry.mock_calls) == 1 async def test_form_create_entry_with_auth(hass: HomeAssistant) -> None: """Test that the user step with auth works.""" 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"] == {} with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG_AUTH, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), patch( "homeassistant.components.nam.async_setup_entry", return_value=True ) as mock_setup_entry, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_CONFIG, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_AUTH, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" assert result["data"]["password"] == "fake_password" assert len(mock_setup_entry.mock_calls) == 1 async def test_reauth_successful(hass: HomeAssistant) -> None: """Test starting a reauthentication flow.""" entry = MockConfigEntry( domain=DOMAIN, title="10.10.2.3", unique_id="aa:bb:cc:dd:ee:ff", data={"host": "10.10.2.3"}, ) entry.add_to_hass(hass) with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG_AUTH, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=VALID_AUTH, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" async def test_reauth_unsuccessful(hass: HomeAssistant) -> None: """Test starting a reauthentication flow.""" entry = MockConfigEntry( domain=DOMAIN, title="10.10.2.3", unique_id="aa:bb:cc:dd:ee:ff", data={"host": "10.10.2.3"}, ) entry.add_to_hass(hass) with patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=ApiError("API Error"), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}, data=entry.data, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=VALID_AUTH, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" @pytest.mark.parametrize( "error", [ (ApiError("API Error"), "cannot_connect"), (AuthFailedError("Auth Error"), "invalid_auth"), (TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], ) async def test_form_with_auth_errors(hass: HomeAssistant, error) -> None: """Test we handle errors when auth is required.""" exc, base_error = error with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=AuthFailedError("Auth Error"), ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" with patch( "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=exc, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_AUTH, ) assert result["errors"] == {"base": base_error} @pytest.mark.parametrize( "error", [ (ApiError("API Error"), "cannot_connect"), (TimeoutError, "cannot_connect"), (ValueError, "unknown"), ], ) async def test_form_errors(hass: HomeAssistant, error) -> None: """Test we handle errors.""" exc, base_error = error with patch( "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=exc, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG, ) assert result["errors"] == {"base": base_error} async def test_form_abort(hass: HomeAssistant) -> None: """Test we handle abort after error.""" with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", side_effect=CannotGetMacError("Cannot get MAC address from device"), ), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "device_unsupported" async def test_form_already_configured(hass: HomeAssistant) -> None: """Test that errors are shown when duplicates are added.""" entry = MockConfigEntry( domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=VALID_CONFIG ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP assert entry.data["host"] == "1.1.1.1" async def test_zeroconf(hass: HomeAssistant) -> None: """Test we get the form.""" with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), ): result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, context={"source": SOURCE_ZEROCONF}, ) context = next( flow["context"] for flow in hass.config_entries.flow.async_progress() if flow["flow_id"] == result["flow_id"] ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" assert context["confirm_only"] is True with patch( "homeassistant.components.nam.async_setup_entry", return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"] == {"host": "10.10.2.3"} assert len(mock_setup_entry.mock_calls) == 1 async def test_zeroconf_with_auth(hass: HomeAssistant) -> None: """Test that the zeroconf step with auth works.""" with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=AuthFailedError("Auth Error"), ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), ): result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, context={"source": SOURCE_ZEROCONF}, ) context = next( flow["context"] for flow in hass.config_entries.flow.async_progress() if flow["flow_id"] == result["flow_id"] ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG_AUTH, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), patch( "homeassistant.components.nam.async_setup_entry", return_value=True ) as mock_setup_entry, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_AUTH, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" assert result["data"]["password"] == "fake_password" assert len(mock_setup_entry.mock_calls) == 1 async def test_zeroconf_host_already_configured(hass: HomeAssistant) -> None: """Test that errors are shown when host is already configured.""" entry = MockConfigEntry( domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=VALID_CONFIG ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, context={"source": SOURCE_ZEROCONF}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" @pytest.mark.parametrize( "error", [ (ApiError("API Error"), "cannot_connect"), (CannotGetMacError("Cannot get MAC address from device"), "device_unsupported"), ], ) async def test_zeroconf_errors(hass: HomeAssistant, error) -> None: """Test we handle errors.""" exc, reason = error with patch( "homeassistant.components.nam.NettigoAirMonitor.initialize", side_effect=exc, ): result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, context={"source": SOURCE_ZEROCONF}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == reason async def test_reconfigure_successful(hass: HomeAssistant) -> None: """Test starting a reconfigure flow.""" entry = MockConfigEntry( domain=DOMAIN, title="10.10.2.3", unique_id="aa:bb:cc:dd:ee:ff", data={ CONF_HOST: "10.10.2.3", CONF_USERNAME: "fake_username", CONF_PASSWORD: "fake_password", }, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": SOURCE_RECONFIGURE, "entry_id": entry.entry_id, }, data=entry.data, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure_confirm" with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG_AUTH, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "10.10.10.10"}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert entry.data == { CONF_HOST: "10.10.10.10", CONF_USERNAME: "fake_username", CONF_PASSWORD: "fake_password", } async def test_reconfigure_not_successful(hass: HomeAssistant) -> None: """Test starting a reconfigure flow but no connection found.""" entry = MockConfigEntry( domain=DOMAIN, title="10.10.2.3", unique_id="aa:bb:cc:dd:ee:ff", data={ CONF_HOST: "10.10.2.3", CONF_USERNAME: "fake_username", CONF_PASSWORD: "fake_password", }, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": SOURCE_RECONFIGURE, "entry_id": entry.entry_id, }, data=entry.data, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure_confirm" with patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=ApiError("API Error"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "10.10.10.10"}, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure_confirm" assert result["errors"] == {"base": "cannot_connect"} with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG_AUTH, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "10.10.10.10"}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" assert entry.data == { CONF_HOST: "10.10.10.10", CONF_USERNAME: "fake_username", CONF_PASSWORD: "fake_password", } async def test_reconfigure_not_the_same_device(hass: HomeAssistant) -> None: """Test starting the reconfiguration process, but with a different printer.""" entry = MockConfigEntry( domain=DOMAIN, title="10.10.2.3", unique_id="11:22:33:44:55:66", data={ CONF_HOST: "10.10.2.3", CONF_USERNAME: "fake_username", CONF_PASSWORD: "fake_password", }, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": SOURCE_RECONFIGURE, "entry_id": entry.entry_id, }, data=entry.data, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reconfigure_confirm" with ( patch( "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", return_value=DEVICE_CONFIG_AUTH, ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "10.10.10.10"}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "another_device"