From f796258529fda0a0e6d115c1d9c7b5945aedfc3d Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Thu, 12 Sep 2024 04:43:23 -0500 Subject: [PATCH 1/4] fix: Broken Social Preview Links (#4183) --- mealie/routes/spa/__init__.py | 14 +-- tests/integration_tests/test_spa.py | 131 ++++++++++++++++++++++++++-- 2 files changed, 133 insertions(+), 12 deletions(-) diff --git a/mealie/routes/spa/__init__.py b/mealie/routes/spa/__init__.py index a673969c7b8..77af4747dc3 100644 --- a/mealie/routes/spa/__init__.py +++ b/mealie/routes/spa/__init__.py @@ -165,13 +165,13 @@ def serve_recipe_with_meta_public( public_repos = AllRepositories(session) group = public_repos.groups.get_by_slug_or_id(group_slug) - if not group or group.preferences.private_group: # type: ignore + if not (group and group.preferences) or group.preferences.private_group: return response_404() - group_repos = AllRepositories(session, group_id=group.id) + group_repos = AllRepositories(session, group_id=group.id, household_id=None) recipe = group_repos.recipes.get_one(recipe_slug) - if not recipe or not recipe.settings.public: # type: ignore + if not (recipe and recipe.settings) or not recipe.settings.public: return response_404() # Inject meta tags @@ -190,9 +190,9 @@ async def serve_recipe_with_meta( return serve_recipe_with_meta_public(group_slug, recipe_slug, session) try: - repos = AllRepositories(session, group_id=user.group_id) + group_repos = AllRepositories(session, group_id=user.group_id, household_id=None) - recipe = repos.recipes.get_one(recipe_slug, "slug") + recipe = group_repos.recipes.get_one(recipe_slug, "slug") if recipe is None: return response_404() @@ -204,8 +204,8 @@ async def serve_recipe_with_meta( async def serve_shared_recipe_with_meta(group_slug: str, token_id: str, session: Session = Depends(generate_session)): try: - repos = AllRepositories(session) - token_summary = repos.recipe_share_tokens.get_one(token_id) + public_repos = AllRepositories(session, group_id=None) + token_summary = public_repos.recipe_share_tokens.get_one(token_id) if token_summary is None: raise Exception("Token Not Found") diff --git a/tests/integration_tests/test_spa.py b/tests/integration_tests/test_spa.py index f4c3dc190c8..68373f79ab8 100644 --- a/tests/integration_tests/test_spa.py +++ b/tests/integration_tests/test_spa.py @@ -1,8 +1,46 @@ +import pytest from bs4 import BeautifulSoup -from mealie.routes.spa import MetaTag, inject_meta, inject_recipe_json +from mealie.routes import spa +from mealie.schema.recipe.recipe import Recipe +from mealie.schema.recipe.recipe_share_token import RecipeShareTokenSave from tests import data as test_data from tests.utils.factories import random_string +from tests.utils.fixture_schemas import TestUser + + +@pytest.fixture(autouse=True) +def set_spa_contents(): + """Inject a simple HTML string into the SPA module to enable metadata injection""" + + spa.__contents = "
" + + +def set_group_is_private(unique_user: TestUser, *, is_private: bool): + group = unique_user.repos.groups.get_by_slug_or_id(unique_user.group_id) + assert group and group.preferences + group.preferences.private_group = is_private + unique_user.repos.group_preferences.update(group.id, group.preferences) + + +def set_recipe_is_public(unique_user: TestUser, recipe: Recipe, *, is_public: bool): + assert recipe.settings + recipe.settings.public = is_public + unique_user.repos.recipes.update(recipe.slug, recipe) + + +def create_recipe(user: TestUser) -> Recipe: + recipe = user.repos.recipes.create( + Recipe( + user_id=user.user_id, + group_id=user.group_id, + name=random_string(), + ) + ) + set_group_is_private(user, is_private=False) + set_recipe_is_public(user, recipe, is_public=True) + + return recipe def test_spa_metadata_injection(): @@ -22,9 +60,9 @@ def test_spa_metadata_injection(): assert title_tag and title_tag["content"] - new_title_tag = MetaTag(hid="og:title", property_name="og:title", content=random_string()) - new_arbitrary_tag = MetaTag(hid=random_string(), property_name=random_string(), content=random_string()) - new_html = inject_meta(str(soup), [new_title_tag, new_arbitrary_tag]) + new_title_tag = spa.MetaTag(hid="og:title", property_name="og:title", content=random_string()) + new_arbitrary_tag = spa.MetaTag(hid=random_string(), property_name=random_string(), content=random_string()) + new_html = spa.inject_meta(str(soup), [new_title_tag, new_arbitrary_tag]) # verify changes were injected soup = BeautifulSoup(new_html, "lxml") @@ -63,8 +101,91 @@ def test_spa_recipe_json_injection(): soup = BeautifulSoup(f, "lxml") assert "https://schema.org" not in str(soup) - html = inject_recipe_json(str(soup), schema) + html = spa.inject_recipe_json(str(soup), schema) assert "@context" in html assert "https://schema.org" in html assert recipe_name in html + + +@pytest.mark.parametrize("use_public_user", [True, False]) +@pytest.mark.asyncio +async def test_spa_serve_recipe_with_meta(unique_user: TestUser, use_public_user: bool): + recipe = create_recipe(unique_user) + user = unique_user.repos.users.get_by_username(unique_user.username) + assert user + + response = await spa.serve_recipe_with_meta( + user.group_slug, recipe.slug, user=None if use_public_user else user, session=unique_user.repos.session + ) + assert response.status_code == 200 + assert "https://schema.org" in response.body.decode() + + +@pytest.mark.parametrize("use_public_user", [True, False]) +@pytest.mark.asyncio +async def test_spa_serve_recipe_with_meta_invalid_data(unique_user: TestUser, use_public_user: bool): + recipe = create_recipe(unique_user) + user = unique_user.repos.users.get_by_username(unique_user.username) + assert user + + response = await spa.serve_recipe_with_meta( + random_string(), recipe.slug, user=None if use_public_user else user, session=unique_user.repos.session + ) + assert response.status_code == 404 + + response = await spa.serve_recipe_with_meta( + user.group_slug, random_string(), user=None if use_public_user else user, session=unique_user.repos.session + ) + assert response.status_code == 404 + + set_recipe_is_public(unique_user, recipe, is_public=False) + response = await spa.serve_recipe_with_meta( + user.group_slug, recipe.slug, user=None if use_public_user else user, session=unique_user.repos.session + ) + if use_public_user: + assert response.status_code == 404 + else: + assert response.status_code == 200 + + set_group_is_private(unique_user, is_private=True) + set_recipe_is_public(unique_user, recipe, is_public=True) + response = await spa.serve_recipe_with_meta( + user.group_slug, recipe.slug, user=None if use_public_user else user, session=unique_user.repos.session + ) + if use_public_user: + assert response.status_code == 404 + else: + assert response.status_code == 200 + + +@pytest.mark.parametrize("use_private_group", [True, False]) +@pytest.mark.parametrize("use_public_recipe", [True, False]) +@pytest.mark.asyncio +async def test_spa_service_shared_recipe_with_meta( + unique_user: TestUser, use_private_group: bool, use_public_recipe: bool +): + group = unique_user.repos.groups.get_by_slug_or_id(unique_user.group_id) + assert group + recipe = create_recipe(unique_user) + + # visibility settings shouldn't matter for shared recipes + set_group_is_private(unique_user, is_private=use_private_group) + set_recipe_is_public(unique_user, recipe, is_public=use_public_recipe) + + token = unique_user.repos.recipe_share_tokens.create( + RecipeShareTokenSave(recipe_id=recipe.id, group_id=unique_user.group_id) + ) + + response = await spa.serve_shared_recipe_with_meta(group.slug, token.id, session=unique_user.repos.session) + assert response.status_code == 200 + assert "https://schema.org" in response.body.decode() + + +@pytest.mark.asyncio +async def test_spa_service_shared_recipe_with_meta_invalid_data(unique_user: TestUser): + group = unique_user.repos.groups.get_by_slug_or_id(unique_user.group_id) + assert group + + response = await spa.serve_shared_recipe_with_meta(group.slug, random_string(), session=unique_user.repos.session) + assert response.status_code == 404 From c97053ef83b03729853c52c8d61af157995925e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 21:59:37 +1000 Subject: [PATCH 2/4] chore(deps): update dependency pydantic-to-typescript2 to v1.0.6 (#4199) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 49180e7aaad..1dcd8c8ced1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2202,13 +2202,13 @@ yaml = ["pyyaml (>=6.0.1)"] [[package]] name = "pydantic-to-typescript2" -version = "1.0.4" +version = "1.0.6" description = "Convert pydantic v1 and pydantic v2 models to typescript interfaces" optional = false python-versions = "*" files = [ - {file = "pydantic-to-typescript2-1.0.4.tar.gz", hash = "sha256:1bd25dea0e1ce4220c495ad408d028ddd905376cbed1a856b2caa6f31c344371"}, - {file = "pydantic_to_typescript2-1.0.4-py3-none-any.whl", hash = "sha256:597f1848e918d0b95879c9a371ff664c8f300894d43aafb0b7f693f6c84f3657"}, + {file = "pydantic-to-typescript2-1.0.6.tar.gz", hash = "sha256:19cc0fb03802abcb508b02fbc334f1667ff50e0853a782b58df9dd0409290163"}, + {file = "pydantic_to_typescript2-1.0.6-py3-none-any.whl", hash = "sha256:89bbdd4b84b72d9f8ada33fd4d7d6605457be302dd6d4c6d48faa9310841bb69"}, ] [package.dependencies] From 6f1df3a95e71d1f4481ccf98b1c7739f96061181 Mon Sep 17 00:00:00 2001 From: Kuchenpirat <24235032+Kuchenpirat@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:07:26 +0200 Subject: [PATCH 3/4] feat: Reorder ShoppingListItemEditor (#4200) --- .../ShoppingList/ShoppingListItemEditor.vue | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/frontend/components/Domain/ShoppingList/ShoppingListItemEditor.vue b/frontend/components/Domain/ShoppingList/ShoppingListItemEditor.vue index a707924723a..da9ca4427ea 100644 --- a/frontend/components/Domain/ShoppingList/ShoppingListItemEditor.vue +++ b/frontend/components/Domain/ShoppingList/ShoppingListItemEditor.vue @@ -3,14 +3,9 @@