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>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package com.recipeapp.admin;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.recipeapp.admin.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.security.Principal;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdminControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Mock
|
||||
private AdminService adminService;
|
||||
|
||||
@InjectMocks
|
||||
private AdminController adminController;
|
||||
|
||||
private final String adminEmail = "admin@example.com";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(adminController).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listUsers_returnsPagedUsers() throws Exception {
|
||||
var user = new AdminUserResponse(UUID.randomUUID(), "jane@example.com", "Jane", "user", true, Instant.now());
|
||||
when(adminService.listUsers(isNull(), isNull(), eq(50), eq(0)))
|
||||
.thenReturn(new AdminService.ListUsersResult(List.of(user), 1));
|
||||
|
||||
mockMvc.perform(get("/v1/admin/users")
|
||||
.principal(() -> adminEmail))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("success"))
|
||||
.andExpect(jsonPath("$.data[0].email").value("jane@example.com"))
|
||||
.andExpect(jsonPath("$.meta.pagination.total").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUser_returns201() throws Exception {
|
||||
var request = new CreateUserRequest("new@example.com", "New User", "TempPass1!", "user");
|
||||
var response = new AdminUserResponse(UUID.randomUUID(), "new@example.com", "New User", "user", true, Instant.now());
|
||||
when(adminService.createUser(any(CreateUserRequest.class), eq(adminEmail)))
|
||||
.thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/v1/admin/users")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request))
|
||||
.principal(() -> adminEmail))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.data.email").value("new@example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUser_returns200() throws Exception {
|
||||
UUID userId = UUID.randomUUID();
|
||||
var request = new UpdateUserRequest("Updated Name", null, null, null);
|
||||
var response = new AdminUserResponse(userId, "jane@example.com", "Updated Name", "user", true, Instant.now());
|
||||
when(adminService.updateUser(eq(userId), any(UpdateUserRequest.class), eq(adminEmail)))
|
||||
.thenReturn(response);
|
||||
|
||||
mockMvc.perform(patch("/v1/admin/users/{id}", userId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request))
|
||||
.principal(() -> adminEmail))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.displayName").value("Updated Name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetPassword_returns200() throws Exception {
|
||||
UUID userId = UUID.randomUUID();
|
||||
var request = new ResetPasswordRequest("NewTemp123!", "User forgot password");
|
||||
var response = new ResetPasswordResponse("Password reset successfully", true);
|
||||
when(adminService.resetPassword(eq(userId), any(ResetPasswordRequest.class), eq(adminEmail)))
|
||||
.thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/v1/admin/users/{id}/reset-password", userId)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request))
|
||||
.principal(() -> adminEmail))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.message").value("Password reset successfully"))
|
||||
.andExpect(jsonPath("$.data.mustChangePassword").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAuditLog_returnsLogs() throws Exception {
|
||||
var log = new AuditLogResponse(UUID.randomUUID(), UUID.randomUUID(), "admin@example.com",
|
||||
UUID.randomUUID(), "jane@example.com", "create_account", Map.of(), Instant.now());
|
||||
when(adminService.listAuditLog(isNull(), eq(50), eq(0)))
|
||||
.thenReturn(List.of(log));
|
||||
|
||||
mockMvc.perform(get("/v1/admin/audit-log")
|
||||
.principal(() -> adminEmail))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data[0].action").value("create_account"));
|
||||
}
|
||||
}
|
||||
170
backend/src/test/java/com/recipeapp/admin/AdminServiceTest.java
Normal file
170
backend/src/test/java/com/recipeapp/admin/AdminServiceTest.java
Normal file
@@ -0,0 +1,170 @@
|
||||
package com.recipeapp.admin;
|
||||
|
||||
import com.recipeapp.admin.dto.*;
|
||||
import com.recipeapp.admin.entity.AdminAuditLog;
|
||||
import com.recipeapp.auth.UserAccountRepository;
|
||||
import com.recipeapp.auth.entity.UserAccount;
|
||||
import com.recipeapp.common.ConflictException;
|
||||
import com.recipeapp.common.ResourceNotFoundException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AdminServiceTest {
|
||||
|
||||
@Mock private UserAccountRepository userAccountRepository;
|
||||
@Mock private AdminAuditLogRepository auditLogRepository;
|
||||
@Mock private AdminUserQueryRepository adminUserQueryRepository;
|
||||
@Mock private PasswordEncoder passwordEncoder;
|
||||
|
||||
private AdminServiceImpl adminService;
|
||||
|
||||
private final String adminEmail = "admin@example.com";
|
||||
private UserAccount adminUser;
|
||||
private UserAccount targetUser;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
adminService = new AdminServiceImpl(userAccountRepository, auditLogRepository, adminUserQueryRepository, passwordEncoder);
|
||||
adminUser = new UserAccount("admin@example.com", "Admin", "hashed");
|
||||
setId(adminUser, UserAccount.class, UUID.randomUUID());
|
||||
targetUser = new UserAccount("jane@example.com", "Jane", "hashed");
|
||||
setId(targetUser, UserAccount.class, UUID.randomUUID());
|
||||
}
|
||||
|
||||
private <T> void setId(T entity, Class<T> clazz, UUID id) {
|
||||
try {
|
||||
var field = clazz.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(entity, id);
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
}
|
||||
|
||||
@Test
|
||||
void listUsers_returnsPaginatedResults() {
|
||||
when(adminUserQueryRepository.findUsersFiltered(isNull(), isNull(), any(Pageable.class)))
|
||||
.thenReturn(List.of(targetUser));
|
||||
when(adminUserQueryRepository.countUsersFiltered(isNull(), isNull()))
|
||||
.thenReturn(1L);
|
||||
|
||||
var result = adminService.listUsers(null, null, 50, 0);
|
||||
|
||||
assertEquals(1, result.users().size());
|
||||
assertEquals(1L, result.total());
|
||||
assertEquals("jane@example.com", result.users().getFirst().email());
|
||||
}
|
||||
|
||||
@Test
|
||||
void listUsers_withSearchFilter() {
|
||||
when(adminUserQueryRepository.findUsersFiltered(eq("jane"), isNull(), any(Pageable.class)))
|
||||
.thenReturn(List.of(targetUser));
|
||||
when(adminUserQueryRepository.countUsersFiltered(eq("jane"), isNull()))
|
||||
.thenReturn(1L);
|
||||
|
||||
var result = adminService.listUsers("jane", null, 50, 0);
|
||||
|
||||
assertEquals(1, result.users().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUser_success() {
|
||||
when(userAccountRepository.existsByEmailIgnoreCase("new@example.com")).thenReturn(false);
|
||||
when(userAccountRepository.findByEmailIgnoreCase(adminEmail)).thenReturn(Optional.of(adminUser));
|
||||
when(passwordEncoder.encode("TempPass1!")).thenReturn("encoded");
|
||||
when(userAccountRepository.save(any(UserAccount.class))).thenAnswer(inv -> {
|
||||
var u = inv.getArgument(0, UserAccount.class);
|
||||
setId(u, UserAccount.class, UUID.randomUUID());
|
||||
return u;
|
||||
});
|
||||
when(auditLogRepository.save(any(AdminAuditLog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = adminService.createUser(
|
||||
new CreateUserRequest("new@example.com", "New User", "TempPass1!", "user"), adminEmail);
|
||||
|
||||
assertEquals("new@example.com", result.email());
|
||||
assertEquals("New User", result.displayName());
|
||||
verify(auditLogRepository).save(argThat(log -> "create_account".equals(log.getAction())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createUser_duplicateEmail_throwsConflict() {
|
||||
when(userAccountRepository.existsByEmailIgnoreCase("jane@example.com")).thenReturn(true);
|
||||
|
||||
assertThrows(ConflictException.class, () ->
|
||||
adminService.createUser(
|
||||
new CreateUserRequest("jane@example.com", "Jane", "TempPass1!", "user"), adminEmail));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUser_success() {
|
||||
when(userAccountRepository.findByEmailIgnoreCase(adminEmail)).thenReturn(Optional.of(adminUser));
|
||||
when(userAccountRepository.findById(targetUser.getId())).thenReturn(Optional.of(targetUser));
|
||||
when(userAccountRepository.save(any(UserAccount.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
when(auditLogRepository.save(any(AdminAuditLog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = adminService.updateUser(targetUser.getId(),
|
||||
new UpdateUserRequest("Updated Jane", null, null, null), adminEmail);
|
||||
|
||||
assertEquals("Updated Jane", result.displayName());
|
||||
verify(auditLogRepository).save(argThat(log -> "update_account".equals(log.getAction())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUser_deactivate() {
|
||||
when(userAccountRepository.findByEmailIgnoreCase(adminEmail)).thenReturn(Optional.of(adminUser));
|
||||
when(userAccountRepository.findById(targetUser.getId())).thenReturn(Optional.of(targetUser));
|
||||
when(userAccountRepository.save(any(UserAccount.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
when(auditLogRepository.save(any(AdminAuditLog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = adminService.updateUser(targetUser.getId(),
|
||||
new UpdateUserRequest(null, null, null, false), adminEmail);
|
||||
|
||||
assertFalse(result.isActive());
|
||||
verify(auditLogRepository).save(argThat(log -> "deactivate_account".equals(log.getAction())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetPassword_success() {
|
||||
when(userAccountRepository.findByEmailIgnoreCase(adminEmail)).thenReturn(Optional.of(adminUser));
|
||||
when(userAccountRepository.findById(targetUser.getId())).thenReturn(Optional.of(targetUser));
|
||||
when(passwordEncoder.encode("NewTemp123!")).thenReturn("encoded");
|
||||
when(userAccountRepository.save(any(UserAccount.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
when(auditLogRepository.save(any(AdminAuditLog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
var result = adminService.resetPassword(targetUser.getId(),
|
||||
new ResetPasswordRequest("NewTemp123!", "Forgot password"), adminEmail);
|
||||
|
||||
assertEquals("Password reset successfully", result.message());
|
||||
assertTrue(result.mustChangePassword());
|
||||
verify(auditLogRepository).save(argThat(log -> "reset_password".equals(log.getAction())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listAuditLog_returnsLogs() {
|
||||
var log = new AdminAuditLog(adminUser.getId(), targetUser.getId(), "create_account", Map.of(), null);
|
||||
setId(log, AdminAuditLog.class, UUID.randomUUID());
|
||||
when(auditLogRepository.findAllByOrderByPerformedAtDesc(any(Pageable.class)))
|
||||
.thenReturn(List.of(log));
|
||||
when(userAccountRepository.findById(adminUser.getId())).thenReturn(Optional.of(adminUser));
|
||||
when(userAccountRepository.findById(targetUser.getId())).thenReturn(Optional.of(targetUser));
|
||||
|
||||
var result = adminService.listAuditLog(null, 50, 0);
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("create_account", result.getFirst().action());
|
||||
assertEquals("admin@example.com", result.getFirst().adminEmail());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.recipeapp.pantry;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.recipeapp.common.GlobalExceptionHandler;
|
||||
import com.recipeapp.pantry.dto.*;
|
||||
import com.recipeapp.recipe.HouseholdResolver;
|
||||
import com.recipeapp.recipe.dto.RecipeDetailResponse.CategoryRef;
|
||||
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.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PantryControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
|
||||
@Mock private PantryService pantryService;
|
||||
@Mock private HouseholdResolver householdResolver;
|
||||
|
||||
@InjectMocks private PantryController pantryController;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(pantryController)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listItemsShouldReturn200() throws Exception {
|
||||
var categoryRef = new CategoryRef(UUID.randomUUID(), "Dairy");
|
||||
var item = new PantryItemResponse(UUID.randomUUID(), UUID.randomUUID(), "Milk", categoryRef,
|
||||
new BigDecimal("2.00"), "liters", LocalDate.of(2026, 4, 10), null);
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(pantryService.listItems(HOUSEHOLD_ID)).thenReturn(List.of(item));
|
||||
|
||||
mockMvc.perform(get("/v1/pantry-items")
|
||||
.principal(() -> "sarah@example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].name").value("Milk"))
|
||||
.andExpect(jsonPath("$[0].category.name").value("Dairy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createItemShouldReturn201() throws Exception {
|
||||
var request = new CreatePantryItemRequest(UUID.randomUUID(), null,
|
||||
new BigDecimal("1.50"), "kg", LocalDate.of(2026, 4, 15), null);
|
||||
var response = new PantryItemResponse(UUID.randomUUID(), request.ingredientId(), "Chicken", null,
|
||||
request.quantity(), request.unit(), request.bestBefore(), null);
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(pantryService.createItem(eq(HOUSEHOLD_ID), any(CreatePantryItemRequest.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/v1/pantry-items")
|
||||
.principal(() -> "sarah@example.com")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.name").value("Chicken"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateItemShouldReturn200() throws Exception {
|
||||
var itemId = UUID.randomUUID();
|
||||
var request = new UpdatePantryItemRequest(new BigDecimal("0.50"), null, null, null);
|
||||
var response = new PantryItemResponse(itemId, UUID.randomUUID(), "Milk", null,
|
||||
new BigDecimal("0.50"), "liters", LocalDate.of(2026, 4, 10), null);
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(pantryService.updateItem(eq(HOUSEHOLD_ID), eq(itemId), any(UpdatePantryItemRequest.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
mockMvc.perform(patch("/v1/pantry-items/{id}", itemId)
|
||||
.principal(() -> "sarah@example.com")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.quantity").value(0.50));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteItemShouldReturn204() throws Exception {
|
||||
var itemId = UUID.randomUUID();
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
|
||||
mockMvc.perform(delete("/v1/pantry-items/{id}", itemId)
|
||||
.principal(() -> "sarah@example.com"))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
verify(pantryService).deleteItem(HOUSEHOLD_ID, itemId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.recipeapp.pantry;
|
||||
|
||||
import com.recipeapp.common.ResourceNotFoundException;
|
||||
import com.recipeapp.common.ValidationException;
|
||||
import com.recipeapp.household.HouseholdRepository;
|
||||
import com.recipeapp.household.entity.Household;
|
||||
import com.recipeapp.pantry.dto.*;
|
||||
import com.recipeapp.pantry.entity.PantryItem;
|
||||
import com.recipeapp.recipe.IngredientRepository;
|
||||
import com.recipeapp.recipe.entity.Ingredient;
|
||||
import com.recipeapp.recipe.entity.IngredientCategory;
|
||||
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 java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PantryServiceTest {
|
||||
|
||||
@Mock private PantryItemRepository pantryItemRepository;
|
||||
@Mock private HouseholdRepository householdRepository;
|
||||
@Mock private IngredientRepository ingredientRepository;
|
||||
|
||||
@InjectMocks private PantryServiceImpl pantryService;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
|
||||
private Household testHousehold() {
|
||||
var h = new Household("Test family", null);
|
||||
setId(h, Household.class, HOUSEHOLD_ID);
|
||||
return h;
|
||||
}
|
||||
|
||||
private Ingredient testIngredient(Household household, String name) {
|
||||
var i = new Ingredient(household, name, false);
|
||||
setId(i, Ingredient.class, UUID.randomUUID());
|
||||
var cat = new IngredientCategory(household, "Dairy", (short) 1);
|
||||
setId(cat, IngredientCategory.class, UUID.randomUUID());
|
||||
i.setCategory(cat);
|
||||
return i;
|
||||
}
|
||||
|
||||
private PantryItem testPantryItem(Household household, Ingredient ingredient) {
|
||||
var item = new PantryItem(household, ingredient, null,
|
||||
new BigDecimal("2.00"), "liters", LocalDate.of(2026, 4, 10), null);
|
||||
setId(item, PantryItem.class, UUID.randomUUID());
|
||||
return item;
|
||||
}
|
||||
|
||||
private <T> void setId(T entity, Class<T> clazz, UUID id) {
|
||||
try {
|
||||
var field = clazz.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(entity, id);
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
}
|
||||
|
||||
@Test
|
||||
void listItemsShouldReturnItemsSortedByBestBefore() {
|
||||
var household = testHousehold();
|
||||
var ingredient = testIngredient(household, "Milk");
|
||||
var item = testPantryItem(household, ingredient);
|
||||
|
||||
when(pantryItemRepository.findByHouseholdIdOrderByBestBeforeAscNullsLast(HOUSEHOLD_ID))
|
||||
.thenReturn(List.of(item));
|
||||
|
||||
List<PantryItemResponse> result = pantryService.listItems(HOUSEHOLD_ID);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).name()).isEqualTo("Milk");
|
||||
assertThat(result.get(0).category()).isNotNull();
|
||||
assertThat(result.get(0).category().name()).isEqualTo("Dairy");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createItemWithIngredientShouldResolveIngredient() {
|
||||
var household = testHousehold();
|
||||
var ingredient = testIngredient(household, "Milk");
|
||||
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(ingredientRepository.findById(ingredient.getId())).thenReturn(Optional.of(ingredient));
|
||||
when(pantryItemRepository.save(any(PantryItem.class))).thenAnswer(invocation -> {
|
||||
PantryItem saved = invocation.getArgument(0);
|
||||
setId(saved, PantryItem.class, UUID.randomUUID());
|
||||
return saved;
|
||||
});
|
||||
|
||||
var request = new CreatePantryItemRequest(ingredient.getId(), null,
|
||||
new BigDecimal("1.00"), "liters", LocalDate.of(2026, 4, 15), null);
|
||||
PantryItemResponse result = pantryService.createItem(HOUSEHOLD_ID, request);
|
||||
|
||||
assertThat(result.name()).isEqualTo("Milk");
|
||||
assertThat(result.ingredientId()).isEqualTo(ingredient.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createItemWithCustomNameShouldUseCustomName() {
|
||||
var household = testHousehold();
|
||||
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(pantryItemRepository.save(any(PantryItem.class))).thenAnswer(invocation -> {
|
||||
PantryItem saved = invocation.getArgument(0);
|
||||
setId(saved, PantryItem.class, UUID.randomUUID());
|
||||
return saved;
|
||||
});
|
||||
|
||||
var request = new CreatePantryItemRequest(null, "Homemade sauce",
|
||||
new BigDecimal("1.00"), "jar", null, null);
|
||||
PantryItemResponse result = pantryService.createItem(HOUSEHOLD_ID, request);
|
||||
|
||||
assertThat(result.name()).isEqualTo("Homemade sauce");
|
||||
assertThat(result.ingredientId()).isNull();
|
||||
assertThat(result.category()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createItemWithoutIngredientOrCustomNameShouldThrowValidation() {
|
||||
var household = testHousehold();
|
||||
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
|
||||
var request = new CreatePantryItemRequest(null, null,
|
||||
new BigDecimal("1.00"), "kg", null, null);
|
||||
|
||||
assertThatThrownBy(() -> pantryService.createItem(HOUSEHOLD_ID, request))
|
||||
.isInstanceOf(ValidationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateItemShouldUpdateFields() {
|
||||
var household = testHousehold();
|
||||
var ingredient = testIngredient(household, "Milk");
|
||||
var item = testPantryItem(household, ingredient);
|
||||
|
||||
when(pantryItemRepository.findByIdAndHouseholdId(item.getId(), HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.of(item));
|
||||
when(pantryItemRepository.save(any(PantryItem.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
var request = new UpdatePantryItemRequest(new BigDecimal("0.50"), null, null, LocalDate.of(2026, 4, 1));
|
||||
PantryItemResponse result = pantryService.updateItem(HOUSEHOLD_ID, item.getId(), request);
|
||||
|
||||
assertThat(result.quantity()).isEqualByComparingTo(new BigDecimal("0.50"));
|
||||
assertThat(result.openedOn()).isEqualTo(LocalDate.of(2026, 4, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteItemShouldRemoveItem() {
|
||||
var household = testHousehold();
|
||||
var ingredient = testIngredient(household, "Milk");
|
||||
var item = testPantryItem(household, ingredient);
|
||||
|
||||
when(pantryItemRepository.findByIdAndHouseholdId(item.getId(), HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.of(item));
|
||||
|
||||
pantryService.deleteItem(HOUSEHOLD_ID, item.getId());
|
||||
|
||||
verify(pantryItemRepository).delete(item);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteItemNotFoundShouldThrow() {
|
||||
var itemId = UUID.randomUUID();
|
||||
|
||||
when(pantryItemRepository.findByIdAndHouseholdId(itemId, HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> pantryService.deleteItem(HOUSEHOLD_ID, itemId))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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.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.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CookingLogControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
|
||||
@Mock private PlanningService planningService;
|
||||
@Mock private HouseholdResolver householdResolver;
|
||||
|
||||
@InjectMocks private CookingLogController cookingLogController;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(cookingLogController)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCookingLogShouldReturn201() throws Exception {
|
||||
var recipeId = UUID.randomUUID();
|
||||
var logResponse = new CookingLogResponse(UUID.randomUUID(), recipeId,
|
||||
"Spaghetti Bolognese", LocalDate.of(2026, 4, 7), UUID.randomUUID());
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(planningService.createCookingLog(eq(HOUSEHOLD_ID), any(), any(CreateCookingLogRequest.class)))
|
||||
.thenReturn(logResponse);
|
||||
|
||||
mockMvc.perform(post("/v1/cooking-logs")
|
||||
.principal(() -> "sarah@example.com")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(
|
||||
new CreateCookingLogRequest(recipeId, LocalDate.of(2026, 4, 7)))))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.recipeName").value("Spaghetti Bolognese"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listCookingLogsShouldReturn200() throws Exception {
|
||||
var log = new CookingLogResponse(UUID.randomUUID(), UUID.randomUUID(),
|
||||
"Spaghetti Bolognese", LocalDate.of(2026, 4, 7), UUID.randomUUID());
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(planningService.listCookingLogs(HOUSEHOLD_ID, 30, 0)).thenReturn(List.of(log));
|
||||
|
||||
mockMvc.perform(get("/v1/cooking-logs")
|
||||
.principal(() -> "sarah@example.com")
|
||||
.param("limit", "30")
|
||||
.param("offset", "0"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].recipeName").value("Spaghetti Bolognese"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package com.recipeapp.planning;
|
||||
|
||||
import com.recipeapp.auth.UserAccountRepository;
|
||||
import com.recipeapp.auth.entity.UserAccount;
|
||||
import com.recipeapp.common.ConflictException;
|
||||
import com.recipeapp.common.ResourceNotFoundException;
|
||||
import com.recipeapp.common.ValidationException;
|
||||
import com.recipeapp.household.HouseholdRepository;
|
||||
import com.recipeapp.household.entity.Household;
|
||||
import com.recipeapp.planning.dto.*;
|
||||
import com.recipeapp.planning.entity.*;
|
||||
import com.recipeapp.recipe.RecipeRepository;
|
||||
import com.recipeapp.recipe.entity.*;
|
||||
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.data.domain.PageRequest;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PlanningServiceTest {
|
||||
|
||||
@Mock private WeekPlanRepository weekPlanRepository;
|
||||
@Mock private WeekPlanSlotRepository weekPlanSlotRepository;
|
||||
@Mock private CookingLogRepository cookingLogRepository;
|
||||
@Mock private RecipeRepository recipeRepository;
|
||||
@Mock private HouseholdRepository householdRepository;
|
||||
@Mock private UserAccountRepository userAccountRepository;
|
||||
|
||||
@InjectMocks private PlanningServiceImpl planningService;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
private static final LocalDate WEEK_START = LocalDate.of(2026, 4, 6); // Monday
|
||||
|
||||
private Household testHousehold() {
|
||||
var h = new Household("Test family", null);
|
||||
setId(h, Household.class, HOUSEHOLD_ID);
|
||||
return h;
|
||||
}
|
||||
|
||||
private WeekPlan testWeekPlan(Household household) {
|
||||
var wp = new WeekPlan(household, WEEK_START);
|
||||
setId(wp, WeekPlan.class, UUID.randomUUID());
|
||||
return wp;
|
||||
}
|
||||
|
||||
private Recipe testRecipe(Household household, String name) {
|
||||
var r = new Recipe(household, name, (short) 4, (short) 45, "medium", true);
|
||||
setId(r, Recipe.class, UUID.randomUUID());
|
||||
return r;
|
||||
}
|
||||
|
||||
private <T> void setId(T entity, Class<T> clazz, UUID id) {
|
||||
try {
|
||||
var field = clazz.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(entity, id);
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
}
|
||||
|
||||
// ── Week Plan CRUD ──
|
||||
|
||||
@Test
|
||||
void getWeekPlanShouldReturnPlanWithSlots() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var recipe = testRecipe(household, "Spaghetti");
|
||||
var slot = new WeekPlanSlot(plan, recipe, WEEK_START);
|
||||
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
||||
plan.getSlots().add(slot);
|
||||
|
||||
when(weekPlanRepository.findByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START))
|
||||
.thenReturn(Optional.of(plan));
|
||||
|
||||
WeekPlanResponse result = planningService.getWeekPlan(HOUSEHOLD_ID, WEEK_START);
|
||||
|
||||
assertThat(result.weekStart()).isEqualTo(WEEK_START);
|
||||
assertThat(result.slots()).hasSize(1);
|
||||
assertThat(result.slots().getFirst().recipe().name()).isEqualTo("Spaghetti");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getWeekPlanShouldThrowWhenNotFound() {
|
||||
when(weekPlanRepository.findByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.getWeekPlan(HOUSEHOLD_ID, WEEK_START))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWeekPlanShouldPersist() {
|
||||
var household = testHousehold();
|
||||
|
||||
when(weekPlanRepository.existsByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START)).thenReturn(false);
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(weekPlanRepository.save(any(WeekPlan.class))).thenAnswer(i -> {
|
||||
WeekPlan wp = i.getArgument(0);
|
||||
setId(wp, WeekPlan.class, UUID.randomUUID());
|
||||
return wp;
|
||||
});
|
||||
|
||||
WeekPlanResponse result = planningService.createWeekPlan(HOUSEHOLD_ID, WEEK_START);
|
||||
|
||||
assertThat(result.weekStart()).isEqualTo(WEEK_START);
|
||||
assertThat(result.status()).isEqualTo("draft");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWeekPlanShouldThrowConflictWhenExists() {
|
||||
when(weekPlanRepository.existsByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START)).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> planningService.createWeekPlan(HOUSEHOLD_ID, WEEK_START))
|
||||
.isInstanceOf(ConflictException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWeekPlanShouldThrowWhenNotMonday() {
|
||||
var tuesday = LocalDate.of(2026, 4, 7);
|
||||
|
||||
assertThatThrownBy(() -> planningService.createWeekPlan(HOUSEHOLD_ID, tuesday))
|
||||
.isInstanceOf(ValidationException.class);
|
||||
}
|
||||
|
||||
// ── Slots ──
|
||||
|
||||
@Test
|
||||
void addSlotShouldCreateSlot() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var recipe = testRecipe(household, "Spaghetti");
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipe.getId(), HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.of(recipe));
|
||||
when(weekPlanSlotRepository.save(any(WeekPlanSlot.class))).thenAnswer(i -> {
|
||||
WeekPlanSlot s = i.getArgument(0);
|
||||
setId(s, WeekPlanSlot.class, UUID.randomUUID());
|
||||
return s;
|
||||
});
|
||||
|
||||
SlotResponse result = planningService.addSlot(HOUSEHOLD_ID, plan.getId(),
|
||||
new CreateSlotRequest(WEEK_START.plusDays(1), recipe.getId()));
|
||||
|
||||
assertThat(result.recipe().name()).isEqualTo("Spaghetti");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateSlotShouldSwapRecipe() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var oldRecipe = testRecipe(household, "Spaghetti");
|
||||
var newRecipe = testRecipe(household, "Stir Fry");
|
||||
var slot = new WeekPlanSlot(plan, oldRecipe, WEEK_START);
|
||||
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
||||
plan.getSlots().add(slot);
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
when(weekPlanSlotRepository.findById(slot.getId())).thenReturn(Optional.of(slot));
|
||||
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(newRecipe.getId(), HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.of(newRecipe));
|
||||
|
||||
SlotResponse result = planningService.updateSlot(HOUSEHOLD_ID, plan.getId(), slot.getId(),
|
||||
new UpdateSlotRequest(newRecipe.getId()));
|
||||
|
||||
assertThat(result.recipe().name()).isEqualTo("Stir Fry");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteSlotShouldRemoveSlot() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var slot = new WeekPlanSlot(plan, testRecipe(household, "X"), WEEK_START);
|
||||
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
||||
plan.getSlots().add(slot);
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
when(weekPlanSlotRepository.findById(slot.getId())).thenReturn(Optional.of(slot));
|
||||
|
||||
planningService.deleteSlot(HOUSEHOLD_ID, plan.getId(), slot.getId());
|
||||
|
||||
verify(weekPlanSlotRepository).delete(slot);
|
||||
}
|
||||
|
||||
// ── Confirm ──
|
||||
|
||||
@Test
|
||||
void confirmPlanShouldSetStatusAndTimestamp() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var slot = new WeekPlanSlot(plan, testRecipe(household, "X"), WEEK_START);
|
||||
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
||||
plan.getSlots().add(slot);
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
|
||||
WeekPlanResponse result = planningService.confirmPlan(HOUSEHOLD_ID, plan.getId());
|
||||
|
||||
assertThat(result.status()).isEqualTo("confirmed");
|
||||
assertThat(result.confirmedAt()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmPlanShouldThrowWhenNoSlots() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
|
||||
assertThatThrownBy(() -> planningService.confirmPlan(HOUSEHOLD_ID, plan.getId()))
|
||||
.isInstanceOf(ValidationException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmPlanShouldThrowWhenAlreadyConfirmed() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
plan.setStatus("confirmed");
|
||||
var slot = new WeekPlanSlot(plan, testRecipe(household, "X"), WEEK_START);
|
||||
setId(slot, WeekPlanSlot.class, UUID.randomUUID());
|
||||
plan.getSlots().add(slot);
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
|
||||
assertThatThrownBy(() -> planningService.confirmPlan(HOUSEHOLD_ID, plan.getId()))
|
||||
.isInstanceOf(ValidationException.class);
|
||||
}
|
||||
|
||||
// ── Cooking Logs ──
|
||||
|
||||
@Test
|
||||
void createCookingLogShouldPersist() {
|
||||
var household = testHousehold();
|
||||
var recipe = testRecipe(household, "Spaghetti");
|
||||
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
||||
setId(user, UserAccount.class, UUID.randomUUID());
|
||||
|
||||
when(recipeRepository.findById(recipe.getId())).thenReturn(Optional.of(recipe));
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(userAccountRepository.findById(user.getId())).thenReturn(Optional.of(user));
|
||||
when(cookingLogRepository.save(any(CookingLog.class))).thenAnswer(i -> {
|
||||
CookingLog cl = i.getArgument(0);
|
||||
setId(cl, CookingLog.class, UUID.randomUUID());
|
||||
return cl;
|
||||
});
|
||||
|
||||
CookingLogResponse result = planningService.createCookingLog(HOUSEHOLD_ID, user.getId(),
|
||||
new CreateCookingLogRequest(recipe.getId(), LocalDate.of(2026, 4, 7)));
|
||||
|
||||
assertThat(result.recipeName()).isEqualTo("Spaghetti");
|
||||
assertThat(result.cookedOn()).isEqualTo(LocalDate.of(2026, 4, 7));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listCookingLogsShouldReturnRecent() {
|
||||
var household = testHousehold();
|
||||
var recipe = testRecipe(household, "Spaghetti");
|
||||
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
||||
setId(user, UserAccount.class, UUID.randomUUID());
|
||||
var log = new CookingLog(recipe, household, LocalDate.of(2026, 4, 7), user);
|
||||
setId(log, CookingLog.class, UUID.randomUUID());
|
||||
|
||||
when(cookingLogRepository.findByHouseholdIdOrderByCookedOnDesc(eq(HOUSEHOLD_ID), any()))
|
||||
.thenReturn(List.of(log));
|
||||
|
||||
List<CookingLogResponse> result = planningService.listCookingLogs(HOUSEHOLD_ID, 30, 0);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.getFirst().recipeName()).isEqualTo("Spaghetti");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.recipeapp.recipe;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.recipeapp.common.GlobalExceptionHandler;
|
||||
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.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IngredientCategoryControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Mock private RecipeService recipeService;
|
||||
@Mock private HouseholdResolver householdResolver;
|
||||
|
||||
@InjectMocks private IngredientCategoryController ingredientCategoryController;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(ingredientCategoryController)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listCategoriesShouldReturn200() throws Exception {
|
||||
var cat = new IngredientCategoryResponse(UUID.randomUUID(), "Produce");
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(recipeService.listCategories(HOUSEHOLD_ID)).thenReturn(List.of(cat));
|
||||
|
||||
mockMvc.perform(get("/v1/ingredient-categories")
|
||||
.principal(() -> "sarah@example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].name").value("Produce"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCategoryShouldReturn201() throws Exception {
|
||||
var request = new IngredientCategoryCreateRequest("Frozen");
|
||||
var response = new IngredientCategoryResponse(UUID.randomUUID(), "Frozen");
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(recipeService.createCategory(eq(HOUSEHOLD_ID), any(IngredientCategoryCreateRequest.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/v1/ingredient-categories")
|
||||
.principal(() -> "sarah@example.com")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.name").value("Frozen"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.recipeapp.recipe;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.recipeapp.common.GlobalExceptionHandler;
|
||||
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.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class IngredientControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Mock private RecipeService recipeService;
|
||||
@Mock private HouseholdResolver householdResolver;
|
||||
|
||||
@InjectMocks private IngredientController ingredientController;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(ingredientController)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void searchIngredientsShouldReturn200() throws Exception {
|
||||
var catRef = new RecipeDetailResponse.CategoryRef(UUID.randomUUID(), "meat");
|
||||
var ingredient = new IngredientResponse(UUID.randomUUID(), "chicken breast", catRef, false);
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(recipeService.searchIngredients(HOUSEHOLD_ID, "chick", null))
|
||||
.thenReturn(List.of(ingredient));
|
||||
|
||||
mockMvc.perform(get("/v1/ingredients")
|
||||
.principal(() -> "sarah@example.com")
|
||||
.param("search", "chick"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].name").value("chicken breast"))
|
||||
.andExpect(jsonPath("$[0].category.name").value("meat"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void patchIngredientShouldReturn200() throws Exception {
|
||||
var catRef = new RecipeDetailResponse.CategoryRef(UUID.randomUUID(), "oil");
|
||||
var ingredientId = UUID.randomUUID();
|
||||
var response = new IngredientResponse(ingredientId, "olive oil", catRef, true);
|
||||
var request = new IngredientPatchRequest(null, true, null);
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(recipeService.patchIngredient(eq(HOUSEHOLD_ID), eq(ingredientId), any(IngredientPatchRequest.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
mockMvc.perform(patch("/v1/ingredients/{id}", ingredientId)
|
||||
.principal(() -> "sarah@example.com")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.isStaple").value(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
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", true, null);
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(recipeService.listRecipes(eq(HOUSEHOLD_ID), isNull(), isNull(), isNull(), isNull(),
|
||||
isNull(), eq(20), eq(0)))
|
||||
.thenReturn(List.of(summary));
|
||||
when(recipeService.countRecipes(eq(HOUSEHOLD_ID), isNull(), 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(true),
|
||||
eq(30), eq("-cookTimeMin"), eq(10), eq(5)))
|
||||
.thenReturn(List.of());
|
||||
when(recipeService.countRecipes(eq(HOUSEHOLD_ID), eq("pasta"), eq("easy"), eq(true), eq(30)))
|
||||
.thenReturn(0L);
|
||||
|
||||
mockMvc.perform(get("/v1/recipes")
|
||||
.principal(() -> "sarah@example.com")
|
||||
.param("search", "pasta")
|
||||
.param("effort", "easy")
|
||||
.param("isChildFriendly", "true")
|
||||
.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", (short) 4, (short) 45, "medium", true, 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", true, 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")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
package com.recipeapp.recipe;
|
||||
|
||||
import com.recipeapp.common.ConflictException;
|
||||
import com.recipeapp.common.ResourceNotFoundException;
|
||||
import com.recipeapp.household.HouseholdRepository;
|
||||
import com.recipeapp.household.entity.Household;
|
||||
import com.recipeapp.recipe.dto.*;
|
||||
import com.recipeapp.recipe.entity.*;
|
||||
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 java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RecipeServiceTest {
|
||||
|
||||
@Mock private RecipeRepository recipeRepository;
|
||||
@Mock private IngredientRepository ingredientRepository;
|
||||
@Mock private TagRepository tagRepository;
|
||||
@Mock private IngredientCategoryRepository ingredientCategoryRepository;
|
||||
@Mock private HouseholdRepository householdRepository;
|
||||
|
||||
@InjectMocks private RecipeServiceImpl recipeService;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
|
||||
private Household testHousehold() {
|
||||
var h = new Household("Test family", null);
|
||||
try {
|
||||
var field = Household.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(h, HOUSEHOLD_ID);
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
return h;
|
||||
}
|
||||
|
||||
private Recipe testRecipe(Household household) {
|
||||
var r = new Recipe(household, "Spaghetti Bolognese", (short) 4, (short) 45, "medium", true);
|
||||
try {
|
||||
var field = Recipe.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(r, UUID.randomUUID());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
return r;
|
||||
}
|
||||
|
||||
private Ingredient testIngredient(Household household, String name) {
|
||||
var ing = new Ingredient(household, name, false);
|
||||
try {
|
||||
var field = Ingredient.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(ing, UUID.randomUUID());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
return ing;
|
||||
}
|
||||
|
||||
private Tag testTag(Household household, String name, String type) {
|
||||
var tag = new Tag(household, name, type);
|
||||
try {
|
||||
var field = Tag.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(tag, UUID.randomUUID());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
return tag;
|
||||
}
|
||||
|
||||
// ── Recipe CRUD ──
|
||||
|
||||
@Test
|
||||
void getRecipeShouldReturnDetail() {
|
||||
var household = testHousehold();
|
||||
var recipe = testRecipe(household);
|
||||
var ingredient = testIngredient(household, "spaghetti");
|
||||
recipe.getIngredients().add(new RecipeIngredient(recipe, ingredient, new BigDecimal("400"), "g", (short) 1));
|
||||
recipe.getSteps().add(new RecipeStep(recipe, (short) 1, "Boil water."));
|
||||
var tag = testTag(household, "beef", "protein");
|
||||
recipe.getTags().add(tag);
|
||||
|
||||
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipe.getId(), HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.of(recipe));
|
||||
|
||||
RecipeDetailResponse result = recipeService.getRecipe(HOUSEHOLD_ID, recipe.getId());
|
||||
|
||||
assertThat(result.name()).isEqualTo("Spaghetti Bolognese");
|
||||
assertThat(result.ingredients()).hasSize(1);
|
||||
assertThat(result.ingredients().getFirst().name()).isEqualTo("spaghetti");
|
||||
assertThat(result.steps()).hasSize(1);
|
||||
assertThat(result.tags()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRecipeShouldThrowWhenNotFound() {
|
||||
var id = UUID.randomUUID();
|
||||
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(id, HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> recipeService.getRecipe(HOUSEHOLD_ID, id))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createRecipeShouldPersistWithIngredientsStepsTags() {
|
||||
var household = testHousehold();
|
||||
var ingredient = testIngredient(household, "spaghetti");
|
||||
var tag = testTag(household, "beef", "protein");
|
||||
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(ingredientRepository.findById(ingredient.getId())).thenReturn(Optional.of(ingredient));
|
||||
when(tagRepository.findAllById(List.of(tag.getId()))).thenReturn(List.of(tag));
|
||||
when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> {
|
||||
Recipe r = i.getArgument(0);
|
||||
try {
|
||||
var field = Recipe.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(r, UUID.randomUUID());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
return r;
|
||||
});
|
||||
|
||||
var request = new RecipeCreateRequest(
|
||||
"Spaghetti Bolognese", (short) 4, (short) 45, "medium", true, null,
|
||||
List.of(new RecipeCreateRequest.IngredientEntry(
|
||||
ingredient.getId(), null, new BigDecimal("400"), "g", (short) 1)),
|
||||
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Boil water.")),
|
||||
List.of(tag.getId()));
|
||||
|
||||
RecipeDetailResponse result = recipeService.createRecipe(HOUSEHOLD_ID, request);
|
||||
|
||||
assertThat(result.name()).isEqualTo("Spaghetti Bolognese");
|
||||
assertThat(result.id()).isNotNull();
|
||||
verify(recipeRepository).save(any(Recipe.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createRecipeShouldCreateNewIngredientWhenNameProvided() {
|
||||
var household = testHousehold();
|
||||
var tag = testTag(household, "beef", "protein");
|
||||
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(ingredientRepository.save(any(Ingredient.class))).thenAnswer(i -> {
|
||||
Ingredient ing = i.getArgument(0);
|
||||
try {
|
||||
var field = Ingredient.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(ing, UUID.randomUUID());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
return ing;
|
||||
});
|
||||
when(tagRepository.findAllById(List.of(tag.getId()))).thenReturn(List.of(tag));
|
||||
when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> {
|
||||
Recipe r = i.getArgument(0);
|
||||
try {
|
||||
var field = Recipe.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(r, UUID.randomUUID());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
return r;
|
||||
});
|
||||
|
||||
var request = new RecipeCreateRequest(
|
||||
"Carbonara", (short) 2, (short) 30, "medium", false, null,
|
||||
List.of(new RecipeCreateRequest.IngredientEntry(
|
||||
null, "pancetta", new BigDecimal("100"), "g", (short) 1)),
|
||||
List.of(),
|
||||
List.of(tag.getId()));
|
||||
|
||||
RecipeDetailResponse result = recipeService.createRecipe(HOUSEHOLD_ID, request);
|
||||
|
||||
assertThat(result.name()).isEqualTo("Carbonara");
|
||||
verify(ingredientRepository).save(any(Ingredient.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateRecipeShouldReplaceChildren() {
|
||||
var household = testHousehold();
|
||||
var recipe = testRecipe(household);
|
||||
var ingredient = testIngredient(household, "rice");
|
||||
var tag = testTag(household, "chicken", "protein");
|
||||
|
||||
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipe.getId(), HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.of(recipe));
|
||||
when(ingredientRepository.findById(ingredient.getId())).thenReturn(Optional.of(ingredient));
|
||||
when(tagRepository.findAllById(List.of(tag.getId()))).thenReturn(List.of(tag));
|
||||
when(recipeRepository.save(any(Recipe.class))).thenAnswer(i -> i.getArgument(0));
|
||||
|
||||
var request = new RecipeCreateRequest(
|
||||
"Chicken Rice", (short) 3, (short) 25, "easy", true, null,
|
||||
List.of(new RecipeCreateRequest.IngredientEntry(
|
||||
ingredient.getId(), null, new BigDecimal("300"), "g", (short) 1)),
|
||||
List.of(new RecipeCreateRequest.StepEntry((short) 1, "Cook rice.")),
|
||||
List.of(tag.getId()));
|
||||
|
||||
RecipeDetailResponse result = recipeService.updateRecipe(HOUSEHOLD_ID, recipe.getId(), request);
|
||||
|
||||
assertThat(result.name()).isEqualTo("Chicken Rice");
|
||||
assertThat(result.serves()).isEqualTo((short) 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRecipeShouldSoftDelete() {
|
||||
var household = testHousehold();
|
||||
var recipe = testRecipe(household);
|
||||
|
||||
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipe.getId(), HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.of(recipe));
|
||||
|
||||
recipeService.deleteRecipe(HOUSEHOLD_ID, recipe.getId());
|
||||
|
||||
assertThat(recipe.getDeletedAt()).isNotNull();
|
||||
}
|
||||
|
||||
// ── Ingredients ──
|
||||
|
||||
@Test
|
||||
void searchIngredientsShouldReturnMatches() {
|
||||
var household = testHousehold();
|
||||
var ingredient = testIngredient(household, "chicken breast");
|
||||
|
||||
when(ingredientRepository.findByHouseholdIdAndNameContainingIgnoreCase(HOUSEHOLD_ID, "chick"))
|
||||
.thenReturn(List.of(ingredient));
|
||||
|
||||
List<IngredientResponse> result = recipeService.searchIngredients(HOUSEHOLD_ID, "chick", null);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.getFirst().name()).isEqualTo("chicken breast");
|
||||
}
|
||||
|
||||
@Test
|
||||
void patchIngredientShouldUpdateFields() {
|
||||
var household = testHousehold();
|
||||
var ingredient = testIngredient(household, "olive oil");
|
||||
|
||||
when(ingredientRepository.findById(ingredient.getId())).thenReturn(Optional.of(ingredient));
|
||||
|
||||
var request = new IngredientPatchRequest("extra virgin olive oil", true, null);
|
||||
IngredientResponse result = recipeService.patchIngredient(HOUSEHOLD_ID, ingredient.getId(), request);
|
||||
|
||||
assertThat(result.name()).isEqualTo("extra virgin olive oil");
|
||||
assertThat(result.isStaple()).isTrue();
|
||||
}
|
||||
|
||||
// ── Tags ──
|
||||
|
||||
@Test
|
||||
void listTagsShouldReturnAll() {
|
||||
var household = testHousehold();
|
||||
var tag = testTag(household, "chicken", "protein");
|
||||
|
||||
when(tagRepository.findByHouseholdId(HOUSEHOLD_ID)).thenReturn(List.of(tag));
|
||||
|
||||
List<TagResponse> result = recipeService.listTags(HOUSEHOLD_ID);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.getFirst().name()).isEqualTo("chicken");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createTagShouldPersist() {
|
||||
var household = testHousehold();
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(tagRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "Thai")).thenReturn(false);
|
||||
when(tagRepository.save(any(Tag.class))).thenAnswer(i -> {
|
||||
Tag t = i.getArgument(0);
|
||||
try {
|
||||
var field = Tag.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(t, UUID.randomUUID());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
return t;
|
||||
});
|
||||
|
||||
TagResponse result = recipeService.createTag(HOUSEHOLD_ID, new TagCreateRequest("Thai", "cuisine"));
|
||||
|
||||
assertThat(result.name()).isEqualTo("Thai");
|
||||
assertThat(result.tagType()).isEqualTo("cuisine");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createTagShouldThrowConflictWhenNameExists() {
|
||||
when(tagRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "Chicken")).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> recipeService.createTag(HOUSEHOLD_ID, new TagCreateRequest("Chicken", "protein")))
|
||||
.isInstanceOf(ConflictException.class);
|
||||
}
|
||||
|
||||
// ── Ingredient Categories ──
|
||||
|
||||
@Test
|
||||
void listCategoriesShouldReturnAllSorted() {
|
||||
var household = testHousehold();
|
||||
var cat = new IngredientCategory(household, "Produce", (short) 1);
|
||||
try {
|
||||
var field = IngredientCategory.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(cat, UUID.randomUUID());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
|
||||
when(ingredientCategoryRepository.findByHouseholdIdOrderBySortOrder(HOUSEHOLD_ID))
|
||||
.thenReturn(List.of(cat));
|
||||
|
||||
List<IngredientCategoryResponse> result = recipeService.listCategories(HOUSEHOLD_ID);
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.getFirst().name()).isEqualTo("Produce");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCategoryShouldPersist() {
|
||||
var household = testHousehold();
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(ingredientCategoryRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "Frozen"))
|
||||
.thenReturn(false);
|
||||
when(ingredientCategoryRepository.countByHouseholdId(HOUSEHOLD_ID)).thenReturn(8L);
|
||||
when(ingredientCategoryRepository.save(any(IngredientCategory.class))).thenAnswer(i -> {
|
||||
IngredientCategory c = i.getArgument(0);
|
||||
try {
|
||||
var field = IngredientCategory.class.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(c, UUID.randomUUID());
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
return c;
|
||||
});
|
||||
|
||||
IngredientCategoryResponse result = recipeService.createCategory(
|
||||
HOUSEHOLD_ID, new IngredientCategoryCreateRequest("Frozen"));
|
||||
|
||||
assertThat(result.name()).isEqualTo("Frozen");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCategoryShouldThrowConflictWhenNameExists() {
|
||||
when(ingredientCategoryRepository.existsByHouseholdIdAndNameIgnoreCase(HOUSEHOLD_ID, "Produce"))
|
||||
.thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> recipeService.createCategory(
|
||||
HOUSEHOLD_ID, new IngredientCategoryCreateRequest("Produce")))
|
||||
.isInstanceOf(ConflictException.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.recipeapp.recipe;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.recipeapp.common.GlobalExceptionHandler;
|
||||
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.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TagControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Mock private RecipeService recipeService;
|
||||
@Mock private HouseholdResolver householdResolver;
|
||||
|
||||
@InjectMocks private TagController tagController;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(tagController)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void listTagsShouldReturn200() throws Exception {
|
||||
var tag = new TagResponse(UUID.randomUUID(), "chicken", "protein");
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(recipeService.listTags(HOUSEHOLD_ID)).thenReturn(List.of(tag));
|
||||
|
||||
mockMvc.perform(get("/v1/tags")
|
||||
.principal(() -> "sarah@example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].name").value("chicken"))
|
||||
.andExpect(jsonPath("$[0].tagType").value("protein"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createTagShouldReturn201() throws Exception {
|
||||
var request = new TagCreateRequest("Thai", "cuisine");
|
||||
var response = new TagResponse(UUID.randomUUID(), "Thai", "cuisine");
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(recipeService.createTag(eq(HOUSEHOLD_ID), any(TagCreateRequest.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/v1/tags")
|
||||
.principal(() -> "sarah@example.com")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.name").value("Thai"))
|
||||
.andExpect(jsonPath("$.tagType").value("cuisine"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.recipeapp.shopping;
|
||||
|
||||
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.recipe.HouseholdResolver;
|
||||
import com.recipeapp.shopping.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.time.Instant;
|
||||
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 ShoppingListControllerTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
|
||||
@Mock private ShoppingService shoppingService;
|
||||
@Mock private HouseholdResolver householdResolver;
|
||||
|
||||
@InjectMocks private ShoppingListController shoppingListController;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
private static final UUID USER_ID = UUID.randomUUID();
|
||||
private static final UUID LIST_ID = UUID.randomUUID();
|
||||
private static final UUID ITEM_ID = UUID.randomUUID();
|
||||
private static final UUID PLAN_ID = UUID.randomUUID();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(shoppingListController)
|
||||
.setControllerAdvice(new GlobalExceptionHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateFromPlanShouldReturn201() throws Exception {
|
||||
var item = new ShoppingListItemResponse(
|
||||
ITEM_ID, UUID.randomUUID(), "Tomatoes",
|
||||
new ShoppingListItemResponse.CategoryRef(UUID.randomUUID(), "Produce"),
|
||||
new BigDecimal("4.00"), "pcs", false, null, List.of(UUID.randomUUID()));
|
||||
var response = new ShoppingListResponse(LIST_ID, PLAN_ID, "draft", null, List.of(item));
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(shoppingService.generateFromPlan(HOUSEHOLD_ID, PLAN_ID)).thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/v1/week-plans/{id}/shopping-list", PLAN_ID)
|
||||
.principal(() -> "sarah@example.com"))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.id").value(LIST_ID.toString()))
|
||||
.andExpect(jsonPath("$.status").value("draft"))
|
||||
.andExpect(jsonPath("$.items[0].name").value("Tomatoes"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getShoppingListShouldReturn200() throws Exception {
|
||||
var response = new ShoppingListResponse(LIST_ID, PLAN_ID, "draft", null, List.of());
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(shoppingService.getShoppingList(HOUSEHOLD_ID, LIST_ID)).thenReturn(response);
|
||||
|
||||
mockMvc.perform(get("/v1/shopping-lists/{id}", LIST_ID)
|
||||
.principal(() -> "sarah@example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(LIST_ID.toString()))
|
||||
.andExpect(jsonPath("$.weekPlanId").value(PLAN_ID.toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishShouldReturn200() throws Exception {
|
||||
var now = Instant.now();
|
||||
var response = new PublishResponse(LIST_ID, "published", now);
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(shoppingService.publish(HOUSEHOLD_ID, LIST_ID)).thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/v1/shopping-lists/{id}/publish", LIST_ID)
|
||||
.principal(() -> "sarah@example.com"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("published"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkItemShouldReturn200() throws Exception {
|
||||
var response = new ShoppingListItemResponse(
|
||||
ITEM_ID, UUID.randomUUID(), "Tomatoes", null,
|
||||
new BigDecimal("4.00"), "pcs", true, USER_ID, List.of());
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(householdResolver.resolveUserId("sarah@example.com")).thenReturn(USER_ID);
|
||||
when(shoppingService.checkItem(eq(HOUSEHOLD_ID), eq(LIST_ID), eq(ITEM_ID),
|
||||
any(CheckItemRequest.class), eq(USER_ID))).thenReturn(response);
|
||||
|
||||
mockMvc.perform(patch("/v1/shopping-lists/{listId}/items/{itemId}", LIST_ID, ITEM_ID)
|
||||
.principal(() -> "sarah@example.com")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(new CheckItemRequest(true))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.isChecked").value(true))
|
||||
.andExpect(jsonPath("$.checkedBy").value(USER_ID.toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addItemShouldReturn201() throws Exception {
|
||||
var response = new ShoppingListItemResponse(
|
||||
ITEM_ID, null, "Paper towels", null,
|
||||
new BigDecimal("1"), "", false, null, List.of());
|
||||
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
when(shoppingService.addItem(eq(HOUSEHOLD_ID), eq(LIST_ID), any(AddItemRequest.class)))
|
||||
.thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/v1/shopping-lists/{id}/items", LIST_ID)
|
||||
.principal(() -> "sarah@example.com")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(
|
||||
new AddItemRequest(null, "Paper towels", new BigDecimal("1"), ""))))
|
||||
.andExpect(status().isCreated())
|
||||
.andExpect(jsonPath("$.name").value("Paper towels"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteItemShouldReturn204() throws Exception {
|
||||
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||
doNothing().when(shoppingService).deleteItem(HOUSEHOLD_ID, LIST_ID, ITEM_ID);
|
||||
|
||||
mockMvc.perform(delete("/v1/shopping-lists/{listId}/items/{itemId}", LIST_ID, ITEM_ID)
|
||||
.principal(() -> "sarah@example.com"))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.recipeapp.shopping;
|
||||
|
||||
import com.recipeapp.auth.UserAccountRepository;
|
||||
import com.recipeapp.auth.entity.UserAccount;
|
||||
import com.recipeapp.common.ResourceNotFoundException;
|
||||
import com.recipeapp.common.ValidationException;
|
||||
import com.recipeapp.household.HouseholdRepository;
|
||||
import com.recipeapp.household.entity.Household;
|
||||
import com.recipeapp.planning.WeekPlanRepository;
|
||||
import com.recipeapp.planning.entity.WeekPlan;
|
||||
import com.recipeapp.planning.entity.WeekPlanSlot;
|
||||
import com.recipeapp.recipe.IngredientRepository;
|
||||
import com.recipeapp.recipe.entity.Ingredient;
|
||||
import com.recipeapp.recipe.entity.IngredientCategory;
|
||||
import com.recipeapp.recipe.entity.Recipe;
|
||||
import com.recipeapp.recipe.entity.RecipeIngredient;
|
||||
import com.recipeapp.shopping.dto.*;
|
||||
import com.recipeapp.shopping.entity.ShoppingList;
|
||||
import com.recipeapp.shopping.entity.ShoppingListItem;
|
||||
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 java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ShoppingServiceTest {
|
||||
|
||||
@Mock private ShoppingListRepository shoppingListRepository;
|
||||
@Mock private ShoppingListItemRepository shoppingListItemRepository;
|
||||
@Mock private WeekPlanRepository weekPlanRepository;
|
||||
@Mock private HouseholdRepository householdRepository;
|
||||
@Mock private IngredientRepository ingredientRepository;
|
||||
@Mock private UserAccountRepository userAccountRepository;
|
||||
|
||||
@InjectMocks private ShoppingServiceImpl shoppingService;
|
||||
|
||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||
private static final LocalDate WEEK_START = LocalDate.of(2026, 4, 6);
|
||||
|
||||
private Household testHousehold() {
|
||||
var h = new Household("Test family", null);
|
||||
setId(h, Household.class, HOUSEHOLD_ID);
|
||||
return h;
|
||||
}
|
||||
|
||||
private WeekPlan testWeekPlan(Household household) {
|
||||
var wp = new WeekPlan(household, WEEK_START);
|
||||
setId(wp, WeekPlan.class, UUID.randomUUID());
|
||||
return wp;
|
||||
}
|
||||
|
||||
private Recipe testRecipe(Household household, String name) {
|
||||
var r = new Recipe(household, name, (short) 4, (short) 45, "medium", true);
|
||||
setId(r, Recipe.class, UUID.randomUUID());
|
||||
return r;
|
||||
}
|
||||
|
||||
private Ingredient testIngredient(Household household, String name, boolean staple) {
|
||||
var i = new Ingredient(household, name, staple);
|
||||
setId(i, Ingredient.class, UUID.randomUUID());
|
||||
return i;
|
||||
}
|
||||
|
||||
private ShoppingList testShoppingList(Household household, WeekPlan weekPlan) {
|
||||
var sl = new ShoppingList(household, weekPlan);
|
||||
setId(sl, ShoppingList.class, UUID.randomUUID());
|
||||
return sl;
|
||||
}
|
||||
|
||||
private ShoppingListItem testItem(ShoppingList list, Ingredient ingredient,
|
||||
BigDecimal quantity, String unit) {
|
||||
var item = new ShoppingListItem(list, ingredient, null, quantity, unit, new UUID[0]);
|
||||
setId(item, ShoppingListItem.class, UUID.randomUUID());
|
||||
return item;
|
||||
}
|
||||
|
||||
private <T> void setId(T entity, Class<T> clazz, UUID id) {
|
||||
try {
|
||||
var field = clazz.getDeclaredField("id");
|
||||
field.setAccessible(true);
|
||||
field.set(entity, id);
|
||||
} catch (Exception e) { throw new RuntimeException(e); }
|
||||
}
|
||||
|
||||
// ── Generate ──
|
||||
|
||||
@Test
|
||||
void generateFromPlanShouldMergeIngredientsAndFilterStaples() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var recipe1 = testRecipe(household, "Spaghetti");
|
||||
var recipe2 = testRecipe(household, "Pizza");
|
||||
|
||||
var tomato = testIngredient(household, "Tomatoes", false);
|
||||
var salt = testIngredient(household, "Salt", true);
|
||||
var cheese = testIngredient(household, "Cheese", false);
|
||||
|
||||
// Recipe 1: 2 tomatoes + salt
|
||||
recipe1.getIngredients().add(new RecipeIngredient(recipe1, tomato, new BigDecimal("2.00"), "pcs", (short) 1));
|
||||
recipe1.getIngredients().add(new RecipeIngredient(recipe1, salt, new BigDecimal("1.00"), "tsp", (short) 2));
|
||||
|
||||
// Recipe 2: 3 tomatoes + cheese
|
||||
recipe2.getIngredients().add(new RecipeIngredient(recipe2, tomato, new BigDecimal("3.00"), "pcs", (short) 1));
|
||||
recipe2.getIngredients().add(new RecipeIngredient(recipe2, cheese, new BigDecimal("200.00"), "g", (short) 2));
|
||||
|
||||
var slot1 = new WeekPlanSlot(plan, recipe1, WEEK_START);
|
||||
setId(slot1, WeekPlanSlot.class, UUID.randomUUID());
|
||||
var slot2 = new WeekPlanSlot(plan, recipe2, WEEK_START.plusDays(1));
|
||||
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.save(any(ShoppingList.class))).thenAnswer(i -> {
|
||||
ShoppingList sl = i.getArgument(0);
|
||||
setId(sl, ShoppingList.class, UUID.randomUUID());
|
||||
return sl;
|
||||
});
|
||||
|
||||
ShoppingListResponse result = shoppingService.generateFromPlan(HOUSEHOLD_ID, plan.getId());
|
||||
|
||||
assertThat(result.status()).isEqualTo("draft");
|
||||
assertThat(result.items()).hasSize(2); // tomatoes + cheese (salt filtered)
|
||||
|
||||
var tomatoItem = result.items().stream()
|
||||
.filter(i -> "Tomatoes".equals(i.name())).findFirst().orElseThrow();
|
||||
assertThat(tomatoItem.quantity()).isEqualByComparingTo(new BigDecimal("5.00")); // 2 + 3
|
||||
assertThat(tomatoItem.sourceRecipes()).hasSize(2);
|
||||
|
||||
var cheeseItem = result.items().stream()
|
||||
.filter(i -> "Cheese".equals(i.name())).findFirst().orElseThrow();
|
||||
assertThat(cheeseItem.quantity()).isEqualByComparingTo(new BigDecimal("200.00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateFromPlanShouldThrowWhenPlanNotFound() {
|
||||
var planId = UUID.randomUUID();
|
||||
when(weekPlanRepository.findById(planId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> shoppingService.generateFromPlan(HOUSEHOLD_ID, planId))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
// ── Get ──
|
||||
|
||||
@Test
|
||||
void getShoppingListShouldReturnListWithItems() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var list = testShoppingList(household, plan);
|
||||
var ingredient = testIngredient(household, "Tomatoes", false);
|
||||
var item = testItem(list, ingredient, new BigDecimal("5.00"), "pcs");
|
||||
list.getItems().add(item);
|
||||
|
||||
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
|
||||
|
||||
ShoppingListResponse result = shoppingService.getShoppingList(HOUSEHOLD_ID, list.getId());
|
||||
|
||||
assertThat(result.id()).isEqualTo(list.getId());
|
||||
assertThat(result.items()).hasSize(1);
|
||||
assertThat(result.items().getFirst().name()).isEqualTo("Tomatoes");
|
||||
}
|
||||
|
||||
// ── Publish ──
|
||||
|
||||
@Test
|
||||
void publishShouldSetStatusAndTimestamp() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var list = testShoppingList(household, plan);
|
||||
|
||||
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
|
||||
when(shoppingListRepository.save(any(ShoppingList.class))).thenAnswer(i -> i.getArgument(0));
|
||||
|
||||
PublishResponse result = shoppingService.publish(HOUSEHOLD_ID, list.getId());
|
||||
|
||||
assertThat(result.status()).isEqualTo("published");
|
||||
assertThat(result.publishedAt()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void publishShouldThrowWhenAlreadyPublished() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var list = testShoppingList(household, plan);
|
||||
list.setStatus("published");
|
||||
|
||||
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
|
||||
|
||||
assertThatThrownBy(() -> shoppingService.publish(HOUSEHOLD_ID, list.getId()))
|
||||
.isInstanceOf(ValidationException.class);
|
||||
}
|
||||
|
||||
// ── Check Item ──
|
||||
|
||||
@Test
|
||||
void checkItemShouldSetCheckedAndUser() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var list = testShoppingList(household, plan);
|
||||
var ingredient = testIngredient(household, "Tomatoes", false);
|
||||
var item = testItem(list, ingredient, new BigDecimal("5.00"), "pcs");
|
||||
list.getItems().add(item);
|
||||
|
||||
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
||||
setId(user, UserAccount.class, UUID.randomUUID());
|
||||
|
||||
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
|
||||
when(userAccountRepository.findById(user.getId())).thenReturn(Optional.of(user));
|
||||
when(shoppingListItemRepository.save(any(ShoppingListItem.class))).thenAnswer(i -> i.getArgument(0));
|
||||
|
||||
ShoppingListItemResponse result = shoppingService.checkItem(
|
||||
HOUSEHOLD_ID, list.getId(), item.getId(), new CheckItemRequest(true), user.getId());
|
||||
|
||||
assertThat(result.isChecked()).isTrue();
|
||||
assertThat(result.checkedBy()).isEqualTo(user.getId());
|
||||
}
|
||||
|
||||
// ── Add Item ──
|
||||
|
||||
@Test
|
||||
void addItemShouldCreateCustomItem() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var list = testShoppingList(household, plan);
|
||||
|
||||
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
|
||||
when(shoppingListItemRepository.save(any(ShoppingListItem.class))).thenAnswer(i -> {
|
||||
ShoppingListItem si = i.getArgument(0);
|
||||
setId(si, ShoppingListItem.class, UUID.randomUUID());
|
||||
return si;
|
||||
});
|
||||
|
||||
ShoppingListItemResponse result = shoppingService.addItem(
|
||||
HOUSEHOLD_ID, list.getId(),
|
||||
new AddItemRequest(null, "Paper towels", new BigDecimal("1"), ""));
|
||||
|
||||
assertThat(result.name()).isEqualTo("Paper towels");
|
||||
assertThat(result.ingredientId()).isNull();
|
||||
}
|
||||
|
||||
// ── Delete Item ──
|
||||
|
||||
@Test
|
||||
void deleteItemShouldRemoveItem() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var list = testShoppingList(household, plan);
|
||||
var ingredient = testIngredient(household, "Tomatoes", false);
|
||||
var item = testItem(list, ingredient, new BigDecimal("5.00"), "pcs");
|
||||
list.getItems().add(item);
|
||||
|
||||
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
|
||||
|
||||
shoppingService.deleteItem(HOUSEHOLD_ID, list.getId(), item.getId());
|
||||
|
||||
verify(shoppingListItemRepository).delete(item);
|
||||
assertThat(list.getItems()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteItemShouldThrowWhenListIsPublished() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var list = testShoppingList(household, plan);
|
||||
list.setStatus("published");
|
||||
var ingredient = testIngredient(household, "Tomatoes", false);
|
||||
var item = testItem(list, ingredient, new BigDecimal("5.00"), "pcs");
|
||||
list.getItems().add(item);
|
||||
|
||||
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
|
||||
|
||||
assertThatThrownBy(() -> shoppingService.deleteItem(HOUSEHOLD_ID, list.getId(), item.getId()))
|
||||
.isInstanceOf(ValidationException.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user