- Add VarietyScoreConfig entity, repository, and V020 migration for per-household scoring weights and configurable tag types - Rewrite getVarietyScore: tag-type repeats on consecutive days, non-staple ingredient overlaps, cooking log history, plan duplicates - Rewrite getSuggestions: simulate variety score for each candidate, add tag filter (AND, case-insensitive) and configurable topN param - Update SuggestionResponse to return simulatedScore instead of fitReasons/warnings, update VarietyScoreResponse to new shape - Seed default VarietyScoreConfig on household creation - Extend test suite across all domains (+270 tests, all passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
559 lines
22 KiB
Java
559 lines
22 KiB
Java
package com.recipeapp.recipe;
|
|
|
|
import com.recipeapp.common.ConflictException;
|
|
import com.recipeapp.common.ResourceNotFoundException;
|
|
import com.recipeapp.household.HouseholdRepository;
|
|
import com.recipeapp.household.entity.Household;
|
|
import com.recipeapp.recipe.dto.*;
|
|
import com.recipeapp.recipe.entity.*;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
import org.mockito.InjectMocks;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.util.*;
|
|
|
|
import static org.assertj.core.api.Assertions.*;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.Mockito.*;
|
|
|
|
@ExtendWith(MockitoExtension.class)
|
|
class RecipeServiceTest {
|
|
|
|
@Mock private RecipeRepository recipeRepository;
|
|
@Mock private IngredientRepository ingredientRepository;
|
|
@Mock private TagRepository tagRepository;
|
|
@Mock private IngredientCategoryRepository ingredientCategoryRepository;
|
|
@Mock private HouseholdRepository householdRepository;
|
|
|
|
@InjectMocks private RecipeServiceImpl recipeService;
|
|
|
|
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
|
|
|
private Household testHousehold() {
|
|
var h = new Household("Test family", null);
|
|
try {
|
|
var field = Household.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(h, HOUSEHOLD_ID);
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return h;
|
|
}
|
|
|
|
private Recipe testRecipe(Household household) {
|
|
var r = new Recipe(household, "Spaghetti Bolognese", (short) 4, (short) 45, "medium", true);
|
|
try {
|
|
var field = Recipe.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(r, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return r;
|
|
}
|
|
|
|
private Ingredient testIngredient(Household household, String name) {
|
|
var ing = new Ingredient(household, name, false);
|
|
try {
|
|
var field = Ingredient.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(ing, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return ing;
|
|
}
|
|
|
|
private Tag testTag(Household household, String name, String type) {
|
|
var tag = new Tag(household, name, type);
|
|
try {
|
|
var field = Tag.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(tag, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return tag;
|
|
}
|
|
|
|
// ── Recipe CRUD ──
|
|
|
|
@Test
|
|
void getRecipeShouldReturnDetail() {
|
|
var household = testHousehold();
|
|
var recipe = testRecipe(household);
|
|
var ingredient = testIngredient(household, "spaghetti");
|
|
recipe.getIngredients().add(new RecipeIngredient(recipe, ingredient, new BigDecimal("400"), "g", (short) 1));
|
|
recipe.getSteps().add(new RecipeStep(recipe, (short) 1, "Boil water."));
|
|
var tag = testTag(household, "beef", "protein");
|
|
recipe.getTags().add(tag);
|
|
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipe.getId(), HOUSEHOLD_ID))
|
|
.thenReturn(Optional.of(recipe));
|
|
|
|
RecipeDetailResponse result = recipeService.getRecipe(HOUSEHOLD_ID, recipe.getId());
|
|
|
|
assertThat(result.name()).isEqualTo("Spaghetti Bolognese");
|
|
assertThat(result.ingredients()).hasSize(1);
|
|
assertThat(result.ingredients().getFirst().name()).isEqualTo("spaghetti");
|
|
assertThat(result.steps()).hasSize(1);
|
|
assertThat(result.tags()).hasSize(1);
|
|
}
|
|
|
|
@Test
|
|
void getRecipeShouldThrowWhenNotFound() {
|
|
var id = UUID.randomUUID();
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(id, HOUSEHOLD_ID))
|
|
.thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> recipeService.getRecipe(HOUSEHOLD_ID, id))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void createRecipeShouldPersistWithIngredientsStepsTags() {
|
|
var household = testHousehold();
|
|
var ingredient = testIngredient(household, "spaghetti");
|
|
var tag = testTag(household, "beef", "protein");
|
|
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(ingredientRepository.findById(ingredient.getId())).thenReturn(Optional.of(ingredient));
|
|
when(tagRepository.findAllById(List.of(tag.getId()))).thenReturn(List.of(tag));
|
|
when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> {
|
|
Recipe r = i.getArgument(0);
|
|
try {
|
|
var field = Recipe.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(r, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return r;
|
|
});
|
|
|
|
var request = new RecipeCreateRequest(
|
|
"Spaghetti Bolognese", (short) 4, (short) 45, "medium", true, null,
|
|
List.of(new RecipeCreateRequest.IngredientEntry(
|
|
ingredient.getId(), null, new BigDecimal("400"), "g", (short) 1)),
|
|
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Boil water.")),
|
|
List.of(tag.getId()));
|
|
|
|
RecipeDetailResponse result = recipeService.createRecipe(HOUSEHOLD_ID, request);
|
|
|
|
assertThat(result.name()).isEqualTo("Spaghetti Bolognese");
|
|
assertThat(result.id()).isNotNull();
|
|
verify(recipeRepository).save(any(Recipe.class));
|
|
}
|
|
|
|
@Test
|
|
void createRecipeShouldCreateNewIngredientWhenNameProvided() {
|
|
var household = testHousehold();
|
|
var tag = testTag(household, "beef", "protein");
|
|
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(ingredientRepository.save(any(Ingredient.class))).thenAnswer(i -> {
|
|
Ingredient ing = i.getArgument(0);
|
|
try {
|
|
var field = Ingredient.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(ing, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return ing;
|
|
});
|
|
when(tagRepository.findAllById(List.of(tag.getId()))).thenReturn(List.of(tag));
|
|
when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> {
|
|
Recipe r = i.getArgument(0);
|
|
try {
|
|
var field = Recipe.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(r, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return r;
|
|
});
|
|
|
|
var request = new RecipeCreateRequest(
|
|
"Carbonara", (short) 2, (short) 30, "medium", false, null,
|
|
List.of(new RecipeCreateRequest.IngredientEntry(
|
|
null, "pancetta", new BigDecimal("100"), "g", (short) 1)),
|
|
List.of(),
|
|
List.of(tag.getId()));
|
|
|
|
RecipeDetailResponse result = recipeService.createRecipe(HOUSEHOLD_ID, request);
|
|
|
|
assertThat(result.name()).isEqualTo("Carbonara");
|
|
verify(ingredientRepository).save(any(Ingredient.class));
|
|
}
|
|
|
|
@Test
|
|
void updateRecipeShouldReplaceChildren() {
|
|
var household = testHousehold();
|
|
var recipe = testRecipe(household);
|
|
var ingredient = testIngredient(household, "rice");
|
|
var tag = testTag(household, "chicken", "protein");
|
|
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipe.getId(), HOUSEHOLD_ID))
|
|
.thenReturn(Optional.of(recipe));
|
|
when(ingredientRepository.findById(ingredient.getId())).thenReturn(Optional.of(ingredient));
|
|
when(tagRepository.findAllById(List.of(tag.getId()))).thenReturn(List.of(tag));
|
|
when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> i.getArgument(0));
|
|
|
|
var request = new RecipeCreateRequest(
|
|
"Chicken Rice", (short) 3, (short) 25, "easy", true, null,
|
|
List.of(new RecipeCreateRequest.IngredientEntry(
|
|
ingredient.getId(), null, new BigDecimal("300"), "g", (short) 1)),
|
|
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Cook rice.")),
|
|
List.of(tag.getId()));
|
|
|
|
RecipeDetailResponse result = recipeService.updateRecipe(HOUSEHOLD_ID, recipe.getId(), request);
|
|
|
|
assertThat(result.name()).isEqualTo("Chicken Rice");
|
|
assertThat(result.serves()).isEqualTo((short) 3);
|
|
}
|
|
|
|
@Test
|
|
void deleteRecipeShouldSoftDelete() {
|
|
var household = testHousehold();
|
|
var recipe = testRecipe(household);
|
|
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipe.getId(), HOUSEHOLD_ID))
|
|
.thenReturn(Optional.of(recipe));
|
|
|
|
recipeService.deleteRecipe(HOUSEHOLD_ID, recipe.getId());
|
|
|
|
assertThat(recipe.getDeletedAt()).isNotNull();
|
|
}
|
|
|
|
// ── Ingredients ──
|
|
|
|
@Test
|
|
void searchIngredientsShouldReturnMatches() {
|
|
var household = testHousehold();
|
|
var ingredient = testIngredient(household, "chicken breast");
|
|
|
|
when(ingredientRepository.findByHouseholdIdAndNameContainingIgnoreCase(HOUSEHOLD_ID, "chick"))
|
|
.thenReturn(List.of(ingredient));
|
|
|
|
List<IngredientResponse> result = recipeService.searchIngredients(HOUSEHOLD_ID, "chick", null);
|
|
|
|
assertThat(result).hasSize(1);
|
|
assertThat(result.getFirst().name()).isEqualTo("chicken breast");
|
|
}
|
|
|
|
@Test
|
|
void patchIngredientShouldUpdateFields() {
|
|
var household = testHousehold();
|
|
var ingredient = testIngredient(household, "olive oil");
|
|
|
|
when(ingredientRepository.findById(ingredient.getId())).thenReturn(Optional.of(ingredient));
|
|
|
|
var request = new IngredientPatchRequest("extra virgin olive oil", true, null);
|
|
IngredientResponse result = recipeService.patchIngredient(HOUSEHOLD_ID, ingredient.getId(), request);
|
|
|
|
assertThat(result.name()).isEqualTo("extra virgin olive oil");
|
|
assertThat(result.isStaple()).isTrue();
|
|
}
|
|
|
|
// ── Tags ──
|
|
|
|
@Test
|
|
void listTagsShouldReturnAll() {
|
|
var household = testHousehold();
|
|
var tag = testTag(household, "chicken", "protein");
|
|
|
|
when(tagRepository.findByHouseholdId(HOUSEHOLD_ID)).thenReturn(List.of(tag));
|
|
|
|
List<TagResponse> result = recipeService.listTags(HOUSEHOLD_ID);
|
|
|
|
assertThat(result).hasSize(1);
|
|
assertThat(result.getFirst().name()).isEqualTo("chicken");
|
|
}
|
|
|
|
@Test
|
|
void createTagShouldPersist() {
|
|
var household = testHousehold();
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(tagRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "Thai")).thenReturn(false);
|
|
when(tagRepository.save(any(Tag.class))).thenAnswer(i -> {
|
|
Tag t = i.getArgument(0);
|
|
try {
|
|
var field = Tag.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(t, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return t;
|
|
});
|
|
|
|
TagResponse result = recipeService.createTag(HOUSEHOLD_ID, new TagCreateRequest("Thai", "cuisine"));
|
|
|
|
assertThat(result.name()).isEqualTo("Thai");
|
|
assertThat(result.tagType()).isEqualTo("cuisine");
|
|
}
|
|
|
|
@Test
|
|
void createTagShouldThrowConflictWhenNameExists() {
|
|
when(tagRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "Chicken")).thenReturn(true);
|
|
|
|
assertThatThrownBy(() -> recipeService.createTag(HOUSEHOLD_ID, new TagCreateRequest("Chicken", "protein")))
|
|
.isInstanceOf(ConflictException.class);
|
|
}
|
|
|
|
// ── Ingredient Categories ──
|
|
|
|
@Test
|
|
void listCategoriesShouldReturnAllSorted() {
|
|
var household = testHousehold();
|
|
var cat = new IngredientCategory(household, "Produce", (short) 1);
|
|
try {
|
|
var field = IngredientCategory.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(cat, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
|
|
when(ingredientCategoryRepository.findByHouseholdIdOrderBySortOrder(HOUSEHOLD_ID))
|
|
.thenReturn(List.of(cat));
|
|
|
|
List<IngredientCategoryResponse> result = recipeService.listCategories(HOUSEHOLD_ID);
|
|
|
|
assertThat(result).hasSize(1);
|
|
assertThat(result.getFirst().name()).isEqualTo("Produce");
|
|
}
|
|
|
|
@Test
|
|
void createCategoryShouldPersist() {
|
|
var household = testHousehold();
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(ingredientCategoryRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "Frozen"))
|
|
.thenReturn(false);
|
|
when(ingredientCategoryRepository.countByHouseholdId(HOUSEHOLD_ID)).thenReturn(8L);
|
|
when(ingredientCategoryRepository.save(any(IngredientCategory.class))).thenAnswer(i -> {
|
|
IngredientCategory c = i.getArgument(0);
|
|
try {
|
|
var field = IngredientCategory.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(c, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return c;
|
|
});
|
|
|
|
IngredientCategoryResponse result = recipeService.createCategory(
|
|
HOUSEHOLD_ID, new IngredientCategoryCreateRequest("Frozen"));
|
|
|
|
assertThat(result.name()).isEqualTo("Frozen");
|
|
}
|
|
|
|
@Test
|
|
void createCategoryShouldThrowConflictWhenNameExists() {
|
|
when(ingredientCategoryRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "Produce"))
|
|
.thenReturn(true);
|
|
|
|
assertThatThrownBy(() -> recipeService.createCategory(
|
|
HOUSEHOLD_ID, new IngredientCategoryCreateRequest("Produce")))
|
|
.isInstanceOf(ConflictException.class);
|
|
}
|
|
|
|
// ── Additional search filter combinations ──
|
|
|
|
@Test
|
|
void searchIngredientsShouldFilterByIsStapleOnly() {
|
|
var household = testHousehold();
|
|
var ingredient = testIngredient(household, "Salt");
|
|
|
|
when(ingredientRepository.findByHouseholdIdAndIsStaple(HOUSEHOLD_ID, true))
|
|
.thenReturn(List.of(ingredient));
|
|
|
|
List<IngredientResponse> result = recipeService.searchIngredients(HOUSEHOLD_ID, null, true);
|
|
|
|
assertThat(result).hasSize(1);
|
|
assertThat(result.getFirst().name()).isEqualTo("Salt");
|
|
}
|
|
|
|
@Test
|
|
void searchIngredientsShouldFilterBySearchAndIsStaple() {
|
|
var household = testHousehold();
|
|
var ingredient = testIngredient(household, "Olive oil");
|
|
|
|
when(ingredientRepository.findByHouseholdIdAndNameContainingIgnoreCaseAndIsStaple(
|
|
HOUSEHOLD_ID, "olive", true)).thenReturn(List.of(ingredient));
|
|
|
|
List<IngredientResponse> result = recipeService.searchIngredients(HOUSEHOLD_ID, "olive", true);
|
|
|
|
assertThat(result).hasSize(1);
|
|
}
|
|
|
|
@Test
|
|
void searchIngredientsShouldReturnAllWhenNoFilters() {
|
|
var household = testHousehold();
|
|
var ingredient = testIngredient(household, "Tomato");
|
|
|
|
when(ingredientRepository.findByHouseholdId(HOUSEHOLD_ID)).thenReturn(List.of(ingredient));
|
|
|
|
List<IngredientResponse> result = recipeService.searchIngredients(HOUSEHOLD_ID, null, null);
|
|
|
|
assertThat(result).hasSize(1);
|
|
}
|
|
|
|
@Test
|
|
void searchIngredientsShouldReturnEmptyListWhenNoMatches() {
|
|
when(ingredientRepository.findByHouseholdIdAndNameContainingIgnoreCase(HOUSEHOLD_ID, "xyz"))
|
|
.thenReturn(List.of());
|
|
|
|
List<IngredientResponse> result = recipeService.searchIngredients(HOUSEHOLD_ID, "xyz", null);
|
|
|
|
assertThat(result).isEmpty();
|
|
}
|
|
|
|
// ── Patch ingredient edge cases ──
|
|
|
|
@Test
|
|
void patchIngredientShouldThrowWhenNotFound() {
|
|
var id = UUID.randomUUID();
|
|
when(ingredientRepository.findById(id)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> recipeService.patchIngredient(HOUSEHOLD_ID, id,
|
|
new IngredientPatchRequest("new name", null, null)))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void patchIngredientShouldSetCategory() {
|
|
var household = testHousehold();
|
|
var ingredient = testIngredient(household, "Chicken breast");
|
|
var category = new IngredientCategory(household, "Fish & Meat", (short) 2);
|
|
try {
|
|
var field = IngredientCategory.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(category, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
|
|
when(ingredientRepository.findById(ingredient.getId())).thenReturn(Optional.of(ingredient));
|
|
when(ingredientCategoryRepository.findById(category.getId())).thenReturn(Optional.of(category));
|
|
|
|
var request = new IngredientPatchRequest(null, null, category.getId());
|
|
IngredientResponse result = recipeService.patchIngredient(HOUSEHOLD_ID, ingredient.getId(), request);
|
|
|
|
assertThat(result.category()).isNotNull();
|
|
assertThat(result.category().name()).isEqualTo("Fish & Meat");
|
|
}
|
|
|
|
@Test
|
|
void patchIngredientShouldThrowWhenCategoryNotFound() {
|
|
var household = testHousehold();
|
|
var ingredient = testIngredient(household, "Chicken breast");
|
|
var categoryId = UUID.randomUUID();
|
|
|
|
when(ingredientRepository.findById(ingredient.getId())).thenReturn(Optional.of(ingredient));
|
|
when(ingredientCategoryRepository.findById(categoryId)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> recipeService.patchIngredient(HOUSEHOLD_ID, ingredient.getId(),
|
|
new IngredientPatchRequest(null, null, categoryId)))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
// ── Create recipe edge cases ──
|
|
|
|
@Test
|
|
void createRecipeShouldThrowWhenHouseholdNotFound() {
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty());
|
|
|
|
var request = new RecipeCreateRequest(
|
|
"Test", (short) 2, (short) 15, "easy", false, null,
|
|
List.of(), List.of(), List.of());
|
|
|
|
assertThatThrownBy(() -> recipeService.createRecipe(HOUSEHOLD_ID, request))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void createRecipeShouldThrowWhenIngredientNotFound() {
|
|
var household = testHousehold();
|
|
var ingredientId = UUID.randomUUID();
|
|
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(ingredientRepository.findById(ingredientId)).thenReturn(Optional.empty());
|
|
|
|
var request = new RecipeCreateRequest(
|
|
"Test", (short) 2, (short) 15, "easy", false, null,
|
|
List.of(new RecipeCreateRequest.IngredientEntry(
|
|
ingredientId, null, new BigDecimal("100"), "g", (short) 1)),
|
|
List.of(), List.of());
|
|
|
|
assertThatThrownBy(() -> recipeService.createRecipe(HOUSEHOLD_ID, request))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void createRecipeShouldHandleNullIngredientsAndSteps() {
|
|
var household = testHousehold();
|
|
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> {
|
|
Recipe r = i.getArgument(0);
|
|
try {
|
|
var field = Recipe.class.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(r, UUID.randomUUID());
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
return r;
|
|
});
|
|
|
|
var request = new RecipeCreateRequest(
|
|
"Simple", (short) 1, (short) 5, "easy", false, null,
|
|
null, null, null);
|
|
|
|
RecipeDetailResponse result = recipeService.createRecipe(HOUSEHOLD_ID, request);
|
|
|
|
assertThat(result.name()).isEqualTo("Simple");
|
|
assertThat(result.ingredients()).isEmpty();
|
|
assertThat(result.steps()).isEmpty();
|
|
}
|
|
|
|
@Test
|
|
void deleteRecipeShouldThrowWhenNotFound() {
|
|
var id = UUID.randomUUID();
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(id, HOUSEHOLD_ID))
|
|
.thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> recipeService.deleteRecipe(HOUSEHOLD_ID, id))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void updateRecipeShouldThrowWhenNotFound() {
|
|
var id = UUID.randomUUID();
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(id, HOUSEHOLD_ID))
|
|
.thenReturn(Optional.empty());
|
|
|
|
var request = new RecipeCreateRequest(
|
|
"Updated", (short) 2, (short) 20, "easy", false, null,
|
|
List.of(), List.of(), List.of());
|
|
|
|
assertThatThrownBy(() -> recipeService.updateRecipe(HOUSEHOLD_ID, id, request))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
// ── Tag/Category edge cases ──
|
|
|
|
@Test
|
|
void createTagShouldThrowWhenHouseholdNotFound() {
|
|
when(tagRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "New")).thenReturn(false);
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> recipeService.createTag(HOUSEHOLD_ID, new TagCreateRequest("New", "other")))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void createCategoryShouldThrowWhenHouseholdNotFound() {
|
|
when(ingredientCategoryRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "New"))
|
|
.thenReturn(false);
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> recipeService.createCategory(
|
|
HOUSEHOLD_ID, new IngredientCategoryCreateRequest("New")))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void listTagsShouldReturnEmptyList() {
|
|
when(tagRepository.findByHouseholdId(HOUSEHOLD_ID)).thenReturn(List.of());
|
|
|
|
List<TagResponse> result = recipeService.listTags(HOUSEHOLD_ID);
|
|
|
|
assertThat(result).isEmpty();
|
|
}
|
|
}
|