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:
2026-04-01 21:56:51 +02:00
parent 4f457303d8
commit 9ec703abcd
88 changed files with 5267 additions and 0 deletions

View File

@@ -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"));
}
}

View 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());
}
}