diff --git a/CODEOWNERS b/CODEOWNERS index aaed793dd4168e254a78149ab51b600e40d250b8..aa33cdfe38fc13b99353c2e5169a2e1207715514 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -826,6 +826,8 @@ build.json @home-assistant/supervisor /tests/components/matrix/ @PaarthShah /homeassistant/components/matter/ @home-assistant/matter /tests/components/matter/ @home-assistant/matter +/homeassistant/components/mealie/ @joostlek +/tests/components/mealie/ @joostlek /homeassistant/components/meater/ @Sotolotl @emontnemery /tests/components/meater/ @Sotolotl @emontnemery /homeassistant/components/medcom_ble/ @elafargue diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c316cf04545322354267470107f0e1ef0033b05c --- /dev/null +++ b/homeassistant/components/mealie/__init__.py @@ -0,0 +1,33 @@ +"""The Mealie integration.""" + +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import MealieCoordinator + +PLATFORMS: list[Platform] = [Platform.CALENDAR] + + +type MealieConfigEntry = ConfigEntry[MealieCoordinator] + + +async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bool: + """Set up Mealie from a config entry.""" + + coordinator = MealieCoordinator(hass) + + await coordinator.async_config_entry_first_refresh() + + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py new file mode 100644 index 0000000000000000000000000000000000000000..08e90ebf5ea29912f4abe4d92d876fb3f2a27009 --- /dev/null +++ b/homeassistant/components/mealie/calendar.py @@ -0,0 +1,81 @@ +"""Calendar platform for Mealie.""" + +from __future__ import annotations + +from datetime import datetime + +from aiomealie import Mealplan, MealplanEntryType + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import MealieConfigEntry, MealieCoordinator +from .entity import MealieEntity + + +async def async_setup_entry( + hass: HomeAssistant, + entry: MealieConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the calendar platform for entity.""" + coordinator = entry.runtime_data + + async_add_entities( + MealieMealplanCalendarEntity(coordinator, entry_type) + for entry_type in MealplanEntryType + ) + + +def _get_event_from_mealplan(mealplan: Mealplan) -> CalendarEvent: + """Create a CalendarEvent from a Mealplan.""" + description: str | None = None + name = "No recipe" + if mealplan.recipe: + name = mealplan.recipe.name + description = mealplan.recipe.description + return CalendarEvent( + start=mealplan.mealplan_date, + end=mealplan.mealplan_date, + summary=name, + description=description, + ) + + +class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity): + """A calendar entity.""" + + def __init__( + self, coordinator: MealieCoordinator, entry_type: MealplanEntryType + ) -> None: + """Create the Calendar entity.""" + super().__init__(coordinator) + self._entry_type = entry_type + self._attr_translation_key = entry_type.name.lower() + self._attr_unique_id = ( + f"{self.coordinator.config_entry.entry_id}_{entry_type.name.lower()}" + ) + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + mealplans = self.coordinator.data[self._entry_type] + if not mealplans: + return None + return _get_event_from_mealplan(mealplans[0]) + + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: + """Get all events in a specific time frame.""" + mealplans = ( + await self.coordinator.client.get_mealplans( + start_date.date(), end_date.date() + ) + ).items + return [ + _get_event_from_mealplan(mealplan) + for mealplan in mealplans + if mealplan.entry_type is self._entry_type + ] diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..b25cade148aff13eb8c9fb9ba15b0d11ed7bdec0 --- /dev/null +++ b/homeassistant/components/mealie/config_flow.py @@ -0,0 +1,55 @@ +"""Config flow for Mealie.""" + +from typing import Any + +from aiomealie import MealieAuthenticationError, MealieClient, MealieConnectionError +import voluptuous as vol + +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 + +SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_API_TOKEN): str, + } +) + + +class MealieConfigFlow(ConfigFlow, domain=DOMAIN): + """Mealie config flow.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] = {} + if user_input: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + client = MealieClient( + user_input[CONF_HOST], + token=user_input[CONF_API_TOKEN], + session=async_get_clientsession(self.hass), + ) + try: + await client.get_mealplan_today() + except MealieConnectionError: + errors["base"] = "cannot_connect" + except MealieAuthenticationError: + errors["base"] = "invalid_auth" + except Exception: # noqa: BLE001 + LOGGER.exception("Unexpected error") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title="Mealie", + data=user_input, + ) + return self.async_show_form( + step_id="user", + data_schema=SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py new file mode 100644 index 0000000000000000000000000000000000000000..28c64d3c0f05635c823c686fe83139b4486ff8fc --- /dev/null +++ b/homeassistant/components/mealie/const.py @@ -0,0 +1,7 @@ +"""Constants for the Mealie integration.""" + +import logging + +DOMAIN = "mealie" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/mealie/coordinator.py b/homeassistant/components/mealie/coordinator.py new file mode 100644 index 0000000000000000000000000000000000000000..0c32544d4d7f8de91081590005213719f1494ff4 --- /dev/null +++ b/homeassistant/components/mealie/coordinator.py @@ -0,0 +1,65 @@ +"""Define an object to manage fetching Mealie data.""" + +from __future__ import annotations + +from datetime import timedelta +from typing import TYPE_CHECKING + +from aiomealie import ( + MealieAuthenticationError, + MealieClient, + MealieConnectionError, + Mealplan, + MealplanEntryType, +) + +from homeassistant.const import CONF_API_TOKEN, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +import homeassistant.util.dt as dt_util + +from .const import LOGGER + +if TYPE_CHECKING: + from . import MealieConfigEntry + +WEEK = timedelta(days=7) + + +class MealieCoordinator(DataUpdateCoordinator[dict[MealplanEntryType, list[Mealplan]]]): + """Class to manage fetching Mealie data.""" + + config_entry: MealieConfigEntry + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize coordinator.""" + super().__init__( + hass, logger=LOGGER, name="Mealie", update_interval=timedelta(hours=1) + ) + self.client = MealieClient( + self.config_entry.data[CONF_HOST], + token=self.config_entry.data[CONF_API_TOKEN], + session=async_get_clientsession(hass), + ) + + async def _async_update_data(self) -> dict[MealplanEntryType, list[Mealplan]]: + next_week = dt_util.now() + WEEK + try: + data = ( + await self.client.get_mealplans(dt_util.now().date(), next_week.date()) + ).items + except MealieAuthenticationError as error: + raise ConfigEntryError("Authentication failed") from error + except MealieConnectionError as error: + raise UpdateFailed(error) from error + res: dict[MealplanEntryType, list[Mealplan]] = { + MealplanEntryType.BREAKFAST: [], + MealplanEntryType.LUNCH: [], + MealplanEntryType.DINNER: [], + MealplanEntryType.SIDE: [], + } + for meal in data: + res[meal.entry_type].append(meal) + return res diff --git a/homeassistant/components/mealie/entity.py b/homeassistant/components/mealie/entity.py new file mode 100644 index 0000000000000000000000000000000000000000..5e339c1d4b851344945dc9b1073c59aca25f508a --- /dev/null +++ b/homeassistant/components/mealie/entity.py @@ -0,0 +1,21 @@ +"""Base class for Mealie entities.""" + +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import MealieCoordinator + + +class MealieEntity(CoordinatorEntity[MealieCoordinator]): + """Defines a base Mealie entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: MealieCoordinator) -> None: + """Initialize Mealie entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..3a2a9b582049bc011d1df561f0040d7e7b1f7d5f --- /dev/null +++ b/homeassistant/components/mealie/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "mealie", + "name": "Mealie", + "codeowners": ["@joostlek"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/mealie", + "integration_type": "service", + "iot_class": "local_polling", + "requirements": ["aiomealie==0.3.1"] +} diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json new file mode 100644 index 0000000000000000000000000000000000000000..0d67bb8975914273d0cda6632d6592adf652536d --- /dev/null +++ b/homeassistant/components/mealie/strings.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "api_token": "[%key:common::config_flow::data::api_token%]" + } + } + }, + "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%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + }, + "entity": { + "calendar": { + "breakfast": { + "name": "Breakfast" + }, + "dinner": { + "name": "Dinner" + }, + "lunch": { + "name": "Lunch" + }, + "side": { + "name": "Side" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 5d0718092e547c2319c9e5c8dcbba4173554047a..7cd0e2707033eb9b7141a50e708cef25e999d5e0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -319,6 +319,7 @@ FLOWS = { "lyric", "mailgun", "matter", + "mealie", "meater", "medcom_ble", "media_extractor", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index fb3e33d3289f0c08abd11cebfbcc971590aa06f6..0fe63cc02ff420fd41f1079bf59c16a39c6ffa13 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3505,6 +3505,12 @@ "config_flow": true, "iot_class": "local_push" }, + "mealie": { + "name": "Mealie", + "integration_type": "service", + "config_flow": true, + "iot_class": "local_polling" + }, "meater": { "name": "Meater", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 04dec29d79886282064dce77048ae30604e292ed..f949d864f5434d4b7e041d2b754931ffb0a70913 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -293,6 +293,9 @@ aiolookin==1.0.0 # homeassistant.components.lyric aiolyric==1.1.0 +# homeassistant.components.mealie +aiomealie==0.3.1 + # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91d0fa2ff2d81f41ae4f98fd36787adf7576cbb0..face667ccc5231659e5a7f50503d45387f635c95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,6 +266,9 @@ aiolookin==1.0.0 # homeassistant.components.lyric aiolyric==1.1.0 +# homeassistant.components.mealie +aiomealie==0.3.1 + # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/tests/components/mealie/__init__.py b/tests/components/mealie/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3e85e241c6f18c98b14c1eb037e85a36ac6eda56 --- /dev/null +++ b/tests/components/mealie/__init__.py @@ -0,0 +1,13 @@ +"""Tests for the Mealie integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..dd6309cb5249e88d6756f889cd78fe18662707b8 --- /dev/null +++ b/tests/components/mealie/conftest.py @@ -0,0 +1,58 @@ +"""Mealie tests configuration.""" + +from unittest.mock import patch + +from aiomealie import Mealplan, MealplanResponse +from mashumaro.codecs.orjson import ORJSONDecoder +import pytest +from typing_extensions import Generator + +from homeassistant.components.mealie.const import DOMAIN +from homeassistant.const import CONF_API_TOKEN, CONF_HOST + +from tests.common import MockConfigEntry, load_fixture +from tests.components.smhi.common import AsyncMock + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.mealie.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_mealie_client() -> Generator[AsyncMock]: + """Mock a Mealie client.""" + with ( + patch( + "homeassistant.components.mealie.coordinator.MealieClient", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.mealie.config_flow.MealieClient", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_mealplans.return_value = MealplanResponse.from_json( + load_fixture("get_mealplans.json", DOMAIN) + ) + client.get_mealplan_today.return_value = ORJSONDecoder(list[Mealplan]).decode( + load_fixture("get_mealplan_today.json", DOMAIN) + ) + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Mealie", + data={CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, + entry_id="01J0BC4QM2YBRP6H5G933CETT7", + ) diff --git a/tests/components/mealie/fixtures/get_mealplan_today.json b/tests/components/mealie/fixtures/get_mealplan_today.json new file mode 100644 index 0000000000000000000000000000000000000000..1413f4a01133bc08aade57c5c556e9a90c0c88bf --- /dev/null +++ b/tests/components/mealie/fixtures/get_mealplan_today.json @@ -0,0 +1,253 @@ +[ + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "40393996-417e-4487-a081-28608a668826", + "id": 192, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "40393996-417e-4487-a081-28608a668826", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Cauliflower Salad", + "slug": "cauliflower-salad", + "image": "qLdv", + "recipeYield": "6 servings", + "totalTime": "2 Hours 35 Minutes", + "prepTime": "25 Minutes", + "cookTime": null, + "performTime": "10 Minutes", + "description": "This is a wonderful option for picnics and grill outs when you are looking for a new take on potato salad. This simple side salad made with cauliflower, peas, and hard boiled eggs can be made the day ahead and chilled until party time!", + "recipeCategory": [], + "tags": [], + "tools": [ + { + "id": "6e199f62-8356-46cf-8f6f-ea923780a1e3", + "name": "Stove", + "slug": "stove", + "onHand": false + } + ], + "rating": null, + "orgURL": "https://www.allrecipes.com/recipe/142152/cauliflower-salad/", + "dateAdded": "2023-12-29", + "dateUpdated": "2024-01-06T13:38:55.116185", + "createdAt": "2023-12-29T00:46:50.138612", + "updateAt": "2024-01-06T13:38:55.119029", + "lastMade": "2024-01-06T22:59:59" + } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "872bb477-8d90-4025-98b0-07a9d0d9ce3a", + "id": 206, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "872bb477-8d90-4025-98b0-07a9d0d9ce3a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "15 Minute Cheesy Sausage & Veg Pasta", + "slug": "15-minute-cheesy-sausage-veg-pasta", + "image": "BeNc", + "recipeYield": "", + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Easy, cheesy, sausage pasta! In the whirlwind of mid-week mayhem, dinner doesn’t have to be a chore – this 15-minute pasta, featuring HECK’s Chicken Italia Chipolatas is your ticket to a delicious and hassle-free mid-week meal.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.annabelkarmel.com/recipes/15-minute-cheesy-sausage-veg-pasta/", + "dateAdded": "2024-01-01", + "dateUpdated": "2024-01-01T20:40:40.441381", + "createdAt": "2024-01-01T20:40:40.443048", + "updateAt": "2024-01-01T20:40:40.443050", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "744a9831-fa56-4f61-9e12-fc5ebce58ed9", + "id": 207, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "744a9831-fa56-4f61-9e12-fc5ebce58ed9", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "cake", + "slug": "cake", + "image": null, + "recipeYield": null, + "totalTime": null, + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": null, + "dateAdded": "2024-01-01", + "dateUpdated": "2024-01-01T14:39:11.214806", + "createdAt": "2024-01-01T14:39:11.216709", + "updateAt": "2024-01-01T14:39:11.216711", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "27455eb2-31d3-4682-84ff-02a114bf293a", + "id": 208, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "27455eb2-31d3-4682-84ff-02a114bf293a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Pomegranate chicken with almond couscous", + "slug": "pomegranate-chicken-with-almond-couscous", + "image": "lF4p", + "recipeYield": "4 servings", + "totalTime": "20 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Jazz up chicken breasts in this fruity, sweetly spiced sauce with pomegranate seeds, toasted almonds and tagine paste", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.bbcgoodfood.com/recipes/pomegranate-chicken-almond-couscous", + "dateAdded": "2023-12-29", + "dateUpdated": "2023-12-29T08:29:03.178355", + "createdAt": "2023-12-29T08:29:03.180819", + "updateAt": "2023-12-29T08:29:03.180820", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "4233330e-6947-4042-90b7-44c405b70714", + "id": 209, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "4233330e-6947-4042-90b7-44c405b70714", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Csirkés és tofus empanadas", + "slug": "csirkes-es-tofus-empanadas", + "image": "ALqz", + "recipeYield": "16 servings", + "totalTime": "95", + "prepTime": "40", + "cookTime": null, + "performTime": "15", + "description": "", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://streetkitchen.hu/street-kitchen/csirkes-es-tofus-empanadas/", + "dateAdded": "2023-12-29", + "dateUpdated": "2023-12-29T07:56:20.087496", + "createdAt": "2023-12-29T07:53:47.765573", + "updateAt": "2023-12-29T07:56:20.090890", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "id": 210, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "All-American Beef Stew Recipe", + "slug": "all-american-beef-stew-recipe", + "image": "356X", + "recipeYield": "6 servings", + "totalTime": "3 Hours 15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "3 Hours 10 Minutes", + "description": "This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.seriouseats.com/all-american-beef-stew-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T03:04:45.606075", + "createdAt": "2024-01-20T20:41:29.266390", + "updateAt": "2024-01-21T03:04:45.609563", + "lastMade": null + } + }, + { + "date": "2024-01-21", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "27455eb2-31d3-4682-84ff-02a114bf293a", + "id": 223, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "27455eb2-31d3-4682-84ff-02a114bf293a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Pomegranate chicken with almond couscous", + "slug": "pomegranate-chicken-with-almond-couscous", + "image": "lF4p", + "recipeYield": "4 servings", + "totalTime": "20 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Jazz up chicken breasts in this fruity, sweetly spiced sauce with pomegranate seeds, toasted almonds and tagine paste", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.bbcgoodfood.com/recipes/pomegranate-chicken-almond-couscous", + "dateAdded": "2023-12-29", + "dateUpdated": "2023-12-29T08:29:03.178355", + "createdAt": "2023-12-29T08:29:03.180819", + "updateAt": "2023-12-29T08:29:03.180820", + "lastMade": null + } + } +] diff --git a/tests/components/mealie/fixtures/get_mealplans.json b/tests/components/mealie/fixtures/get_mealplans.json new file mode 100644 index 0000000000000000000000000000000000000000..2d63b753d998a8407bf353f87e66e35bb4f2a495 --- /dev/null +++ b/tests/components/mealie/fixtures/get_mealplans.json @@ -0,0 +1,612 @@ +{ + "page": 1, + "per_page": 50, + "total": 14, + "total_pages": 1, + "items": [ + { + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "c5f00a93-71a2-4e48-900f-d9ad0bb9de93", + "id": 230, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "c5f00a93-71a2-4e48-900f-d9ad0bb9de93", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Zoete aardappel curry traybake", + "slug": "zoete-aardappel-curry-traybake", + "image": "AiIo", + "recipeYield": "2 servings", + "totalTime": "40 Minutes", + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/", + "dateAdded": "2024-01-22", + "dateUpdated": "2024-01-22T00:27:46.324512", + "createdAt": "2024-01-22T00:27:46.327546", + "updateAt": "2024-01-22T00:27:46.327548", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "breakfast", + "title": "", + "text": "", + "recipeId": "5b055066-d57d-4fd0-8dfd-a2c2f07b36f1", + "id": 229, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "5b055066-d57d-4fd0-8dfd-a2c2f07b36f1", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Roast Chicken", + "slug": "roast-chicken", + "image": "JeQ2", + "recipeYield": "6 servings", + "totalTime": "1 Hour 35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "1 Hour 20 Minutes", + "description": "The BEST Roast Chicken recipe is simple, budget friendly, and gives you a tender, mouth-watering chicken full of flavor! Served with roasted vegetables, this recipe is simple enough for any cook!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://tastesbetterfromscratch.com/roast-chicken/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T15:29:25.664540", + "createdAt": "2024-01-21T15:29:25.667450", + "updateAt": "2024-01-21T15:29:25.667452", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "e360a0cc-18b0-4a84-a91b-8aa59e2451c9", + "id": 226, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "e360a0cc-18b0-4a84-a91b-8aa59e2451c9", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Receta de pollo al curry en 10 minutos (con vÃdeo incluido)", + "slug": "receta-de-pollo-al-curry-en-10-minutos-con-video-incluido", + "image": "INQz", + "recipeYield": "2 servings", + "totalTime": "10 Minutes", + "prepTime": "3 Minutes", + "cookTime": null, + "performTime": "7 Minutes", + "description": "Te explicamos paso a paso, de manera sencilla, la elaboración de la receta de pollo al curry con leche de coco en 10 minutos. Ingredientes, tiempo de...", + "recipeCategory": [], + "tags": [], + "tools": [ + { + "id": "1170e609-20d3-45b8-b0c7-3a4cfa614e88", + "name": "Backofen", + "slug": "backofen", + "onHand": false + }, + { + "id": "9ab522ad-c3be-4dad-8b4f-d9d53817f4d0", + "name": "Magimix blender", + "slug": "magimix-blender", + "onHand": false + }, + { + "id": "b4ca27dc-9bf6-48be-ad10-3e7056cb24bc", + "name": "Alluminio", + "slug": "alluminio", + "onHand": false + } + ], + "rating": null, + "orgURL": "https://www.directoalpaladar.com/recetas-de-carnes-y-aves/receta-de-pollo-al-curry-en-10-minutos", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T12:56:31.483701", + "createdAt": "2024-01-21T12:45:28.589669", + "updateAt": "2024-01-21T12:56:31.487406", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "9c7b8aee-c93c-4b1b-ab48-2625d444743a", + "id": 224, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "9c7b8aee-c93c-4b1b-ab48-2625d444743a", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Boeuf bourguignon : la vraie recette (2)", + "slug": "boeuf-bourguignon-la-vraie-recette-2", + "image": "nj5M", + "recipeYield": "4 servings", + "totalTime": "5 Hours", + "prepTime": "1 Hour", + "cookTime": null, + "performTime": "4 Hours", + "description": "bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre", + "recipeCategory": [], + "tags": [ + { + "id": "01c2f4ac-54ce-49bc-9bd7-8a49f353a3a4", + "name": "Poivre", + "slug": "poivre" + }, + { + "id": "90a26cea-a8a1-41a1-9e8c-e94e3c40f7a7", + "name": "Sel", + "slug": "sel" + }, + { + "id": "d7b01a4b-5206-4bd2-b9c4-d13b95ca0edb", + "name": "Beurre", + "slug": "beurre" + }, + { + "id": "304faaf8-13ec-4537-91f3-9f39a3585545", + "name": "Facile", + "slug": "facile" + }, + { + "id": "6508fb05-fb60-4bed-90c4-584bd6d74cb5", + "name": "Daube", + "slug": "daube" + }, + { + "id": "18ff59b6-b599-456a-896b-4b76448b08ca", + "name": "Bourguignon", + "slug": "bourguignon" + }, + { + "id": "685a0d90-8de4-494e-8eb8-68e7f5d5ffbe", + "name": "Vin Rouge", + "slug": "vin-rouge" + }, + { + "id": "5dedc8b5-30f5-4d6e-875f-34deefd01883", + "name": "Oignon", + "slug": "oignon" + }, + { + "id": "065b79e0-6276-4ebb-9428-7018b40c55bb", + "name": "Bouquet Garni", + "slug": "bouquet-garni" + }, + { + "id": "d858b1d9-2ca1-46d4-acc2-3d03f991f03f", + "name": "Moyen", + "slug": "moyen" + }, + { + "id": "bded0bd8-8d41-4ec5-ad73-e0107fb60908", + "name": "Boeuf Bourguignon : La Vraie Recette", + "slug": "boeuf-bourguignon-la-vraie-recette" + }, + { + "id": "7f99b04f-914a-408b-a057-511ca1125734", + "name": "Carotte", + "slug": "carotte" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.marmiton.org/recettes/recette_boeuf-bourguignon_18889.aspx", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T08:45:28.780361", + "createdAt": "2024-01-21T08:45:28.782322", + "updateAt": "2024-01-21T08:45:28.782324", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "f79f7e9d-4b58-4930-a586-2b127f16ee34", + "id": 222, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "f79f7e9d-4b58-4930-a586-2b127f16ee34", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "ΕÏκολη μακαÏονάδα με κεφτεδάκια στον φοÏÏνο (1)", + "slug": "eukole-makaronada-me-kephtedakia-ston-phourno-1", + "image": "En9o", + "recipeYield": "6 servings", + "totalTime": null, + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "50 Minutes", + "description": "ΕÏκολη μακαÏονάδα με κεφτεδάκια στον φοÏÏνο από τον Άκη ΠετÏετζίκη. Φτιάξτε την πιο εÏκολη μακαÏονάδα με κεφτεδάκια σε μόνο Îνα σκεÏος.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://akispetretzikis.com/recipe/7959/efkolh-makaronada-me-keftedakia-ston-fourno", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T09:08:58.056854", + "createdAt": "2024-01-21T09:08:58.059401", + "updateAt": "2024-01-21T09:08:58.059403", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "47595e4c-52bc-441d-b273-3edf4258806d", + "id": 221, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "47595e4c-52bc-441d-b273-3edf4258806d", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Greek Turkey Meatballs with Lemon Orzo & Creamy Feta Yogurt Sauce", + "slug": "greek-turkey-meatballs-with-lemon-orzo-creamy-feta-yogurt-sauce", + "image": "Kn62", + "recipeYield": "4 servings", + "totalTime": "1 Hour", + "prepTime": "40 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.ambitiouskitchen.com/greek-turkey-meatballs/", + "dateAdded": "2024-01-04", + "dateUpdated": "2024-01-04T11:51:00.843570", + "createdAt": "2024-01-04T11:51:00.847033", + "updateAt": "2024-01-04T11:51:00.847035", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "side", + "title": "", + "text": "", + "recipeId": "9d553779-607e-471b-acf3-84e6be27b159", + "id": 220, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "9d553779-607e-471b-acf3-84e6be27b159", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Einfacher Nudelauflauf mit Brokkoli", + "slug": "einfacher-nudelauflauf-mit-brokkoli", + "image": "nOPT", + "recipeYield": "4 servings", + "totalTime": "35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T03:04:25.718367", + "createdAt": "2024-01-21T02:13:11.323363", + "updateAt": "2024-01-21T03:04:25.721489", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "92635fd0-f2dc-4e78-a6e4-ecd556ad361f", + "id": 219, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "92635fd0-f2dc-4e78-a6e4-ecd556ad361f", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Pampered Chef Double Chocolate Mocha Trifle", + "slug": "pampered-chef-double-chocolate-mocha-trifle", + "image": "ibL6", + "recipeYield": "12 servings", + "totalTime": "1 Hour 15 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "1 Hour", + "description": "This is a modified Pampered Chef recipe. You can use a trifle bowl or large glass punch/salad bowl to show it off. It is really easy to make and I never have any leftovers. Cook time includes chill time.", + "recipeCategory": [], + "tags": [ + { + "id": "0248c21d-c85a-47b2-aaf6-fb6caf1b7726", + "name": "Weeknight", + "slug": "weeknight" + }, + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": 3, + "orgURL": "https://www.food.com/recipe/pampered-chef-double-chocolate-mocha-trifle-74963", + "dateAdded": "2024-01-06", + "dateUpdated": "2024-01-06T08:11:21.427447", + "createdAt": "2024-01-06T06:29:24.966994", + "updateAt": "2024-01-06T08:11:21.430079", + "lastMade": null + } + }, + { + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "8bdd3656-5e7e-45d3-a3c4-557390846a22", + "id": 217, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "8bdd3656-5e7e-45d3-a3c4-557390846a22", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Cheeseburger Sliders (Easy, 30-min Recipe)", + "slug": "cheeseburger-sliders-easy-30-min-recipe", + "image": "beGq", + "recipeYield": "24 servings", + "totalTime": "30 Minutes", + "prepTime": "8 Minutes", + "cookTime": null, + "performTime": "22 Minutes", + "description": "Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.", + "recipeCategory": [], + "tags": [ + { + "id": "7a4ca427-642f-4428-8dc7-557ea9c8d1b4", + "name": "Cheeseburger Sliders", + "slug": "cheeseburger-sliders" + }, + { + "id": "941558d2-50d5-4c9d-8890-a0258f18d493", + "name": "Sliders", + "slug": "sliders" + } + ], + "tools": [], + "rating": 5, + "orgURL": "https://natashaskitchen.com/cheeseburger-sliders/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T07:43:24.261010", + "createdAt": "2024-01-21T06:49:35.466777", + "updateAt": "2024-01-21T06:49:35.466778", + "lastMade": "2024-01-22T04:59:59" + } + }, + { + "date": "2024-01-22", + "entryType": "lunch", + "title": "", + "text": "", + "recipeId": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "id": 216, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "All-American Beef Stew Recipe", + "slug": "all-american-beef-stew-recipe", + "image": "356X", + "recipeYield": "6 servings", + "totalTime": "3 Hours 15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "3 Hours 10 Minutes", + "description": "This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.seriouseats.com/all-american-beef-stew-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T03:04:45.606075", + "createdAt": "2024-01-20T20:41:29.266390", + "updateAt": "2024-01-21T03:04:45.609563", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "id": 212, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "All-American Beef Stew Recipe", + "slug": "all-american-beef-stew-recipe", + "image": "356X", + "recipeYield": "6 servings", + "totalTime": "3 Hours 15 Minutes", + "prepTime": "5 Minutes", + "cookTime": null, + "performTime": "3 Hours 10 Minutes", + "description": "This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://www.seriouseats.com/all-american-beef-stew-recipe", + "dateAdded": "2024-01-20", + "dateUpdated": "2024-01-21T03:04:45.606075", + "createdAt": "2024-01-20T20:41:29.266390", + "updateAt": "2024-01-21T03:04:45.609563", + "lastMade": null + } + }, + { + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "9d553779-607e-471b-acf3-84e6be27b159", + "id": 211, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "9d553779-607e-471b-acf3-84e6be27b159", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Einfacher Nudelauflauf mit Brokkoli", + "slug": "einfacher-nudelauflauf-mit-brokkoli", + "image": "nOPT", + "recipeYield": "4 servings", + "totalTime": "35 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "20 Minutes", + "description": "Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!", + "recipeCategory": [], + "tags": [ + { + "id": "78318c97-75c7-4d06-95b6-51ef8f4a0257", + "name": "< 4 Hours", + "slug": "4-hours" + } + ], + "tools": [], + "rating": null, + "orgURL": "https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/", + "dateAdded": "2024-01-21", + "dateUpdated": "2024-01-21T03:04:25.718367", + "createdAt": "2024-01-21T02:13:11.323363", + "updateAt": "2024-01-21T03:04:25.721489", + "lastMade": null + } + }, + { + "date": "2024-01-23", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "25b814f2-d9bf-4df0-b40d-d2f2457b4317", + "id": 196, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "25b814f2-d9bf-4df0-b40d-d2f2457b4317", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Miso Udon Noodles with Spinach and Tofu", + "slug": "miso-udon-noodles-with-spinach-and-tofu", + "image": "5G1v", + "recipeYield": "2 servings", + "totalTime": "25 Minutes", + "prepTime": "10 Minutes", + "cookTime": null, + "performTime": "15 Minutes", + "description": "Simple to prepare and ready in 25 minutes, this vegetarian miso noodle recipe can be eaten on its own or served as a side.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.allrecipes.com/recipe/284039/miso-udon-noodles-with-spinach-and-tofu/", + "dateAdded": "2024-01-05", + "dateUpdated": "2024-01-05T16:35:00.264511", + "createdAt": "2024-01-05T16:00:45.090493", + "updateAt": "2024-01-05T16:35:00.267508", + "lastMade": null + } + }, + { + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "55c88810-4cf1-4d86-ae50-63b15fd173fb", + "id": 195, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "55c88810-4cf1-4d86-ae50-63b15fd173fb", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Mousse de saumon", + "slug": "mousse-de-saumon", + "image": "rrNL", + "recipeYield": "12 servings", + "totalTime": "17 Minutes", + "prepTime": "15 Minutes", + "cookTime": null, + "performTime": "2 Minutes", + "description": "Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://www.ricardocuisine.com/recettes/8919-mousse-de-saumon", + "dateAdded": "2024-01-02", + "dateUpdated": "2024-01-02T06:35:05.206948", + "createdAt": "2024-01-02T06:33:15.329794", + "updateAt": "2024-01-02T06:35:05.209189", + "lastMade": "2024-01-02T22:59:59" + } + } + ], + "next": null, + "previous": null +} diff --git a/tests/components/mealie/snapshots/test_calendar.ambr b/tests/components/mealie/snapshots/test_calendar.ambr new file mode 100644 index 0000000000000000000000000000000000000000..6af53c112ded3675756524432f2391646faa0051 --- /dev/null +++ b/tests/components/mealie/snapshots/test_calendar.ambr @@ -0,0 +1,359 @@ +# serializer version: 1 +# name: test_api_calendar + list([ + dict({ + 'entity_id': 'calendar.mealie_breakfast', + 'name': 'Mealie Breakfast', + }), + dict({ + 'entity_id': 'calendar.mealie_dinner', + 'name': 'Mealie Dinner', + }), + dict({ + 'entity_id': 'calendar.mealie_lunch', + 'name': 'Mealie Lunch', + }), + dict({ + 'entity_id': 'calendar.mealie_side', + 'name': 'Mealie Side', + }), + ]) +# --- +# name: test_api_events + list([ + dict({ + 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + 'end': dict({ + 'date': '2024-01-23', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-22', + }), + 'summary': 'Zoete aardappel curry traybake', + 'uid': None, + }), + dict({ + 'description': 'ΕÏκολη μακαÏονάδα με κεφτεδάκια στον φοÏÏνο από τον Άκη ΠετÏετζίκη. Φτιάξτε την πιο εÏκολη μακαÏονάδα με κεφτεδάκια σε μόνο Îνα σκεÏος.', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'ΕÏκολη μακαÏονάδα με κεφτεδάκια στον φοÏÏνο (1)', + 'uid': None, + }), + dict({ + 'description': 'Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'Greek Turkey Meatballs with Lemon Orzo & Creamy Feta Yogurt Sauce', + 'uid': None, + }), + dict({ + 'description': 'This is a modified Pampered Chef recipe. You can use a trifle bowl or large glass punch/salad bowl to show it off. It is really easy to make and I never have any leftovers. Cook time includes chill time.', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'Pampered Chef Double Chocolate Mocha Trifle', + 'uid': None, + }), + dict({ + 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', + 'end': dict({ + 'date': '2024-01-23', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-22', + }), + 'summary': 'Cheeseburger Sliders (Easy, 30-min Recipe)', + 'uid': None, + }), + dict({ + 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'All-American Beef Stew Recipe', + 'uid': None, + }), + dict({ + 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', + 'end': dict({ + 'date': '2024-01-23', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-22', + }), + 'summary': 'Einfacher Nudelauflauf mit Brokkoli', + 'uid': None, + }), + dict({ + 'description': 'Simple to prepare and ready in 25 minutes, this vegetarian miso noodle recipe can be eaten on its own or served as a side.', + 'end': dict({ + 'date': '2024-01-24', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-23', + }), + 'summary': 'Miso Udon Noodles with Spinach and Tofu', + 'uid': None, + }), + dict({ + 'description': 'Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !', + 'end': dict({ + 'date': '2024-01-23', + }), + 'location': None, + 'recurrence_id': None, + 'rrule': None, + 'start': dict({ + 'date': '2024-01-22', + }), + 'summary': 'Mousse de saumon', + 'uid': None, + }), + ]) +# --- +# name: test_entities[calendar.mealie_breakfast-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.mealie_breakfast', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Breakfast', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'breakfast', + 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_breakfast', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[calendar.mealie_breakfast-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': True, + 'description': 'The BEST Roast Chicken recipe is simple, budget friendly, and gives you a tender, mouth-watering chicken full of flavor! Served with roasted vegetables, this recipe is simple enough for any cook!', + 'end_time': '2024-01-24 00:00:00', + 'friendly_name': 'Mealie Breakfast', + 'location': '', + 'message': 'Roast Chicken', + 'start_time': '2024-01-23 00:00:00', + }), + 'context': <ANY>, + 'entity_id': 'calendar.mealie_breakfast', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'off', + }) +# --- +# name: test_entities[calendar.mealie_dinner-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.mealie_dinner', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Dinner', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'dinner', + 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_dinner', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[calendar.mealie_dinner-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': True, + 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + 'end_time': '2024-01-23 00:00:00', + 'friendly_name': 'Mealie Dinner', + 'location': '', + 'message': 'Zoete aardappel curry traybake', + 'start_time': '2024-01-22 00:00:00', + }), + 'context': <ANY>, + 'entity_id': 'calendar.mealie_dinner', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'off', + }) +# --- +# name: test_entities[calendar.mealie_lunch-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.mealie_lunch', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lunch', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lunch', + 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_lunch', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[calendar.mealie_lunch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': True, + 'description': 'Te explicamos paso a paso, de manera sencilla, la elaboración de la receta de pollo al curry con leche de coco en 10 minutos. Ingredientes, tiempo de...', + 'end_time': '2024-01-24 00:00:00', + 'friendly_name': 'Mealie Lunch', + 'location': '', + 'message': 'Receta de pollo al curry en 10 minutos (con vÃdeo incluido)', + 'start_time': '2024-01-23 00:00:00', + }), + 'context': <ANY>, + 'entity_id': 'calendar.mealie_lunch', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'off', + }) +# --- +# name: test_entities[calendar.mealie_side-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': <ANY>, + 'device_class': None, + 'device_id': <ANY>, + 'disabled_by': None, + 'domain': 'calendar', + 'entity_category': None, + 'entity_id': 'calendar.mealie_side', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': <ANY>, + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Side', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'side', + 'unique_id': '01J0BC4QM2YBRP6H5G933CETT7_side', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[calendar.mealie_side-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'all_day': True, + 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', + 'end_time': '2024-01-24 00:00:00', + 'friendly_name': 'Mealie Side', + 'location': '', + 'message': 'Einfacher Nudelauflauf mit Brokkoli', + 'start_time': '2024-01-23 00:00:00', + }), + 'context': <ANY>, + 'entity_id': 'calendar.mealie_side', + 'last_changed': <ANY>, + 'last_reported': <ANY>, + 'last_updated': <ANY>, + 'state': 'off', + }) +# --- diff --git a/tests/components/mealie/snapshots/test_init.ambr b/tests/components/mealie/snapshots/test_init.ambr new file mode 100644 index 0000000000000000000000000000000000000000..c2752d938e491d676651504ca62efd0a3339e98d --- /dev/null +++ b/tests/components/mealie/snapshots/test_init.ambr @@ -0,0 +1,31 @@ +# serializer version: 1 +# name: test_device_info + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': <ANY>, + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': <DeviceEntryType.SERVICE: 'service'>, + 'hw_version': None, + 'id': <ANY>, + 'identifiers': set({ + tuple( + 'mealie', + '01J0BC4QM2YBRP6H5G933CETT7', + ), + }), + 'is_new': False, + 'labels': set({ + }), + 'manufacturer': None, + 'model': None, + 'name': 'Mealie', + 'name_by_user': None, + 'serial_number': None, + 'suggested_area': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- diff --git a/tests/components/mealie/test_calendar.py b/tests/components/mealie/test_calendar.py new file mode 100644 index 0000000000000000000000000000000000000000..9df2c1810fd72e7f85bf824760b1d5ae0a3bf924 --- /dev/null +++ b/tests/components/mealie/test_calendar.py @@ -0,0 +1,69 @@ +"""Tests for the Mealie calendar.""" + +from datetime import date +from http import HTTPStatus +from unittest.mock import AsyncMock + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform +from tests.typing import ClientSessionGenerator + + +async def test_api_calendar( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test the API returns the calendar.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_client() + response = await client.get("/api/calendars") + assert response.status == HTTPStatus.OK + data = await response.json() + assert data == snapshot + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the API returns the calendar.""" + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_api_events( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client: ClientSessionGenerator, +) -> None: + """Test the Mealie calendar view.""" + await setup_integration(hass, mock_config_entry) + + client = await hass_client() + response = await client.get( + "/api/calendars/calendar.mealie_dinner?start=2023-08-01&end=2023-11-01" + ) + assert mock_mealie_client.get_mealplans.called == 1 + assert mock_mealie_client.get_mealplans.call_args_list[1].args == ( + date(2023, 8, 1), + date(2023, 11, 1), + ) + assert response.status == HTTPStatus.OK + events = await response.json() + assert events == snapshot diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..ac68ed2fac5abff122598ecccbc2706f517ac311 --- /dev/null +++ b/tests/components/mealie/test_config_flow.py @@ -0,0 +1,107 @@ +"""Tests for the Mealie config flow.""" + +from unittest.mock import AsyncMock + +from aiomealie import MealieAuthenticationError, MealieConnectionError +import pytest + +from homeassistant.components.mealie.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_TOKEN, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test full flow.""" + 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.CREATE_ENTRY + assert result["title"] == "Mealie" + assert result["data"] == { + CONF_HOST: "demo.mealie.io", + CONF_API_TOKEN: "token", + } + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (MealieConnectionError, "cannot_connect"), + (MealieAuthenticationError, "invalid_auth"), + (Exception, "unknown"), + ], +) +async def test_flow_errors( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test flow errors.""" + mock_mealie_client.get_mealplan_today.side_effect = exception + + 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": error} + + mock_mealie_client.get_mealplan_today.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "demo.mealie.io", CONF_API_TOKEN: "token"}, + ) + await hass.async_block_till_done() + assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_duplicate( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test duplicate flow.""" + mock_config_entry.add_to_hass(hass) + + 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.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py new file mode 100644 index 0000000000000000000000000000000000000000..7d63ad135f9d1f7a598255b0e0e245112879c102 --- /dev/null +++ b/tests/components/mealie/test_init.py @@ -0,0 +1,70 @@ +"""Tests for the Mealie integration.""" + +from unittest.mock import AsyncMock + +from aiomealie import MealieAuthenticationError, MealieConnectionError +import pytest +from syrupy import SnapshotAssertion + +from homeassistant.components.mealie.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_device_info( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test device registry integration.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, mock_config_entry.entry_id)} + ) + assert device_entry is not None + assert device_entry == snapshot + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_remove(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + ("exc", "state"), + [ + (MealieConnectionError, ConfigEntryState.SETUP_RETRY), + (MealieAuthenticationError, ConfigEntryState.SETUP_ERROR), + ], +) +async def test_initialization_failure( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exc: Exception, + state: ConfigEntryState, +) -> None: + """Test initialization failure.""" + mock_mealie_client.get_mealplans.side_effect = exc + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is state