feat(recipe): validate heroImageUrl content type before persisting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package com.recipeapp.recipe;
|
|||||||
|
|
||||||
import com.recipeapp.common.ConflictException;
|
import com.recipeapp.common.ConflictException;
|
||||||
import com.recipeapp.common.ResourceNotFoundException;
|
import com.recipeapp.common.ResourceNotFoundException;
|
||||||
|
import com.recipeapp.common.ValidationException;
|
||||||
import com.recipeapp.household.HouseholdRepository;
|
import com.recipeapp.household.HouseholdRepository;
|
||||||
import com.recipeapp.household.entity.Household;
|
import com.recipeapp.household.entity.Household;
|
||||||
import com.recipeapp.recipe.dto.*;
|
import com.recipeapp.recipe.dto.*;
|
||||||
@@ -60,6 +61,8 @@ public class RecipeService {
|
|||||||
Household household = householdRepository.findById(householdId)
|
Household household = householdRepository.findById(householdId)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Household not found"));
|
.orElseThrow(() -> new ResourceNotFoundException("Household not found"));
|
||||||
|
|
||||||
|
validateHeroImageUrl(request.heroImageUrl());
|
||||||
|
|
||||||
Recipe recipe = new Recipe(household, request.name(),
|
Recipe recipe = new Recipe(household, request.name(),
|
||||||
request.serves() != null ? request.serves().shortValue() : 0,
|
request.serves() != null ? request.serves().shortValue() : 0,
|
||||||
request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0,
|
request.cookTimeMin() != null ? request.cookTimeMin().shortValue() : 0,
|
||||||
@@ -80,6 +83,8 @@ public class RecipeService {
|
|||||||
Recipe recipe = findRecipe(householdId, recipeId);
|
Recipe recipe = findRecipe(householdId, recipeId);
|
||||||
Household household = recipe.getHousehold();
|
Household household = recipe.getHousehold();
|
||||||
|
|
||||||
|
validateHeroImageUrl(request.heroImageUrl());
|
||||||
|
|
||||||
recipe.setName(request.name());
|
recipe.setName(request.name());
|
||||||
recipe.setServes(request.serves() != null ? request.serves().shortValue() : 0);
|
recipe.setServes(request.serves() != null ? request.serves().shortValue() : 0);
|
||||||
recipe.setCookTimeMin(request.cookTimeMin() != null ? request.cookTimeMin().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());
|
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 helpers ──
|
||||||
|
|
||||||
private Recipe findRecipe(UUID householdId, UUID recipeId) {
|
private Recipe findRecipe(UUID householdId, UUID recipeId) {
|
||||||
|
|||||||
@@ -571,6 +571,31 @@ class RecipeServiceTest {
|
|||||||
.isInstanceOf(ResourceNotFoundException.class);
|
.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
|
@Test
|
||||||
void listTagsShouldReturnEmptyList() {
|
void listTagsShouldReturnEmptyList() {
|
||||||
when(tagRepository.findByHouseholdId(HOUSEHOLD_ID)).thenReturn(List.of());
|
when(tagRepository.findByHouseholdId(HOUSEHOLD_ID)).thenReturn(List.of());
|
||||||
|
|||||||
Reference in New Issue
Block a user