From eb5ee1ab5ad376e1e38dcead54c6b14b87f87142 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Mon, 6 Apr 2026 19:50:27 +0200 Subject: [PATCH] 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 --- .../shopping/ShoppingServiceTest.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/backend/src/test/java/com/recipeapp/shopping/ShoppingServiceTest.java b/backend/src/test/java/com/recipeapp/shopping/ShoppingServiceTest.java index 6be71dc..d5d1f0c 100644 --- a/backend/src/test/java/com/recipeapp/shopping/ShoppingServiceTest.java +++ b/backend/src/test/java/com/recipeapp/shopping/ShoppingServiceTest.java @@ -468,6 +468,97 @@ class ShoppingServiceTest { .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 ── @Test