From 90cff0c4d2fdd5816afd175b1fd867e029fa43bb Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Fri, 10 Apr 2026 09:08:45 +0200 Subject: [PATCH] feat(recipe): validate heroImageUrl content type before persisting Co-Authored-By: Claude Sonnet 4.6 --- .../com/recipeapp/recipe/RecipeService.java | 17 +++++++++++++ .../recipeapp/recipe/RecipeServiceTest.java | 25 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/backend/src/main/java/com/recipeapp/recipe/RecipeService.java b/backend/src/main/java/com/recipeapp/recipe/RecipeService.java index cfd36f2..8228b50 100644 --- a/backend/src/main/java/com/recipeapp/recipe/RecipeService.java +++ b/backend/src/main/java/com/recipeapp/recipe/RecipeService.java @@ -2,6 +2,7 @@ package com.recipeapp.recipe; import com.recipeapp.common.ConflictException; import com.recipeapp.common.ResourceNotFoundException; +import com.recipeapp.common.ValidationException; import com.recipeapp.household.HouseholdRepository; import com.recipeapp.household.entity.Household; import com.recipeapp.recipe.dto.*; @@ -60,6 +61,8 @@ public class RecipeService { Household household = householdRepository.findById(householdId) .orElseThrow(() -> new ResourceNotFoundException("Household not found")); + validateHeroImageUrl(request.heroImageUrl()); + Recipe recipe = new Recipe(household, request.name(), request.serves() != null ? request.serves().shortValue() : 0, request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0, @@ -80,6 +83,8 @@ public class RecipeService { Recipe recipe = findRecipe(householdId, recipeId); Household household = recipe.getHousehold(); + validateHeroImageUrl(request.heroImageUrl()); + recipe.setName(request.name()); recipe.setServes(request.serves() != null ? request.serves().shortValue() : 0); recipe.setCookTimeMin(request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0); @@ -183,6 +188,18 @@ public class RecipeService { return new IngredientCategoryResponse(category.getId(), category.getName()); } + // ── Image validation ── + + private static final java.util.regex.Pattern ALLOWED_IMAGE_PATTERN = + java.util.regex.Pattern.compile("^data:image/(jpeg|jpg|png|gif|webp);base64,"); + + private void validateHeroImageUrl(String heroImageUrl) { + if (heroImageUrl == null || heroImageUrl.isBlank()) return; + if (!ALLOWED_IMAGE_PATTERN.matcher(heroImageUrl).find()) { + throw new ValidationException("Ungültiger Bildtyp. Erlaubt sind: JPEG, PNG, GIF, WebP."); + } + } + // ── Private helpers ── private Recipe findRecipe(UUID householdId, UUID recipeId) { diff --git a/backend/src/test/java/com/recipeapp/recipe/RecipeServiceTest.java b/backend/src/test/java/com/recipeapp/recipe/RecipeServiceTest.java index 8892d25..792be7f 100644 --- a/backend/src/test/java/com/recipeapp/recipe/RecipeServiceTest.java +++ b/backend/src/test/java/com/recipeapp/recipe/RecipeServiceTest.java @@ -571,6 +571,31 @@ class RecipeServiceTest { .isInstanceOf(ResourceNotFoundException.class); } + @Test + void createRecipeWithDisallowedImageTypeShouldThrowValidationException() { + when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(testHousehold())); + + var request = new RecipeCreateRequest( + "Test", null, null, "easy", "data:application/pdf;base64,abc", + List.of(), List.of(), List.of()); + + assertThatThrownBy(() -> recipeService.createRecipe(HOUSEHOLD_ID, request)) + .isInstanceOf(com.recipeapp.common.ValidationException.class); + } + + @Test + void createRecipeWithAllowedImageTypeShouldNotThrow() { + var household = testHousehold(); + when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household)); + when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> i.getArgument(0)); + + var request = new RecipeCreateRequest( + "Test", null, null, "easy", "data:image/jpeg;base64,abc", + List.of(), List.of(), List.of()); + + assertThatNoException().isThrownBy(() -> recipeService.createRecipe(HOUSEHOLD_ID, request)); + } + @Test void listTagsShouldReturnEmptyList() { when(tagRepository.findByHouseholdId(HOUSEHOLD_ID)).thenReturn(List.of());