From a9c9963f0f9d7cf38bdb235f5f1f515aaf0fbde5 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Wed, 10 Jul 2024 20:25:49 +0100 Subject: [PATCH] Mealie min version check (#121677) --- homeassistant/components/mealie/__init__.py | 14 +++++- .../components/mealie/config_flow.py | 20 +++++--- homeassistant/components/mealie/const.py | 4 ++ homeassistant/components/mealie/strings.json | 6 ++- homeassistant/components/mealie/utils.py | 10 ++++ tests/components/mealie/test_config_flow.py | 36 +++++++++++++- tests/components/mealie/test_init.py | 47 ++++++++++++++++++- 7 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/mealie/utils.py diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py index 2d261af37a2..57f75e7d61e 100644 --- a/homeassistant/components/mealie/__init__.py +++ b/homeassistant/components/mealie/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DOMAIN, MIN_REQUIRED_MEALIE_VERSION from .coordinator import ( MealieConfigEntry, MealieData, @@ -20,6 +20,7 @@ from .coordinator import ( MealieShoppingListCoordinator, ) from .services import setup_services +from .utils import create_version PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.TODO] @@ -41,11 +42,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo ) try: about = await client.get_about() + version = create_version(about.version) except MealieAuthenticationError as error: raise ConfigEntryError("Authentication failed") from error except MealieConnectionError as error: raise ConfigEntryNotReady(error) from error + if not version.valid or version < MIN_REQUIRED_MEALIE_VERSION: + raise ConfigEntryError( + translation_domain=DOMAIN, + translation_key="version_error", + translation_placeholders={ + "mealie_version": about.version, + "min_version": MIN_REQUIRED_MEALIE_VERSION, + }, + ) + assert entry.unique_id device_registry = dr.async_get(hass) device_registry.async_get_or_create( diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py index 550e4679720..6da423cdc26 100644 --- a/homeassistant/components/mealie/config_flow.py +++ b/homeassistant/components/mealie/config_flow.py @@ -9,7 +9,8 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_TOKEN, CONF_HOST from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, LOGGER +from .const import DOMAIN, LOGGER, MIN_REQUIRED_MEALIE_VERSION +from .utils import create_version SCHEMA = vol.Schema( { @@ -35,6 +36,8 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): ) try: info = await client.get_user_info() + about = await client.get_about() + version = create_version(about.version) except MealieConnectionError: errors["base"] = "cannot_connect" except MealieAuthenticationError: @@ -43,12 +46,15 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN): LOGGER.exception("Unexpected error") errors["base"] = "unknown" else: - await self.async_set_unique_id(info.user_id) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title="Mealie", - data=user_input, - ) + if not version.valid or version < MIN_REQUIRED_MEALIE_VERSION: + errors["base"] = "mealie_version" + else: + await self.async_set_unique_id(info.user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Mealie", + data=user_input, + ) return self.async_show_form( step_id="user", data_schema=SCHEMA, diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py index 0eb7d98164c..800cfd21db3 100644 --- a/homeassistant/components/mealie/const.py +++ b/homeassistant/components/mealie/const.py @@ -2,6 +2,8 @@ import logging +from awesomeversion import AwesomeVersion + DOMAIN = "mealie" LOGGER = logging.getLogger(__package__) @@ -12,3 +14,5 @@ ATTR_END_DATE = "end_date" ATTR_RECIPE_ID = "recipe_id" ATTR_URL = "url" ATTR_INCLUDE_TAGS = "include_tags" + +MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0") diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 0e54a64b199..2227882fc3a 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -14,7 +14,8 @@ "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "mealie_version": "Minimum required version is v1.0.0. Please upgrade Mealie and then retry." }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" @@ -66,6 +67,9 @@ }, "item_not_found_error": { "message": "Item {shopping_list_item} not found." + }, + "version_error": { + "message": "You are running {mealie_version} of Mealie. Minimum required version is {min_version}. Please upgrade Mealie and then retry." } }, "services": { diff --git a/homeassistant/components/mealie/utils.py b/homeassistant/components/mealie/utils.py new file mode 100644 index 00000000000..36d0831208b --- /dev/null +++ b/homeassistant/components/mealie/utils.py @@ -0,0 +1,10 @@ +"""Mealie util functions.""" + +from __future__ import annotations + +from awesomeversion import AwesomeVersion + + +def create_version(version: str) -> AwesomeVersion: + """Convert beta versions to PEP440.""" + return AwesomeVersion(version.replace("beta-", "b")) diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py index 777bb1e4ad1..c1159a4b51b 100644 --- a/tests/components/mealie/test_config_flow.py +++ b/tests/components/mealie/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from aiomealie import MealieAuthenticationError, MealieConnectionError +from aiomealie import About, MealieAuthenticationError, MealieConnectionError import pytest from homeassistant.components.mealie.const import DOMAIN @@ -83,6 +83,40 @@ async def test_flow_errors( assert result["type"] is FlowResultType.CREATE_ENTRY +@pytest.mark.parametrize( + ("version"), + [ + ("v1.0.0beta-5"), + ("v1.0.0-RC2"), + ("v0.1.0"), + ("something"), + ], +) +async def test_flow_version_error( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, + version, +) -> None: + """Test flow version error.""" + mock_mealie_client.get_about.return_value = About(version=version) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": "mealie_version"} + + async def test_duplicate( hass: HomeAssistant, mock_mealie_client: AsyncMock, diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index bec03ab3719..77041e1cecc 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock -from aiomealie import MealieAuthenticationError, MealieConnectionError +from aiomealie import About, MealieAuthenticationError, MealieConnectionError import pytest from syrupy import SnapshotAssertion @@ -32,6 +32,51 @@ async def test_device_info( assert device_entry == snapshot +@pytest.mark.parametrize( + ("exc", "state"), + [ + (MealieConnectionError, ConfigEntryState.SETUP_RETRY), + (MealieAuthenticationError, ConfigEntryState.SETUP_ERROR), + ], +) +async def test_setup_failure( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exc: Exception, + state: ConfigEntryState, +) -> None: + """Test setup failure.""" + mock_mealie_client.get_about.side_effect = exc + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is state + + +@pytest.mark.parametrize( + ("version"), + [ + ("v1.0.0beta-5"), + ("v1.0.0-RC2"), + ("v0.1.0"), + ("something"), + ], +) +async def test_setup_too_old( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + version, +) -> None: + """Test setup of Mealie entry with too old version of Mealie.""" + mock_mealie_client.get_about.return_value = About(version=version) + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + async def test_load_unload_entry( hass: HomeAssistant, mock_mealie_client: AsyncMock,