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>
187 lines
8.3 KiB
Java
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));
|
|
}
|
|
}
|