diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index 6eeedf36c..c5946c49b 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -523,13 +523,12 @@ class RecipeController(BaseRecipeController): @router.put("/{slug}/image", response_model=UpdateImageResponse, tags=["Recipe: Images and Assets"]) def update_recipe_image(self, slug: str, image: bytes = File(...), extension: str = Form(...)): - group_recipes = get_repositories(self.session, group_id=self.group_id, household_id=None).recipes - recipe = group_recipes.get_one(slug) - data_service = RecipeDataService(recipe.id) - data_service.write_image(image, extension) - - new_version = self.group_recipes.update_image(slug, extension) - return UpdateImageResponse(image=new_version) + try: + new_version = self.service.update_recipe_image(slug, image, extension) + return UpdateImageResponse(image=new_version) + except Exception as e: + self.handle_exceptions(e) + return None @router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"]) def upload_recipe_asset( @@ -551,8 +550,7 @@ class RecipeController(BaseRecipeController): file_name = f"{file_slug}.{extension}" asset_in = RecipeAsset(name=name, icon=icon, file_name=file_name) - group_recipes = get_repositories(self.session, group_id=self.group_id, household_id=None).recipes - recipe = group_recipes.get_one(slug) + recipe = self.service.get_one(slug) dest = recipe.asset_dir / file_name @@ -569,8 +567,9 @@ class RecipeController(BaseRecipeController): if not dest.is_file(): raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR) - recipe.assets.append(asset_in) + if recipe.assets is not None: + recipe.assets.append(asset_in) - group_recipes.update(slug, recipe) + self.service.update_one(slug, recipe) return asset_in diff --git a/mealie/services/recipe/recipe_service.py b/mealie/services/recipe/recipe_service.py index 42d4f1e88..381793eb3 100644 --- a/mealie/services/recipe/recipe_service.py +++ b/mealie/services/recipe/recipe_service.py @@ -408,6 +408,16 @@ class RecipeService(RecipeServiceBase): self.check_assets(new_data, recipe.slug) return new_data + def update_recipe_image(self, slug: str, image: bytes, extension: str): + recipe = self.get_one(slug) + if not self.can_update(recipe): + raise exceptions.PermissionDenied("You do not have permission to edit this recipe.") + + data_service = RecipeDataService(recipe.id) + data_service.write_image(image, extension) + + return self.group_recipes.update_image(slug, extension) + def patch_one(self, slug_or_id: str | UUID, patch_data: Recipe) -> Recipe: recipe: Recipe = self._pre_update_check(slug_or_id, patch_data) diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_cross_household.py b/tests/integration_tests/user_recipe_tests/test_recipe_cross_household.py index 4334c0e55..5b649e151 100644 --- a/tests/integration_tests/user_recipe_tests/test_recipe_cross_household.py +++ b/tests/integration_tests/user_recipe_tests/test_recipe_cross_household.py @@ -7,6 +7,7 @@ from fastapi.testclient import TestClient from mealie.schema.cookbook.cookbook import SaveCookBook from mealie.schema.recipe.recipe import Recipe from mealie.schema.recipe.recipe_category import TagSave +from tests import data from tests.utils import api_routes from tests.utils.factories import random_string from tests.utils.fixture_schemas import TestUser @@ -371,3 +372,53 @@ def test_cookbooks_from_other_households(api_client: TestClient, unique_user: Te response = api_client.get(api_routes.recipes, params={"cookbook": h2_cookbook.slug}, headers=unique_user.token) assert response.status_code == 200 + + +@pytest.mark.parametrize("is_private_household", [True, False]) +@pytest.mark.parametrize("household_lock_recipe_edits", [True, False]) +def test_update_recipe_image_from_other_households( + api_client: TestClient, + unique_user: TestUser, + h2_user: TestUser, + is_private_household: bool, + household_lock_recipe_edits: bool, +): + household = unique_user.repos.households.get_one(h2_user.household_id) + assert household and household.preferences + household.preferences.private_household = is_private_household + household.preferences.lock_recipe_edits_from_other_households = household_lock_recipe_edits + unique_user.repos.household_preferences.update(household.id, household.preferences) + + response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=h2_user.token) + assert response.status_code == 201 + h2_recipe = h2_user.repos.recipes.get_one(response.json()) + assert h2_recipe and h2_recipe.id + h2_recipe_id = str(h2_recipe.id) + + response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token) + assert response.status_code == 200 + recipe_json = response.json() + assert recipe_json["id"] == h2_recipe_id + image_version = response.json()["image"] + + data_payload = {"extension": "jpg"} + file_payload = {"image": data.images_test_image_1.read_bytes()} + + response = api_client.put( + api_routes.recipes_slug_image(recipe_json["slug"]), + data=data_payload, + files=file_payload, + headers=unique_user.token, + ) + + if household_lock_recipe_edits: + assert response.status_code == 403 + response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token) + recipe_respons = response.json() + assert recipe_respons["image"] == image_version + + else: + assert response.status_code == 200 + response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token) + recipe_respons = response.json() + assert recipe_respons["image"] is not None diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_owner.py b/tests/integration_tests/user_recipe_tests/test_recipe_owner.py index f1b1c1ec9..3bfe0191a 100644 --- a/tests/integration_tests/user_recipe_tests/test_recipe_owner.py +++ b/tests/integration_tests/user_recipe_tests/test_recipe_owner.py @@ -6,6 +6,7 @@ from fastapi.testclient import TestClient from mealie.repos.repository_factory import AllRepositories from mealie.schema.recipe.recipe import Recipe from mealie.schema.recipe.recipe_settings import RecipeSettings +from tests import data from tests.utils import api_routes from tests.utils.factories import random_string from tests.utils.fixture_schemas import TestUser @@ -180,3 +181,33 @@ def test_admin_can_delete_locked_recipe_owned_by_another_user( response = api_client.delete(api_routes.recipes_slug(slug), headers=admin_user.token) assert response.status_code == 200 + + +def test_user_can_update_recipe_image(api_client: TestClient, unique_user: TestUser): + data_payload = {"extension": "jpg"} + file_payload = {"image": data.images_test_image_1.read_bytes()} + + household = unique_user.repos.households.get_one(unique_user.household_id) + assert household and household.preferences + household.preferences.private_household = True + household.preferences.lock_recipe_edits_from_other_households = True + unique_user.repos.household_preferences.update(household.id, household.preferences) + + response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token) + assert response.status_code == 201 + recipe_json = unique_user.repos.recipes.get_one(response.json()) + assert recipe_json and recipe_json.id + assert recipe_json.image is None + recipe_id = str(recipe_json.id) + + response = api_client.put( + api_routes.recipes_slug_image(recipe_json.slug), + data=data_payload, + files=file_payload, + headers=unique_user.token, + ) + assert response.status_code == 200 + + response = api_client.get(api_routes.recipes_slug(recipe_id), headers=unique_user.token) + recipe_respons = response.json() + assert recipe_respons["image"] is not None