Add API key validation for Forecast.Solar (#80856)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
Klaas Schoute 2022-11-25 15:05:01 +01:00 committed by GitHub
parent fcba9974e5
commit be13f3fbcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 126 additions and 28 deletions

View File

@ -1,6 +1,7 @@
"""Config flow for Forecast.Solar integration.""" """Config flow for Forecast.Solar integration."""
from __future__ import annotations from __future__ import annotations
import re
from typing import Any from typing import Any
import voluptuous as vol 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.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult 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 ( from .const import (
CONF_AZIMUTH, CONF_AZIMUTH,
@ -20,6 +21,8 @@ from .const import (
DOMAIN, DOMAIN,
) )
RE_API_KEY = re.compile(r"^[a-zA-Z0-9]{16}$")
class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN): class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Forecast.Solar.""" """Handle a config flow for Forecast.Solar."""
@ -88,8 +91,16 @@ class ForecastSolarOptionFlowHandler(OptionsFlow):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Manage the options.""" """Manage the options."""
errors = {}
if user_input is not None: 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( return self.async_show_form(
step_id="init", step_id="init",
@ -129,4 +140,5 @@ class ForecastSolarOptionFlowHandler(OptionsFlow):
): vol.Coerce(int), ): vol.Coerce(int),
} }
), ),
errors=errors,
) )

View File

@ -15,6 +15,9 @@
} }
}, },
"options": { "options": {
"error": {
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
},
"step": { "step": {
"init": { "init": {
"description": "These values allow tweaking the Forecast.Solar result. Please refer to the documentation if a field is unclear.", "description": "These values allow tweaking the Forecast.Solar result. Please refer to the documentation if a field is unclear.",

View File

@ -15,6 +15,9 @@
} }
}, },
"options": { "options": {
"error": {
"invalid_api_key": "Invalid API key"
},
"step": { "step": {
"init": { "init": {
"data": { "data": {

View File

@ -2,7 +2,7 @@
from collections.abc import Generator from collections.abc import Generator
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from forecast_solar import models from forecast_solar import models
import pytest import pytest
@ -22,6 +22,15 @@ from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry 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 @pytest.fixture
def mock_config_entry() -> MockConfigEntry: def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry.""" """Return the default mocked config entry."""

View File

@ -1,5 +1,5 @@
"""Test the Forecast.Solar config flow.""" """Test the Forecast.Solar config flow."""
from unittest.mock import patch from unittest.mock import AsyncMock
from homeassistant.components.forecast_solar.const import ( from homeassistant.components.forecast_solar.const import (
CONF_AZIMUTH, CONF_AZIMUTH,
@ -17,7 +17,7 @@ from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry 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.""" """Test the full user configuration flow."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} 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 result.get("step_id") == SOURCE_USER
assert "flow_id" in result assert "flow_id" in result
with patch( result2 = await hass.config_entries.flow.async_configure(
"homeassistant.components.forecast_solar.async_setup_entry", return_value=True result["flow_id"],
) as mock_setup_entry: user_input={
result2 = await hass.config_entries.flow.async_configure( CONF_NAME: "Name",
result["flow_id"], CONF_LATITUDE: 52.42,
user_input={ CONF_LONGITUDE: 4.42,
CONF_NAME: "Name", CONF_AZIMUTH: 142,
CONF_LATITUDE: 52.42, CONF_DECLINATION: 42,
CONF_LONGITUDE: 4.42, CONF_MODULES_POWER: 4242,
CONF_AZIMUTH: 142, },
CONF_DECLINATION: 42, )
CONF_MODULES_POWER: 4242,
},
)
assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("type") == FlowResultType.CREATE_ENTRY
assert result2.get("title") == "Name" 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 assert len(mock_setup_entry.mock_calls) == 1
async def test_options_flow( async def test_options_flow_invalid_api(
hass: HomeAssistant, mock_config_entry: MockConfigEntry hass: HomeAssistant,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test config flow options.""" """Test options config flow when API key is invalid."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
with patch( await hass.config_entries.async_setup(mock_config_entry.entry_id)
"homeassistant.components.forecast_solar.async_setup_entry", return_value=True 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) 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, 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("type") == FlowResultType.CREATE_ENTRY
assert result2.get("data") == { 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_DECLINATION: 21,
CONF_AZIMUTH: 22, CONF_AZIMUTH: 22,
CONF_MODULES_POWER: 2122, CONF_MODULES_POWER: 2122,