V025 migration drops the column. Removed from Recipe entity, RecipeDetailResponse, RecipeSummaryResponse, RecipeRepository JPQL, RecipeService, and RecipeController. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
8.0 KiB
Java
184 lines
8.0 KiB
Java
package com.recipeapp.recipe;
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.recipeapp.common.GlobalExceptionHandler;
|
|
import com.recipeapp.common.ResourceNotFoundException;
|
|
import com.recipeapp.recipe.dto.*;
|
|
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.math.BigDecimal;
|
|
import java.util.List;
|
|
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 RecipeControllerTest {
|
|
|
|
private MockMvc mockMvc;
|
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
@Mock private RecipeService recipeService;
|
|
@Mock private HouseholdResolver householdResolver;
|
|
|
|
@InjectMocks private RecipeController recipeController;
|
|
|
|
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
|
private static final UUID RECIPE_ID = UUID.randomUUID();
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
mockMvc = MockMvcBuilders.standaloneSetup(recipeController)
|
|
.setControllerAdvice(new GlobalExceptionHandler())
|
|
.build();
|
|
}
|
|
|
|
@Test
|
|
void listRecipesShouldReturn200WithPagination() throws Exception {
|
|
var summary = new RecipeSummaryResponse(RECIPE_ID, "Spaghetti Bolognese",
|
|
(short) 4, (short) 45, "medium", null);
|
|
|
|
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
|
when(recipeService.listRecipes(eq(HOUSEHOLD_ID), isNull(), isNull(), isNull(),
|
|
isNull(), eq(20), eq(0)))
|
|
.thenReturn(List.of(summary));
|
|
when(recipeService.countRecipes(eq(HOUSEHOLD_ID), isNull(), isNull(), isNull()))
|
|
.thenReturn(1L);
|
|
|
|
mockMvc.perform(get("/v1/recipes")
|
|
.principal(() -> "sarah@example.com")
|
|
.param("limit", "20")
|
|
.param("offset", "0"))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data[0].name").value("Spaghetti Bolognese"))
|
|
.andExpect(jsonPath("$.meta.pagination.total").value(1))
|
|
.andExpect(jsonPath("$.meta.pagination.hasMore").value(false));
|
|
}
|
|
|
|
@Test
|
|
void listRecipesWithFiltersShouldPassParams() throws Exception {
|
|
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
|
when(recipeService.listRecipes(eq(HOUSEHOLD_ID), eq("pasta"), eq("easy"),
|
|
eq(30), eq("-cookTimeMin"), eq(10), eq(5)))
|
|
.thenReturn(List.of());
|
|
when(recipeService.countRecipes(eq(HOUSEHOLD_ID), eq("pasta"), eq("easy"), eq(30)))
|
|
.thenReturn(0L);
|
|
|
|
mockMvc.perform(get("/v1/recipes")
|
|
.principal(() -> "sarah@example.com")
|
|
.param("search", "pasta")
|
|
.param("effort", "easy")
|
|
.param("cookTimeMin.lte", "30")
|
|
.param("sort", "-cookTimeMin")
|
|
.param("limit", "10")
|
|
.param("offset", "5"))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.data").isArray());
|
|
}
|
|
|
|
@Test
|
|
void getRecipeShouldReturn200WithDetail() throws Exception {
|
|
var detail = sampleDetail();
|
|
|
|
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
|
when(recipeService.getRecipe(HOUSEHOLD_ID, RECIPE_ID)).thenReturn(detail);
|
|
|
|
mockMvc.perform(get("/v1/recipes/{id}", RECIPE_ID)
|
|
.principal(() -> "sarah@example.com"))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.name").value("Spaghetti Bolognese"))
|
|
.andExpect(jsonPath("$.ingredients[0].name").value("spaghetti"))
|
|
.andExpect(jsonPath("$.steps[0].instruction").value("Boil water."))
|
|
.andExpect(jsonPath("$.tags[0].name").value("beef"));
|
|
}
|
|
|
|
@Test
|
|
void getRecipeShouldReturn404WhenNotFound() throws Exception {
|
|
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
|
when(recipeService.getRecipe(HOUSEHOLD_ID, RECIPE_ID))
|
|
.thenThrow(new ResourceNotFoundException("Recipe not found"));
|
|
|
|
mockMvc.perform(get("/v1/recipes/{id}", RECIPE_ID)
|
|
.principal(() -> "sarah@example.com"))
|
|
.andExpect(status().isNotFound());
|
|
}
|
|
|
|
@Test
|
|
void createRecipeShouldReturn201() throws Exception {
|
|
var request = sampleCreateRequest();
|
|
var detail = sampleDetail();
|
|
|
|
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
|
when(recipeService.createRecipe(eq(HOUSEHOLD_ID), any(RecipeCreateRequest.class)))
|
|
.thenReturn(detail);
|
|
|
|
mockMvc.perform(post("/v1/recipes")
|
|
.principal(() -> "sarah@example.com")
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(objectMapper.writeValueAsString(request)))
|
|
.andExpect(status().isCreated())
|
|
.andExpect(jsonPath("$.name").value("Spaghetti Bolognese"))
|
|
.andExpect(header().string("Location", "/v1/recipes/" + RECIPE_ID));
|
|
}
|
|
|
|
@Test
|
|
void updateRecipeShouldReturn200() throws Exception {
|
|
var request = sampleCreateRequest();
|
|
var detail = sampleDetail();
|
|
|
|
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
|
when(recipeService.updateRecipe(eq(HOUSEHOLD_ID), eq(RECIPE_ID), any(RecipeCreateRequest.class)))
|
|
.thenReturn(detail);
|
|
|
|
mockMvc.perform(put("/v1/recipes/{id}", RECIPE_ID)
|
|
.principal(() -> "sarah@example.com")
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.content(objectMapper.writeValueAsString(request)))
|
|
.andExpect(status().isOk())
|
|
.andExpect(jsonPath("$.name").value("Spaghetti Bolognese"));
|
|
}
|
|
|
|
@Test
|
|
void deleteRecipeShouldReturn204() throws Exception {
|
|
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
|
doNothing().when(recipeService).deleteRecipe(HOUSEHOLD_ID, RECIPE_ID);
|
|
|
|
mockMvc.perform(delete("/v1/recipes/{id}", RECIPE_ID)
|
|
.principal(() -> "sarah@example.com"))
|
|
.andExpect(status().isNoContent());
|
|
|
|
verify(recipeService).deleteRecipe(HOUSEHOLD_ID, RECIPE_ID);
|
|
}
|
|
|
|
private RecipeCreateRequest sampleCreateRequest() {
|
|
var ingredientId = UUID.randomUUID();
|
|
return new RecipeCreateRequest(
|
|
"Spaghetti Bolognese", 4, 45, "medium", null,
|
|
List.of(new RecipeCreateRequest.IngredientEntry(
|
|
ingredientId, null, new BigDecimal("400"), "g", (short) 1)),
|
|
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Boil water.")),
|
|
List.of(UUID.randomUUID(), UUID.randomUUID()));
|
|
}
|
|
|
|
private RecipeDetailResponse sampleDetail() {
|
|
var catRef = new RecipeDetailResponse.CategoryRef(UUID.randomUUID(), "pasta");
|
|
return new RecipeDetailResponse(
|
|
RECIPE_ID, "Spaghetti Bolognese", (short) 4, (short) 45, "medium", null,
|
|
List.of(new RecipeDetailResponse.IngredientItem(
|
|
UUID.randomUUID(), "spaghetti", catRef, new BigDecimal("400"), "g", (short) 1)),
|
|
List.of(new RecipeDetailResponse.StepItem((short) 1, "Boil water.")),
|
|
List.of(new RecipeDetailResponse.TagItem(UUID.randomUUID(), "beef", "protein")));
|
|
}
|
|
}
|