diff --git a/mealie/core/exceptions.py b/mealie/core/exceptions.py index 89ed9538e..c51d5af63 100644 --- a/mealie/core/exceptions.py +++ b/mealie/core/exceptions.py @@ -22,6 +22,14 @@ class PermissionDenied(Exception): pass +class SlugError(Exception): + """ + This exception is raised when the recipe name generates an invalid slug. + """ + + pass + + class NoEntryFound(Exception): """ This exception is raised when a user tries to access a resource that does not exist. diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index c5946c49b..5232bf339 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -4,6 +4,7 @@ from uuid import UUID import orjson import sqlalchemy +import sqlalchemy.exc from fastapi import ( BackgroundTasks, Depends, @@ -80,13 +81,25 @@ class RecipeController(BaseRecipeController): if thrownType == exceptions.PermissionDenied: self.logger.error("Permission Denied on recipe controller action") - raise HTTPException(status_code=403, detail=ErrorResponse.respond(message="Permission Denied")) + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail=ErrorResponse.respond(message="Permission Denied") + ) elif thrownType == exceptions.NoEntryFound: self.logger.error("No Entry Found on recipe controller action") - raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No Entry Found")) + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail=ErrorResponse.respond(message="No Entry Found") + ) elif thrownType == sqlalchemy.exc.IntegrityError: self.logger.error("SQL Integrity Error on recipe controller action") - raise HTTPException(status_code=400, detail=ErrorResponse.respond(message="Recipe already exists")) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ErrorResponse.respond(message="Recipe already exists") + ) + elif thrownType == exceptions.SlugError: + self.logger.error("Failed to generate a valid slug from recipe name") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ErrorResponse.respond(message="Unable to generate recipe slug"), + ) else: self.logger.error("Unknown Error on recipe controller action") self.logger.exception(ex) diff --git a/mealie/schema/recipe/recipe.py b/mealie/schema/recipe/recipe.py index ffc62d112..3863e8f80 100644 --- a/mealie/schema/recipe/recipe.py +++ b/mealie/schema/recipe/recipe.py @@ -14,6 +14,7 @@ from sqlalchemy.orm import Session, joinedload, selectinload from sqlalchemy.orm.interfaces import LoaderOption from mealie.core.config import get_app_dirs +from mealie.core.exceptions import SlugError from mealie.db.models.users.users import User from mealie.schema._mealie import MealieModel, SearchType from mealie.schema._mealie.mealie_model import UpdatedAtField @@ -45,8 +46,13 @@ def create_recipe_slug(name: str, max_length: int = 250) -> str: Returns: A truncated slug string + + Raises: + ValueError: If the name cannot be converted to a valid slug """ generated_slug = slugify(name) + if not generated_slug: + raise SlugError("Recipe name cannot be empty or contain only special characters") if len(generated_slug) > max_length: generated_slug = generated_slug[:max_length] return generated_slug diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_crud.py b/tests/integration_tests/user_recipe_tests/test_recipe_crud.py index b87200fc6..b36efb6de 100644 --- a/tests/integration_tests/user_recipe_tests/test_recipe_crud.py +++ b/tests/integration_tests/user_recipe_tests/test_recipe_crud.py @@ -929,6 +929,13 @@ def test_create_recipe_with_extremely_long_slug(api_client: TestClient, unique_u assert updated_recipe["slug"] == slugify(new_name) +def test_create_recipe_slug_not_empty(api_client: TestClient, unique_user: TestUser): + recipe_name = "---" # will result in an empty slug + + response = api_client.post(api_routes.recipes, json={"name": recipe_name}, headers=unique_user.token) + assert response.status_code == 400 + + def test_create_recipe_slug_length_validation(api_client: TestClient, unique_user: TestUser): """Test that recipe slugs are properly truncated to a reasonable length.""" very_long_name = "A" * 500 # 500 character name