fix(backend): add role guard to variety-preview and extract shared scoring method
- 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>
This commit is contained in:
@@ -447,34 +447,65 @@ class PlanningServiceTest {
|
||||
// ── Variety preview ──
|
||||
|
||||
@Test
|
||||
void getVarietyPreviewShouldReturnScoreDelta() {
|
||||
void getVarietyPreviewShouldReturnScoreDeltaForDifferentRecipe() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var planId = plan.getId();
|
||||
|
||||
// Plan already has one slot (Mon)
|
||||
// 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(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));
|
||||
|
||||
// With no penalties, projected score should be 10.0; current (1 slot, no conflicts) is also 10.0
|
||||
assertThat(result.projectedScore()).isBetween(0.0, 10.0);
|
||||
assertThat(result.scoreDelta()).isEqualTo(result.projectedScore() - result.currentScore());
|
||||
assertThat(result.currentScore()).isBetween(0.0, 10.0);
|
||||
// 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
|
||||
|
||||
@@ -265,4 +265,5 @@ class WeekPlanControllerTest {
|
||||
.principal(() -> "member@example.com"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user