diff --git a/homeassistant/components/analytics_insights/config_flow.py b/homeassistant/components/analytics_insights/config_flow.py index b409a9c0fb9..d2ebdd943a2 100644 --- a/homeassistant/components/analytics_insights/config_flow.py +++ b/homeassistant/components/analytics_insights/config_flow.py @@ -53,10 +53,25 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the initial step.""" self._async_abort_entries_match() - if user_input: - return self.async_create_entry( - title="Home Assistant Analytics Insights", data={}, options=user_input - ) + errors: dict[str, str] = {} + if user_input is not None: + if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( + CONF_TRACKED_CUSTOM_INTEGRATIONS + ): + errors["base"] = "no_integrations_selected" + else: + return self.async_create_entry( + title="Home Assistant Analytics Insights", + data={}, + options={ + CONF_TRACKED_INTEGRATIONS: user_input.get( + CONF_TRACKED_INTEGRATIONS, [] + ), + CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get( + CONF_TRACKED_CUSTOM_INTEGRATIONS, [] + ), + }, + ) client = HomeassistantAnalyticsClient( session=async_get_clientsession(self.hass) @@ -78,16 +93,17 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN): ] return self.async_show_form( step_id="user", + errors=errors, data_schema=vol.Schema( { - vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector( + vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=options, multiple=True, sort=True, ) ), - vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( + vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=list(custom_integrations), multiple=True, @@ -106,8 +122,24 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" - if user_input: - return self.async_create_entry(title="", data=user_input) + errors: dict[str, str] = {} + if user_input is not None: + if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get( + CONF_TRACKED_CUSTOM_INTEGRATIONS + ): + errors["base"] = "no_integrations_selected" + else: + return self.async_create_entry( + title="", + data={ + CONF_TRACKED_INTEGRATIONS: user_input.get( + CONF_TRACKED_INTEGRATIONS, [] + ), + CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get( + CONF_TRACKED_CUSTOM_INTEGRATIONS, [] + ), + }, + ) client = HomeassistantAnalyticsClient( session=async_get_clientsession(self.hass) @@ -129,17 +161,18 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry): ] return self.async_show_form( step_id="init", + errors=errors, data_schema=self.add_suggested_values_to_schema( vol.Schema( { - vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector( + vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=options, multiple=True, sort=True, ) ), - vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( + vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector( SelectSelectorConfig( options=list(custom_integrations), multiple=True, diff --git a/homeassistant/components/analytics_insights/strings.json b/homeassistant/components/analytics_insights/strings.json index 58e47d1df08..6de1ab9dbe4 100644 --- a/homeassistant/components/analytics_insights/strings.json +++ b/homeassistant/components/analytics_insights/strings.json @@ -15,6 +15,9 @@ "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, + "error": { + "no_integration_selected": "You must select at least one integration to track" } }, "options": { @@ -32,6 +35,9 @@ }, "abort": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "error": { + "no_integration_selected": "[%key:component::analytics_insights::config::error::no_integration_selected%]" } }, "entity": { diff --git a/tests/components/analytics_insights/test_config_flow.py b/tests/components/analytics_insights/test_config_flow.py index 8cefa29ee7b..6ddbe285df7 100644 --- a/tests/components/analytics_insights/test_config_flow.py +++ b/tests/components/analytics_insights/test_config_flow.py @@ -1,6 +1,8 @@ """Test the Homeassistant Analytics config flow.""" +from typing import Any from unittest.mock import AsyncMock +import pytest from python_homeassistant_analytics import HomeassistantAnalyticsConnectionError from homeassistant import config_entries @@ -16,8 +18,45 @@ from tests.common import MockConfigEntry from tests.components.analytics_insights import setup_integration +@pytest.mark.parametrize( + ("user_input", "expected_options"), + [ + ( + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + ), + ( + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + }, + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, + ), + ( + { + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + { + CONF_TRACKED_INTEGRATIONS: [], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + ), + ], +) async def test_form( - hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_analytics_client: AsyncMock + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_analytics_client: AsyncMock, + user_input: dict[str, Any], + expected_options: dict[str, Any], ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -25,6 +64,50 @@ async def test_form( ) assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Home Assistant Analytics Insights" + assert result["data"] == {} + assert result["options"] == expected_options + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "user_input", + [ + { + CONF_TRACKED_INTEGRATIONS: [], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, + {}, + ], +) +async def test_submitting_empty_form( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_analytics_client: AsyncMock, + user_input: dict[str, Any], +) -> None: + """Test we can't submit an empty form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "no_integrations_selected"} + result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -81,10 +164,45 @@ async def test_form_already_configured( assert result["reason"] == "already_configured" +@pytest.mark.parametrize( + ("user_input", "expected_options"), + [ + ( + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + ), + ( + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + }, + { + CONF_TRACKED_INTEGRATIONS: ["youtube"], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, + ), + ( + { + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + { + CONF_TRACKED_INTEGRATIONS: [], + CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], + }, + ), + ], +) async def test_options_flow( hass: HomeAssistant, mock_analytics_client: AsyncMock, mock_config_entry: MockConfigEntry, + user_input: dict[str, Any], + expected_options: dict[str, Any], ) -> None: """Test options flow.""" await setup_integration(hass, mock_config_entry) @@ -95,7 +213,50 @@ async def test_options_flow( mock_analytics_client.get_integrations.reset_mock() result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={ + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == expected_options + await hass.async_block_till_done() + mock_analytics_client.get_integrations.assert_called_once() + + +@pytest.mark.parametrize( + "user_input", + [ + { + CONF_TRACKED_INTEGRATIONS: [], + CONF_TRACKED_CUSTOM_INTEGRATIONS: [], + }, + {}, + ], +) +async def test_submitting_empty_options_flow( + hass: HomeAssistant, + mock_analytics_client: AsyncMock, + mock_config_entry: MockConfigEntry, + user_input: dict[str, Any], +) -> None: + """Test options flow.""" + await setup_integration(hass, mock_config_entry) + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "no_integrations_selected"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + { CONF_TRACKED_INTEGRATIONS: ["youtube", "hue"], CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], }, @@ -108,7 +269,6 @@ async def test_options_flow( CONF_TRACKED_CUSTOM_INTEGRATIONS: ["hacs"], } await hass.async_block_till_done() - mock_analytics_client.get_integrations.assert_called_once() async def test_options_flow_cannot_connect(