mirror of
				https://github.com/mealie-recipes/mealie.git
				synced 2025-10-30 17:53:31 -04:00 
			
		
		
		
	fix: dash slug names (#5709)
Co-authored-by: Michael Genson <71845777+michael-genson@users.noreply.github.com>
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							144d4caea6
						
					
				
				
					commit
					107dfc34de
				
			| @@ -22,6 +22,14 @@ class PermissionDenied(Exception): | |||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SlugError(Exception): | ||||||
|  |     """ | ||||||
|  |     This exception is raised when the recipe name generates an invalid slug. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
| class NoEntryFound(Exception): | class NoEntryFound(Exception): | ||||||
|     """ |     """ | ||||||
|     This exception is raised when a user tries to access a resource that does not exist. |     This exception is raised when a user tries to access a resource that does not exist. | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ from uuid import UUID | |||||||
|  |  | ||||||
| import orjson | import orjson | ||||||
| import sqlalchemy | import sqlalchemy | ||||||
|  | import sqlalchemy.exc | ||||||
| from fastapi import ( | from fastapi import ( | ||||||
|     BackgroundTasks, |     BackgroundTasks, | ||||||
|     Depends, |     Depends, | ||||||
| @@ -80,13 +81,25 @@ class RecipeController(BaseRecipeController): | |||||||
|  |  | ||||||
|         if thrownType == exceptions.PermissionDenied: |         if thrownType == exceptions.PermissionDenied: | ||||||
|             self.logger.error("Permission Denied on recipe controller action") |             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: |         elif thrownType == exceptions.NoEntryFound: | ||||||
|             self.logger.error("No Entry Found on recipe controller action") |             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: |         elif thrownType == sqlalchemy.exc.IntegrityError: | ||||||
|             self.logger.error("SQL Integrity Error on recipe controller action") |             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: |         else: | ||||||
|             self.logger.error("Unknown Error on recipe controller action") |             self.logger.error("Unknown Error on recipe controller action") | ||||||
|             self.logger.exception(ex) |             self.logger.exception(ex) | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ from sqlalchemy.orm import Session, joinedload, selectinload | |||||||
| from sqlalchemy.orm.interfaces import LoaderOption | from sqlalchemy.orm.interfaces import LoaderOption | ||||||
|  |  | ||||||
| from mealie.core.config import get_app_dirs | from mealie.core.config import get_app_dirs | ||||||
|  | from mealie.core.exceptions import SlugError | ||||||
| from mealie.db.models.users.users import User | from mealie.db.models.users.users import User | ||||||
| from mealie.schema._mealie import MealieModel, SearchType | from mealie.schema._mealie import MealieModel, SearchType | ||||||
| from mealie.schema._mealie.mealie_model import UpdatedAtField | from mealie.schema._mealie.mealie_model import UpdatedAtField | ||||||
| @@ -45,8 +46,13 @@ def create_recipe_slug(name: str, max_length: int = 250) -> str: | |||||||
|  |  | ||||||
|     Returns: |     Returns: | ||||||
|         A truncated slug string |         A truncated slug string | ||||||
|  |  | ||||||
|  |     Raises: | ||||||
|  |         ValueError: If the name cannot be converted to a valid slug | ||||||
|     """ |     """ | ||||||
|     generated_slug = slugify(name) |     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: |     if len(generated_slug) > max_length: | ||||||
|         generated_slug = generated_slug[:max_length] |         generated_slug = generated_slug[:max_length] | ||||||
|     return generated_slug |     return generated_slug | ||||||
|   | |||||||
| @@ -929,6 +929,13 @@ def test_create_recipe_with_extremely_long_slug(api_client: TestClient, unique_u | |||||||
|     assert updated_recipe["slug"] == slugify(new_name) |     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): | def test_create_recipe_slug_length_validation(api_client: TestClient, unique_user: TestUser): | ||||||
|     """Test that recipe slugs are properly truncated to a reasonable length.""" |     """Test that recipe slugs are properly truncated to a reasonable length.""" | ||||||
|     very_long_name = "A" * 500  # 500 character name |     very_long_name = "A" * 500  # 500 character name | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user