From be13f3fbcfe92ec4c7f0d273f958e9a68a779c5a Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Fri, 25 Nov 2022 15:05:01 +0100 Subject: [PATCH] Add API key validation for Forecast.Solar (#80856) Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- .../components/forecast_solar/config_flow.py | 16 ++- .../components/forecast_solar/strings.json | 3 + .../forecast_solar/translations/en.json | 3 + tests/components/forecast_solar/conftest.py | 11 +- .../forecast_solar/test_config_flow.py | 121 ++++++++++++++---- 5 files changed, 126 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py index 600ed363a8b..e74585da35b 100644 --- a/homeassistant/components/forecast_solar/config_flow.py +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Forecast.Solar integration.""" from __future__ import annotations +import re from typing import Any import voluptuous as vol @@ -9,7 +10,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv from .const import ( CONF_AZIMUTH, @@ -20,6 +21,8 @@ from .const import ( DOMAIN, ) +RE_API_KEY = re.compile(r"^[a-zA-Z0-9]{16}$") + class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Forecast.Solar.""" @@ -88,8 +91,16 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" + errors = {} if user_input is not None: - return self.async_create_entry(title="", data=user_input) + if (api_key := user_input.get(CONF_API_KEY)) and RE_API_KEY.match( + api_key + ) is None: + errors[CONF_API_KEY] = "invalid_api_key" + else: + return self.async_create_entry( + title="", data=user_input | {CONF_API_KEY: api_key or None} + ) return self.async_show_form( step_id="init", @@ -129,4 +140,5 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): ): vol.Coerce(int), } ), + errors=errors, ) diff --git a/homeassistant/components/forecast_solar/strings.json b/homeassistant/components/forecast_solar/strings.json index 041935c59ef..a7bc0190f5f 100644 --- a/homeassistant/components/forecast_solar/strings.json +++ b/homeassistant/components/forecast_solar/strings.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" + }, "step": { "init": { "description": "These values allow tweaking the Forecast.Solar result. Please refer to the documentation if a field is unclear.", diff --git a/homeassistant/components/forecast_solar/translations/en.json b/homeassistant/components/forecast_solar/translations/en.json index 56c210a7c19..35542dcdf75 100644 --- a/homeassistant/components/forecast_solar/translations/en.json +++ b/homeassistant/components/forecast_solar/translations/en.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Invalid API key" + }, "step": { "init": { "data": { diff --git a/tests/components/forecast_solar/conftest.py b/tests/components/forecast_solar/conftest.py index 31256c95866..ea6eb40b542 100644 --- a/tests/components/forecast_solar/conftest.py +++ b/tests/components/forecast_solar/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator from datetime import datetime, timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from forecast_solar import models import pytest @@ -22,6 +22,15 @@ from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.forecast_solar.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index 2380e65aabb..616dcba4a36 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Forecast.Solar config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock from homeassistant.components.forecast_solar.const import ( CONF_AZIMUTH, @@ -17,7 +17,7 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_user_flow(hass: HomeAssistant) -> None: +async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: """Test the full user configuration flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -27,20 +27,17 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert result.get("step_id") == SOURCE_USER assert "flow_id" in result - with patch( - "homeassistant.components.forecast_solar.async_setup_entry", return_value=True - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_NAME: "Name", - CONF_LATITUDE: 52.42, - CONF_LONGITUDE: 4.42, - CONF_AZIMUTH: 142, - CONF_DECLINATION: 42, - CONF_MODULES_POWER: 4242, - }, - ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_NAME: "Name", + CONF_LATITUDE: 52.42, + CONF_LONGITUDE: 4.42, + CONF_AZIMUTH: 142, + CONF_DECLINATION: 42, + CONF_MODULES_POWER: 4242, + }, + ) assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" @@ -57,16 +54,15 @@ async def test_user_flow(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_options_flow( - hass: HomeAssistant, mock_config_entry: MockConfigEntry +async def test_options_flow_invalid_api( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: - """Test config flow options.""" + """Test options config flow when API key is invalid.""" mock_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.forecast_solar.async_setup_entry", return_value=True - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + 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) @@ -85,10 +81,85 @@ async def test_options_flow( CONF_INVERTER_SIZE: 2000, }, ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2["errors"] == {CONF_API_KEY: "invalid_api_key"} + + +async def test_options_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config flow options.""" + 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.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + # With the API key + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "SolarForecast150", + CONF_DECLINATION: 21, + CONF_AZIMUTH: 22, + CONF_MODULES_POWER: 2122, + CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, + }, + ) + await hass.async_block_till_done() assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("data") == { - CONF_API_KEY: "solarPOWER!", + CONF_API_KEY: "SolarForecast150", + CONF_DECLINATION: 21, + CONF_AZIMUTH: 22, + CONF_MODULES_POWER: 2122, + CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, + } + + +async def test_options_flow_without_key( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test config flow options.""" + 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.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert "flow_id" in result + + # Without the API key + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_DECLINATION: 21, + CONF_AZIMUTH: 22, + CONF_MODULES_POWER: 2122, + CONF_DAMPING: 0.25, + CONF_INVERTER_SIZE: 2000, + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("data") == { + CONF_API_KEY: None, CONF_DECLINATION: 21, CONF_AZIMUTH: 22, CONF_MODULES_POWER: 2122,