test(shopping): add missing service tests for stale items, dedup, and household isolation
- generateFromPlan removes stale generated items - sourceRecipes deduplicates when same recipe appears in two slots - checkItem throws ResourceNotFoundException on household mismatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -468,6 +468,97 @@ class ShoppingServiceTest {
|
|||||||
.isInstanceOf(ResourceNotFoundException.class);
|
.isInstanceOf(ResourceNotFoundException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Generate removes stale items ──
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generateFromPlanShouldRemoveStaleGeneratedItems() {
|
||||||
|
var household = testHousehold();
|
||||||
|
var plan = testWeekPlan(household);
|
||||||
|
var existingList = testShoppingList(household, plan);
|
||||||
|
|
||||||
|
var tomato = testIngredient(household, "Tomatoes", false);
|
||||||
|
var onion = testIngredient(household, "Onions", false);
|
||||||
|
|
||||||
|
// Existing list has both tomatoes and onions (generated)
|
||||||
|
var tomatoItem = testItem(existingList, tomato, new BigDecimal("2.00"), "pcs");
|
||||||
|
tomatoItem.setSourceRecipes(new UUID[]{UUID.randomUUID()});
|
||||||
|
existingList.getItems().add(tomatoItem);
|
||||||
|
|
||||||
|
var onionItem = testItem(existingList, onion, new BigDecimal("1.00"), "pcs");
|
||||||
|
onionItem.setSourceRecipes(new UUID[]{UUID.randomUUID()});
|
||||||
|
existingList.getItems().add(onionItem);
|
||||||
|
|
||||||
|
// New plan only has tomatoes — onions removed from recipes
|
||||||
|
var recipe = testRecipe(household, "Sauce");
|
||||||
|
recipe.getIngredients().add(new RecipeIngredient(recipe, tomato, new BigDecimal("3.00"), "pcs", (short) 1));
|
||||||
|
var slot = new WeekPlanSlot(plan, recipe, WEEK_START);
|
||||||
|
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
||||||
|
plan.getSlots().add(slot);
|
||||||
|
|
||||||
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||||
|
when(shoppingListRepository.findByHouseholdIdAndWeekPlanWeekStart(HOUSEHOLD_ID, WEEK_START))
|
||||||
|
.thenReturn(Optional.of(existingList));
|
||||||
|
when(shoppingListRepository.save(any(ShoppingList.class))).thenAnswer(i -> i.getArgument(0));
|
||||||
|
when(recipeRepository.findAllById(any())).thenReturn(List.of(recipe));
|
||||||
|
|
||||||
|
ShoppingListResponse result = shoppingService.generateFromPlan(HOUSEHOLD_ID, plan.getId());
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
assertThat(result.items().getFirst().name()).isEqualTo("Tomatoes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Source recipes deduplication ──
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void generateFromPlanShouldDeduplicateSourceRecipesWhenSameRecipeInTwoSlots() {
|
||||||
|
var household = testHousehold();
|
||||||
|
var plan = testWeekPlan(household);
|
||||||
|
var recipe = testRecipe(household, "Pasta");
|
||||||
|
var tomato = testIngredient(household, "Tomatoes", false);
|
||||||
|
recipe.getIngredients().add(new RecipeIngredient(recipe, tomato, new BigDecimal("2.00"), "pcs", (short) 1));
|
||||||
|
|
||||||
|
// Same recipe in two slots
|
||||||
|
var slot1 = new WeekPlanSlot(plan, recipe, WEEK_START);
|
||||||
|
setId(slot1, WeekPlanSlot.class, UUID.randomUUID());
|
||||||
|
var slot2 = new WeekPlanSlot(plan, recipe, WEEK_START.plusDays(2));
|
||||||
|
setId(slot2, WeekPlanSlot.class, UUID.randomUUID());
|
||||||
|
plan.getSlots().add(slot1);
|
||||||
|
plan.getSlots().add(slot2);
|
||||||
|
|
||||||
|
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||||
|
when(shoppingListRepository.findByHouseholdIdAndWeekPlanWeekStart(HOUSEHOLD_ID, WEEK_START))
|
||||||
|
.thenReturn(Optional.empty());
|
||||||
|
when(shoppingListRepository.save(any(ShoppingList.class))).thenAnswer(i -> {
|
||||||
|
ShoppingList sl = i.getArgument(0);
|
||||||
|
if (sl.getId() == null) setId(sl, ShoppingList.class, UUID.randomUUID());
|
||||||
|
return sl;
|
||||||
|
});
|
||||||
|
when(recipeRepository.findAllById(any())).thenReturn(List.of(recipe));
|
||||||
|
|
||||||
|
ShoppingListResponse result = shoppingService.generateFromPlan(HOUSEHOLD_ID, plan.getId());
|
||||||
|
|
||||||
|
assertThat(result.items()).hasSize(1);
|
||||||
|
assertThat(result.items().getFirst().sourceRecipes()).hasSize(1); // deduplicated
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── checkItem household isolation ──
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkItemShouldThrowWhenHouseholdMismatch() {
|
||||||
|
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());
|
||||||
|
var list = new ShoppingList(otherHousehold, plan);
|
||||||
|
setId(list, ShoppingList.class, UUID.randomUUID());
|
||||||
|
|
||||||
|
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> shoppingService.checkItem(
|
||||||
|
HOUSEHOLD_ID, list.getId(), UUID.randomUUID(), new CheckItemRequest(true), UUID.randomUUID()))
|
||||||
|
.isInstanceOf(ResourceNotFoundException.class);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Generate from plan with empty slots ──
|
// ── Generate from plan with empty slots ──
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user