Files
mealprep/backend/src/test/java/com/recipeapp/planning/WeekPlanControllerTest.java
Marcel Raddatz 9ec703abcd Implement Recipe, Planning, Shopping, Pantry, and Admin domains
Outside-in TDD for all 5 remaining domains (128 tests total):
- Recipe: CRUD, ingredients autocomplete/patch, tags, categories (27 tests)
- Planning: week plans, slots, confirm, suggestions, variety score, cooking logs (24 tests)
- Shopping: generate from plan, publish, check/add/remove items (15 tests)
- Pantry: CRUD with expiry sorting (11 tests)
- Admin: user management, password reset, audit logging (13 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 21:56:51 +02:00

187 lines
8.3 KiB
Java

package com.recipeapp.planning;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.recipeapp.common.GlobalExceptionHandler;
import com.recipeapp.common.ValidationException;
import com.recipeapp.planning.dto.*;
import com.recipeapp.recipe.HouseholdResolver;
import org.junit.jupiter.api.BeforeEach;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(MockitoExtension.class)
class WeekPlanControllerTest {
private MockMvc mockMvc;
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
@Mock private PlanningService planningService;
@Mock private HouseholdResolver householdResolver;
@InjectMocks private WeekPlanController weekPlanController;
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
private static final UUID PLAN_ID = UUID.randomUUID();
private static final UUID SLOT_ID = UUID.randomUUID();
private static final LocalDate WEEK_START = LocalDate.of(2026, 4, 6);
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(weekPlanController)
.setControllerAdvice(new GlobalExceptionHandler())
.build();
}
@Test
void getWeekPlanShouldReturn200() throws Exception {
var plan = new WeekPlanResponse(PLAN_ID, WEEK_START, "draft", null, List.of());
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(planningService.getWeekPlan(HOUSEHOLD_ID, WEEK_START)).thenReturn(plan);
mockMvc.perform(get("/v1/week-plans")
.principal(() -> "sarah@example.com")
.param("weekStart", "2026-04-06"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.weekStart").value("2026-04-06"))
.andExpect(jsonPath("$.status").value("draft"));
}
@Test
void createWeekPlanShouldReturn201() throws Exception {
var plan = new WeekPlanResponse(PLAN_ID, WEEK_START, "draft", null, List.of());
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(planningService.createWeekPlan(HOUSEHOLD_ID, WEEK_START)).thenReturn(plan);
mockMvc.perform(post("/v1/week-plans")
.principal(() -> "sarah@example.com")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new CreateWeekPlanRequest(WEEK_START))))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.weekStart").value("2026-04-06"));
}
@Test
void addSlotShouldReturn201() throws Exception {
var recipeId = UUID.randomUUID();
var slotRecipe = new SlotResponse.SlotRecipe(recipeId, "Spaghetti", "medium", (short) 45, null);
var slot = new SlotResponse(SLOT_ID, WEEK_START.plusDays(1), slotRecipe);
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(planningService.addSlot(eq(HOUSEHOLD_ID), eq(PLAN_ID), any(CreateSlotRequest.class)))
.thenReturn(slot);
mockMvc.perform(post("/v1/week-plans/{id}/slots", PLAN_ID)
.principal(() -> "sarah@example.com")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(
new CreateSlotRequest(WEEK_START.plusDays(1), recipeId))))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.recipe.name").value("Spaghetti"));
}
@Test
void updateSlotShouldReturn200() throws Exception {
var recipeId = UUID.randomUUID();
var slotRecipe = new SlotResponse.SlotRecipe(recipeId, "Stir Fry", "easy", (short) 15, null);
var slot = new SlotResponse(SLOT_ID, WEEK_START.plusDays(1), slotRecipe);
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(planningService.updateSlot(eq(HOUSEHOLD_ID), eq(PLAN_ID), eq(SLOT_ID),
any(UpdateSlotRequest.class))).thenReturn(slot);
mockMvc.perform(patch("/v1/week-plans/{planId}/slots/{slotId}", PLAN_ID, SLOT_ID)
.principal(() -> "sarah@example.com")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new UpdateSlotRequest(recipeId))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.recipe.name").value("Stir Fry"));
}
@Test
void deleteSlotShouldReturn204() throws Exception {
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
doNothing().when(planningService).deleteSlot(HOUSEHOLD_ID, PLAN_ID, SLOT_ID);
mockMvc.perform(delete("/v1/week-plans/{planId}/slots/{slotId}", PLAN_ID, SLOT_ID)
.principal(() -> "sarah@example.com"))
.andExpect(status().isNoContent());
}
@Test
void confirmPlanShouldReturn200() throws Exception {
var plan = new WeekPlanResponse(PLAN_ID, WEEK_START, "confirmed", Instant.now(), List.of());
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(planningService.confirmPlan(HOUSEHOLD_ID, PLAN_ID)).thenReturn(plan);
mockMvc.perform(post("/v1/week-plans/{id}/confirm", PLAN_ID)
.principal(() -> "sarah@example.com"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("confirmed"));
}
@Test
void confirmPlanShouldReturn422WhenNoSlots() throws Exception {
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(planningService.confirmPlan(HOUSEHOLD_ID, PLAN_ID))
.thenThrow(new ValidationException("Plan has no slots"));
mockMvc.perform(post("/v1/week-plans/{id}/confirm", PLAN_ID)
.principal(() -> "sarah@example.com"))
.andExpect(status().isUnprocessableEntity());
}
@Test
void getSuggestionsShouldReturn200() throws Exception {
var recipe = new SlotResponse.SlotRecipe(UUID.randomUUID(), "Stir Fry", "easy", (short) 15, null);
var item = new SuggestionResponse.SuggestionItem(recipe,
List.of("not_cooked_recently"), List.of());
var response = new SuggestionResponse(List.of(item));
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(planningService.getSuggestions(HOUSEHOLD_ID, PLAN_ID, WEEK_START.plusDays(2)))
.thenReturn(response);
mockMvc.perform(get("/v1/week-plans/{id}/suggestions", PLAN_ID)
.principal(() -> "sarah@example.com")
.param("slotDate", "2026-04-08"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.suggestions[0].recipe.name").value("Stir Fry"))
.andExpect(jsonPath("$.suggestions[0].fitReasons[0]").value("not_cooked_recently"));
}
@Test
void getVarietyScoreShouldReturn200() throws Exception {
var response = new VarietyScoreResponse(7.5, List.of(), List.of(),
Map.of("easy", 2, "medium", 3, "hard", 2));
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(planningService.getVarietyScore(HOUSEHOLD_ID, PLAN_ID)).thenReturn(response);
mockMvc.perform(get("/v1/week-plans/{id}/variety-score", PLAN_ID)
.principal(() -> "sarah@example.com"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.score").value(7.5));
}
}