- Add @RequiresHouseholdRole("member") to GET /{planId}/variety-preview endpoint
to require household membership (was accessible to any authenticated user)
- Extract scoreFromSimulatedSlots() private method eliminating duplicate logic
between simulateVarietyScore() and the old computeCurrentScore()
- Fix loose variety preview test assertions (isBetween → exact assertEquals)
- Add test verifying negative scoreDelta when candidate is a duplicate recipe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
536 lines
22 KiB
Java
536 lines
22 KiB
Java
package com.recipeapp.planning;
|
|
|
|
import com.recipeapp.auth.UserAccountRepository;
|
|
import com.recipeapp.auth.entity.UserAccount;
|
|
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.planning.dto.*;
|
|
import com.recipeapp.planning.entity.*;
|
|
import com.recipeapp.recipe.RecipeRepository;
|
|
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 org.springframework.data.domain.PageRequest;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.time.LocalDate;
|
|
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 PlanningServiceTest {
|
|
|
|
@Mock private WeekPlanRepository weekPlanRepository;
|
|
@Mock private WeekPlanSlotRepository weekPlanSlotRepository;
|
|
@Mock private CookingLogRepository cookingLogRepository;
|
|
@Mock private RecipeRepository recipeRepository;
|
|
@Mock private HouseholdRepository householdRepository;
|
|
@Mock private UserAccountRepository userAccountRepository;
|
|
@Mock private VarietyScoreConfigRepository varietyScoreConfigRepository;
|
|
|
|
@InjectMocks private PlanningService planningService;
|
|
|
|
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
|
private static final LocalDate WEEK_START = LocalDate.of(2026, 4, 6); // Monday
|
|
|
|
private Household testHousehold() {
|
|
var h = new Household("Test family", null);
|
|
setId(h, Household.class, HOUSEHOLD_ID);
|
|
return h;
|
|
}
|
|
|
|
private WeekPlan testWeekPlan(Household household) {
|
|
var wp = new WeekPlan(household, WEEK_START);
|
|
setId(wp, WeekPlan.class, UUID.randomUUID());
|
|
return wp;
|
|
}
|
|
|
|
private Recipe testRecipe(Household household, String name) {
|
|
var r = new Recipe(household, name, (short) 4, (short) 45, "medium", true);
|
|
setId(r, Recipe.class, UUID.randomUUID());
|
|
return r;
|
|
}
|
|
|
|
private <T> void setId(T entity, Class<T> clazz, UUID id) {
|
|
try {
|
|
var field = clazz.getDeclaredField("id");
|
|
field.setAccessible(true);
|
|
field.set(entity, id);
|
|
} catch (Exception e) { throw new RuntimeException(e); }
|
|
}
|
|
|
|
// ── Week Plan CRUD ──
|
|
|
|
@Test
|
|
void getWeekPlanShouldReturnPlanWithSlots() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var recipe = testRecipe(household, "Spaghetti");
|
|
var slot = new WeekPlanSlot(plan, recipe, WEEK_START);
|
|
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
|
plan.getSlots().add(slot);
|
|
|
|
when(weekPlanRepository.findByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START))
|
|
.thenReturn(Optional.of(plan));
|
|
|
|
WeekPlanResponse result = planningService.getWeekPlan(HOUSEHOLD_ID, WEEK_START);
|
|
|
|
assertThat(result.weekStart()).isEqualTo(WEEK_START);
|
|
assertThat(result.slots()).hasSize(1);
|
|
assertThat(result.slots().getFirst().recipe().name()).isEqualTo("Spaghetti");
|
|
}
|
|
|
|
@Test
|
|
void getWeekPlanShouldThrowWhenNotFound() {
|
|
when(weekPlanRepository.findByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START))
|
|
.thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.getWeekPlan(HOUSEHOLD_ID, WEEK_START))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void createWeekPlanShouldPersist() {
|
|
var household = testHousehold();
|
|
|
|
when(weekPlanRepository.existsByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START)).thenReturn(false);
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(weekPlanRepository.save(any(WeekPlan.class))).thenAnswer(i -> {
|
|
WeekPlan wp = i.getArgument(0);
|
|
setId(wp, WeekPlan.class, UUID.randomUUID());
|
|
return wp;
|
|
});
|
|
|
|
WeekPlanResponse result = planningService.createWeekPlan(HOUSEHOLD_ID, WEEK_START);
|
|
|
|
assertThat(result.weekStart()).isEqualTo(WEEK_START);
|
|
assertThat(result.status()).isEqualTo("draft");
|
|
}
|
|
|
|
@Test
|
|
void createWeekPlanShouldThrowConflictWhenExists() {
|
|
when(weekPlanRepository.existsByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START)).thenReturn(true);
|
|
|
|
assertThatThrownBy(() -> planningService.createWeekPlan(HOUSEHOLD_ID, WEEK_START))
|
|
.isInstanceOf(ConflictException.class);
|
|
}
|
|
|
|
@Test
|
|
void createWeekPlanShouldThrowWhenNotMonday() {
|
|
var tuesday = LocalDate.of(2026, 4, 7);
|
|
|
|
assertThatThrownBy(() -> planningService.createWeekPlan(HOUSEHOLD_ID, tuesday))
|
|
.isInstanceOf(ValidationException.class);
|
|
}
|
|
|
|
// ── Slots ──
|
|
|
|
@Test
|
|
void addSlotShouldCreateSlot() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var recipe = testRecipe(household, "Spaghetti");
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipe.getId(), HOUSEHOLD_ID))
|
|
.thenReturn(Optional.of(recipe));
|
|
when(weekPlanSlotRepository.save(any(WeekPlanSlot.class))).thenAnswer(i -> {
|
|
WeekPlanSlot s = i.getArgument(0);
|
|
setId(s, WeekPlanSlot.class, UUID.randomUUID());
|
|
return s;
|
|
});
|
|
|
|
SlotResponse result = planningService.addSlot(HOUSEHOLD_ID, plan.getId(),
|
|
new CreateSlotRequest(WEEK_START.plusDays(1), recipe.getId()));
|
|
|
|
assertThat(result.recipe().name()).isEqualTo("Spaghetti");
|
|
}
|
|
|
|
@Test
|
|
void updateSlotShouldSwapRecipe() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var oldRecipe = testRecipe(household, "Spaghetti");
|
|
var newRecipe = testRecipe(household, "Stir Fry");
|
|
var slot = new WeekPlanSlot(plan, oldRecipe, WEEK_START);
|
|
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
|
plan.getSlots().add(slot);
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
when(weekPlanSlotRepository.findById(slot.getId())).thenReturn(Optional.of(slot));
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(newRecipe.getId(), HOUSEHOLD_ID))
|
|
.thenReturn(Optional.of(newRecipe));
|
|
|
|
SlotResponse result = planningService.updateSlot(HOUSEHOLD_ID, plan.getId(), slot.getId(),
|
|
new UpdateSlotRequest(newRecipe.getId()));
|
|
|
|
assertThat(result.recipe().name()).isEqualTo("Stir Fry");
|
|
}
|
|
|
|
@Test
|
|
void deleteSlotShouldRemoveSlot() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var slot = new WeekPlanSlot(plan, testRecipe(household, "X"), WEEK_START);
|
|
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
|
plan.getSlots().add(slot);
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
when(weekPlanSlotRepository.findById(slot.getId())).thenReturn(Optional.of(slot));
|
|
|
|
planningService.deleteSlot(HOUSEHOLD_ID, plan.getId(), slot.getId());
|
|
|
|
verify(weekPlanSlotRepository).delete(slot);
|
|
}
|
|
|
|
// ── Confirm ──
|
|
|
|
@Test
|
|
void confirmPlanShouldSetStatusAndTimestamp() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var slot = new WeekPlanSlot(plan, testRecipe(household, "X"), WEEK_START);
|
|
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
|
plan.getSlots().add(slot);
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
|
|
WeekPlanResponse result = planningService.confirmPlan(HOUSEHOLD_ID, plan.getId());
|
|
|
|
assertThat(result.status()).isEqualTo("confirmed");
|
|
assertThat(result.confirmedAt()).isNotNull();
|
|
}
|
|
|
|
@Test
|
|
void confirmPlanShouldThrowWhenNoSlots() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
|
|
assertThatThrownBy(() -> planningService.confirmPlan(HOUSEHOLD_ID, plan.getId()))
|
|
.isInstanceOf(ValidationException.class);
|
|
}
|
|
|
|
@Test
|
|
void confirmPlanShouldThrowWhenAlreadyConfirmed() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
plan.setStatus("confirmed");
|
|
var slot = new WeekPlanSlot(plan, testRecipe(household, "X"), WEEK_START);
|
|
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
|
plan.getSlots().add(slot);
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
|
|
assertThatThrownBy(() -> planningService.confirmPlan(HOUSEHOLD_ID, plan.getId()))
|
|
.isInstanceOf(ValidationException.class);
|
|
}
|
|
|
|
// ── Cooking Logs ──
|
|
|
|
@Test
|
|
void createCookingLogShouldPersist() {
|
|
var household = testHousehold();
|
|
var recipe = testRecipe(household, "Spaghetti");
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
|
setId(user, UserAccount.class, UUID.randomUUID());
|
|
|
|
when(recipeRepository.findById(recipe.getId())).thenReturn(Optional.of(recipe));
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(userAccountRepository.findById(user.getId())).thenReturn(Optional.of(user));
|
|
when(cookingLogRepository.save(any(CookingLog.class))).thenAnswer(i -> {
|
|
CookingLog cl = i.getArgument(0);
|
|
setId(cl, CookingLog.class, UUID.randomUUID());
|
|
return cl;
|
|
});
|
|
|
|
CookingLogResponse result = planningService.createCookingLog(HOUSEHOLD_ID, user.getId(),
|
|
new CreateCookingLogRequest(recipe.getId(), LocalDate.of(2026, 4, 7)));
|
|
|
|
assertThat(result.recipeName()).isEqualTo("Spaghetti");
|
|
assertThat(result.cookedOn()).isEqualTo(LocalDate.of(2026, 4, 7));
|
|
}
|
|
|
|
@Test
|
|
void listCookingLogsShouldReturnRecent() {
|
|
var household = testHousehold();
|
|
var recipe = testRecipe(household, "Spaghetti");
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
|
setId(user, UserAccount.class, UUID.randomUUID());
|
|
var log = new CookingLog(recipe, household, LocalDate.of(2026, 4, 7), user);
|
|
setId(log, CookingLog.class, UUID.randomUUID());
|
|
|
|
when(cookingLogRepository.findByHouseholdIdOrderByCookedOnDesc(eq(HOUSEHOLD_ID), any()))
|
|
.thenReturn(List.of(log));
|
|
|
|
List<CookingLogResponse> result = planningService.listCookingLogs(HOUSEHOLD_ID, 30, 0);
|
|
|
|
assertThat(result).hasSize(1);
|
|
assertThat(result.getFirst().recipeName()).isEqualTo("Spaghetti");
|
|
}
|
|
|
|
// ── Plan not found / household mismatch ──
|
|
|
|
@Test
|
|
void getWeekPlanShouldThrowWhenPlanBelongsToDifferentHousehold() {
|
|
var otherHousehold = new Household("Other family", null);
|
|
setId(otherHousehold, Household.class, UUID.randomUUID());
|
|
var plan = new WeekPlan(otherHousehold, WEEK_START);
|
|
setId(plan, WeekPlan.class, UUID.randomUUID());
|
|
|
|
// getWeekPlan uses findByHouseholdIdAndWeekStart so it won't find it for wrong household
|
|
when(weekPlanRepository.findByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START))
|
|
.thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.getWeekPlan(HOUSEHOLD_ID, WEEK_START))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void addSlotShouldThrowWhenPlanNotFound() {
|
|
var planId = UUID.randomUUID();
|
|
when(weekPlanRepository.findById(planId)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.addSlot(HOUSEHOLD_ID, planId,
|
|
new CreateSlotRequest(WEEK_START, UUID.randomUUID())))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void addSlotShouldThrowWhenPlanHouseholdMismatch() {
|
|
var otherHousehold = new Household("Other family", null);
|
|
setId(otherHousehold, Household.class, UUID.randomUUID());
|
|
var plan = new WeekPlan(otherHousehold, WEEK_START);
|
|
setId(plan, WeekPlan.class, UUID.randomUUID());
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
|
|
assertThatThrownBy(() -> planningService.addSlot(HOUSEHOLD_ID, plan.getId(),
|
|
new CreateSlotRequest(WEEK_START, UUID.randomUUID())))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void addSlotShouldThrowWhenRecipeNotFound() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var recipeId = UUID.randomUUID();
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipeId, HOUSEHOLD_ID))
|
|
.thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.addSlot(HOUSEHOLD_ID, plan.getId(),
|
|
new CreateSlotRequest(WEEK_START, recipeId)))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void updateSlotShouldThrowWhenSlotNotFound() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var slotId = UUID.randomUUID();
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
when(weekPlanSlotRepository.findById(slotId)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.updateSlot(HOUSEHOLD_ID, plan.getId(), slotId,
|
|
new UpdateSlotRequest(UUID.randomUUID())))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void deleteSlotShouldThrowWhenSlotNotFound() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var slotId = UUID.randomUUID();
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
when(weekPlanSlotRepository.findById(slotId)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.deleteSlot(HOUSEHOLD_ID, plan.getId(), slotId))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void confirmPlanShouldThrowWhenPlanNotFound() {
|
|
var planId = UUID.randomUUID();
|
|
when(weekPlanRepository.findById(planId)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.confirmPlan(HOUSEHOLD_ID, planId))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
// ── Cooking log edge cases ──
|
|
|
|
@Test
|
|
void createCookingLogShouldDefaultToTodayWhenCookedOnNull() {
|
|
var household = testHousehold();
|
|
var recipe = testRecipe(household, "Spaghetti");
|
|
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
|
setId(user, UserAccount.class, UUID.randomUUID());
|
|
|
|
when(recipeRepository.findById(recipe.getId())).thenReturn(Optional.of(recipe));
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(userAccountRepository.findById(user.getId())).thenReturn(Optional.of(user));
|
|
when(cookingLogRepository.save(any(CookingLog.class))).thenAnswer(i -> {
|
|
CookingLog cl = i.getArgument(0);
|
|
setId(cl, CookingLog.class, UUID.randomUUID());
|
|
return cl;
|
|
});
|
|
|
|
CookingLogResponse result = planningService.createCookingLog(HOUSEHOLD_ID, user.getId(),
|
|
new CreateCookingLogRequest(recipe.getId(), null));
|
|
|
|
assertThat(result.cookedOn()).isEqualTo(LocalDate.now());
|
|
}
|
|
|
|
@Test
|
|
void createCookingLogShouldThrowWhenRecipeNotFound() {
|
|
var recipeId = UUID.randomUUID();
|
|
when(recipeRepository.findById(recipeId)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.createCookingLog(HOUSEHOLD_ID, UUID.randomUUID(),
|
|
new CreateCookingLogRequest(recipeId, LocalDate.now())))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void createCookingLogShouldThrowWhenHouseholdNotFound() {
|
|
var household = testHousehold();
|
|
var recipe = testRecipe(household, "Spaghetti");
|
|
|
|
when(recipeRepository.findById(recipe.getId())).thenReturn(Optional.of(recipe));
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.createCookingLog(HOUSEHOLD_ID, UUID.randomUUID(),
|
|
new CreateCookingLogRequest(recipe.getId(), LocalDate.now())))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void createCookingLogShouldThrowWhenUserNotFound() {
|
|
var household = testHousehold();
|
|
var recipe = testRecipe(household, "Spaghetti");
|
|
var userId = UUID.randomUUID();
|
|
|
|
when(recipeRepository.findById(recipe.getId())).thenReturn(Optional.of(recipe));
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
|
when(userAccountRepository.findById(userId)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.createCookingLog(HOUSEHOLD_ID, userId,
|
|
new CreateCookingLogRequest(recipe.getId(), LocalDate.now())))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
// ── Create week plan edge cases ──
|
|
|
|
@Test
|
|
void createWeekPlanShouldThrowWhenHouseholdNotFound() {
|
|
when(weekPlanRepository.existsByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START)).thenReturn(false);
|
|
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.createWeekPlan(HOUSEHOLD_ID, WEEK_START))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
// ── Variety preview ──
|
|
|
|
@Test
|
|
void getVarietyPreviewShouldReturnScoreDeltaForDifferentRecipe() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var planId = plan.getId();
|
|
|
|
// Plan already has one slot (Mon) with Spaghetti
|
|
var existingRecipe = testRecipe(household, "Spaghetti");
|
|
var slot = new WeekPlanSlot(plan, existingRecipe, WEEK_START);
|
|
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
|
plan.getSlots().add(slot);
|
|
|
|
// Candidate is Lachsfilet (different recipe, no shared tags/ingredients)
|
|
var candidate = testRecipe(household, "Lachsfilet");
|
|
var candidateId = candidate.getId();
|
|
|
|
when(weekPlanRepository.findById(planId)).thenReturn(Optional.of(plan));
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(candidateId, HOUSEHOLD_ID))
|
|
.thenReturn(Optional.of(candidate));
|
|
when(varietyScoreConfigRepository.findByHouseholdId(HOUSEHOLD_ID)).thenReturn(Optional.empty());
|
|
when(cookingLogRepository.findByHouseholdIdAndCookedOnAfter(eq(HOUSEHOLD_ID), any()))
|
|
.thenReturn(List.of());
|
|
|
|
var result = planningService.getVarietyPreview(HOUSEHOLD_ID, planId, candidateId, WEEK_START.plusDays(1));
|
|
|
|
// 1 existing slot with no conflicts → currentScore = 10.0
|
|
// Adding a different recipe with no tags/ingredients → projectedScore = 10.0, delta = 0
|
|
assertThat(result.currentScore()).isEqualTo(10.0);
|
|
assertThat(result.projectedScore()).isEqualTo(10.0);
|
|
assertThat(result.scoreDelta()).isEqualTo(0.0);
|
|
}
|
|
|
|
@Test
|
|
void getVarietyPreviewShouldReturnNegativeDeltaForDuplicateRecipe() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var planId = plan.getId();
|
|
|
|
// Plan already has Spaghetti on Mon
|
|
var existingRecipe = testRecipe(household, "Spaghetti");
|
|
var slot = new WeekPlanSlot(plan, existingRecipe, WEEK_START);
|
|
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
|
plan.getSlots().add(slot);
|
|
|
|
// Candidate is the same Spaghetti recipe → triggers duplicate penalty (wPlanDuplicate = 2.0)
|
|
when(weekPlanRepository.findById(planId)).thenReturn(Optional.of(plan));
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(existingRecipe.getId(), HOUSEHOLD_ID))
|
|
.thenReturn(Optional.of(existingRecipe));
|
|
when(varietyScoreConfigRepository.findByHouseholdId(HOUSEHOLD_ID)).thenReturn(Optional.empty());
|
|
when(cookingLogRepository.findByHouseholdIdAndCookedOnAfter(eq(HOUSEHOLD_ID), any()))
|
|
.thenReturn(List.of());
|
|
|
|
var result = planningService.getVarietyPreview(
|
|
HOUSEHOLD_ID, planId, existingRecipe.getId(), WEEK_START.plusDays(1));
|
|
|
|
// currentScore = 10.0 (1 slot, no conflicts)
|
|
// projectedScore = 10.0 - 1 * 2.0 (duplicate penalty) = 8.0
|
|
assertThat(result.currentScore()).isEqualTo(10.0);
|
|
assertThat(result.projectedScore()).isEqualTo(8.0);
|
|
assertThat(result.scoreDelta()).isEqualTo(-2.0);
|
|
}
|
|
|
|
@Test
|
|
void getVarietyPreviewShouldThrowWhenPlanNotFound() {
|
|
var planId = UUID.randomUUID();
|
|
when(weekPlanRepository.findById(planId)).thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.getVarietyPreview(
|
|
HOUSEHOLD_ID, planId, UUID.randomUUID(), WEEK_START))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
|
|
@Test
|
|
void getVarietyPreviewShouldThrowWhenRecipeNotFound() {
|
|
var household = testHousehold();
|
|
var plan = testWeekPlan(household);
|
|
var recipeId = UUID.randomUUID();
|
|
|
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
|
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipeId, HOUSEHOLD_ID))
|
|
.thenReturn(Optional.empty());
|
|
|
|
assertThatThrownBy(() -> planningService.getVarietyPreview(
|
|
HOUSEHOLD_ID, plan.getId(), recipeId, WEEK_START))
|
|
.isInstanceOf(ResourceNotFoundException.class);
|
|
}
|
|
}
|