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>
171 lines
7.5 KiB
Java
171 lines
7.5 KiB
Java
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());
|
|
}
|
|
}
|