feat(recipe): validate heroImageUrl content type before persisting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-10 09:08:45 +02:00
parent b1eb9ed964
commit 90cff0c4d2
2 changed files with 42 additions and 0 deletions

View File

@@ -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) {

View File

@@ -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());