관리자 기능
우선 MVP에서는 관리자로 특정 회원들을 삭제하는 권한을 주려고 했다. 기본적으로 관리자 권한을 주려고 하는 것이고, 추후 부가 기능들에 관리자 권한이 많아 질 수 있다. 불량 회원 신고 등 기능을 추가하면 관리자가 경고를 보낸다던지 일시 정지 등 권한을 변경하는 기능을 추가 할 지 생각중이다. 현재는 우선 기본적인 회원 조회 기능과 회원 삭제 기능을 기본으로 구현해두었다.
소스코드
MVP모델이라서 아직 크게 기능은 없는 상태이다. 추후 부가 기능 등을 추가하면서 관리자쪽의 기능은 추가 될 필요성이 있다. 우선 현재 상태의 최소 기능 소스코드는 다음과 같다.
AdminController.java
@Controller
@RequestMapping("/api/admin")
@RequiredArgsConstructor
public class AdminController {
private final AdminService adminService;
@GetMapping
public ResponseEntity<List<AdminResponseDto>> getAllUsers() {
return ResponseEntity.ok(adminService.getAllUsers());
}
@DeleteMapping("/{userId}")
public ResponseEntity<MessageDto> deleteUser(@PathVariable Long userId,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
return ResponseEntity.ok(adminService.deleteUser(userId, userDetails.getUser()));
}
}
Java
복사
AdminService.java
@Service
@RequiredArgsConstructor
public class AdminService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public List<AdminResponseDto> getAllUsers() {
return userRepository.findAll().stream().map(AdminResponseDto::new).toList();
}
@Transactional
public MessageDto deleteUser(Long userId, User loginUser) {
User user = getUserById(userId);
if (!loginUser.equals(user) && loginUser.getRole().equals(UserRoleEnum.ADMIN)) {
throw new IllegalArgumentException("회원을 삭제할 권한이 없습니다.");
}
userRepository.delete(user);
return new MessageDto("삭제가 완료되었습니다");
}
private User getUserById(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(()-> new IllegalArgumentException("회원을 찾을 수 없습니다."));
return user;
}
}
Java
복사
AdminRepository.java
@Repository
public interface AdminRepository extends JpaRepository<User, Long> {
}
Java
복사
Service 단위 테스트코드
주요 비지니스 로직이 담긴 AdminService.java의 테스트 코드를 작성했다. 처음에는 로그인을 생각하지 않고 단순히 기능적인 부분 코드만 작성하고, 실제 AdminService.java 코드를 작성했다. 그 이후 AdminService의 특성상 관리자 전용이라는 특수성 때문에라도 로그인 상태가 필수적이기 때문에 로그인을 필요로하는 테스트코드를 작성하고 그에 따라 본 AdminService.java를 수정했다. 테스트 코드를 먼저 작성하고 코드를 작성하는 방법이 생각보다 좀 더 효율적인것 같은 느낌이 들었다.
아직 본 Service코드가 복잡한 로직을 담고 있지 않기도 하지만 테스트 코드를 잘 작성하고 많은 케이스를 담아야 하는 것에 대한 개념이 부족한 것 같다.
@SpringBootTest
class AdminServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private AdminService adminService;
@Nested
@DisplayName("AdminService - READ - 단순 로직 작동 여부 체크")
class BasicReadTest {
@Test
@DisplayName("회원 전체 조회 성공")
void Admin_getAllUsersSuccess() {
// given
User user1 = new User();
User user2 = new User();
User user3 = new User();
List<User> userList = Arrays.asList(user1, user2, user3);
when(userRepository.findAll()).thenReturn(userList);
// when
List<AdminResponseDto> result = adminService.getAllUsers();
// then
assertThat(result).hasSize(3);
}
@Test
@DisplayName("회원 전체 조회 실패 - 등록된 회원이 없는 경우")
void Admin_getAllUsersFail() {
// given
List<User> emptyUserList = Collections.emptyList();
when(userRepository.findAll()).thenReturn(emptyUserList);
// when
List<AdminResponseDto> result = adminService.getAllUsers();
// then
assertThat(result).isEmpty();
}
}
@Nested
@DisplayName("AdminService - DELETE - 단순 로직 작동 여부 체크")
class BasicDeleteTest {
@Test
@DisplayName("유저 삭제 성공")
void deleteUserSuccess() {
// given
User user = User.builder()
.userId(1L)
.username("user1")
.password("pass1")
.role(UserRoleEnum.USER)
.build();
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
doNothing().when(userRepository).delete(user);
// when
MessageDto result = adminService.deleteUser(1L, user);
// then
assertThat(result.getMsg()).isEqualTo("삭제가 완료되었습니다");
verify(userRepository, times(1)).delete(user);
}
@Test
@DisplayName("유저 삭제 실패 - 존재하지 않는 유저 삭제 시도")
void deleteNonExistingUserFail() {
// given
when(userRepository.findById(1L)).thenReturn(Optional.empty());
// when & then
assertThrows(IllegalArgumentException.class,
() -> adminService.deleteUser(1L, any(User.class)),
"회원을 찾을 수 없습니다.");
}
}
@Nested
@DisplayName("AdminService - READ - 로그인된 회원의 ROLE에 대한 테스트")
class LoginnedTest {
@Test
@DisplayName("회원 전체 조회 성공 - ADMIN으로 로그인한 경우에 성공")
void Admin_getAllUsersSuccessWithAdminRoleSuccess() {
// given
User adminUser = User.builder()
.userId(1L)
.username("admin")
.password("adminPass")
.role(UserRoleEnum.valueOf("ADMIN"))
.build();
List<User> userList = Arrays.asList(new User(), new User(), new User());
when(userRepository.findAll()).thenReturn(userList);
// 가상의 관리자로 로그인 상태를 설정
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Authentication authentication = Mockito.mock(Authentication.class);
UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser);
SecurityContextHolder.setContext(securityContext);
when(securityContext.getAuthentication()).thenReturn(authentication);
when(authentication.getPrincipal()).thenReturn(adminUserDetails);
// when
List<AdminResponseDto> result = adminService.getAllUsers();
// then
assertThat(result).hasSize(3);
}
@Test
@DisplayName("회원 전체 조회 실패 - USER로 로그인한 경우에 조회 실패")
void Admin_getAllUsersFailWithUserRole() {
// given
User user = User.builder()
.userId(2L)
.username("user")
.password("userPass")
.role(UserRoleEnum.valueOf("USER"))
.build();
List<User> emptyUserList = Collections.emptyList();
when(userRepository.findAll()).thenReturn(emptyUserList);
// 가상의 USER로 로그인 상태를 설정
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Authentication authentication = Mockito.mock(Authentication.class);
UserDetailsImpl userDetails = new UserDetailsImpl(user);
SecurityContextHolder.setContext(securityContext);
when(securityContext.getAuthentication()).thenReturn(authentication);
when(authentication.getPrincipal()).thenReturn(userDetails);
// when
List<AdminResponseDto> result = adminService.getAllUsers();
// then
assertThat(result).isEmpty();
}
}
@Nested
@DisplayName("AdminService - DELETE - 로그인된 회원의 ROLE에 대한 테스트")
class LoginnedDeleteTest {
@Test
@DisplayName("유저 삭제 성공 - 관리자로 로그인한 경우")
void deleteUserSuccessWithAdminRole() {
// given
User adminUser = User.builder()
.userId(1L)
.username("admin")
.password("adminPass")
.role(UserRoleEnum.ADMIN)
.build();
User userToDelete = User.builder()
.userId(3L)
.username("userToDelete")
.password("password")
.role(UserRoleEnum.USER)
.build();
when(userRepository.findById(3L)).thenReturn(Optional.of(userToDelete));
// 가상의 ADMIN으로 로그인 상태를 설정
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Authentication authentication = Mockito.mock(Authentication.class);
UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser);
SecurityContextHolder.setContext(securityContext);
when(securityContext.getAuthentication()).thenReturn(authentication);
when(authentication.getPrincipal()).thenReturn(adminUserDetails);
// when
MessageDto msg = adminService.deleteUser(3L, userToDelete);
// then
assertThat(msg.getMsg()).isEqualTo("삭제가 완료되었습니다");
}
@Test
@DisplayName("유저 삭제 실패 - 존재하지 않는 유저 삭제 시도")
void deleteNonExistingUserFail() {
// given
User adminUser = User.builder()
.userId(1L)
.username("admin")
.password("adminPass")
.role(UserRoleEnum.ADMIN)
.build();
when(userRepository.findById(3L)).thenReturn(Optional.empty());
// 가상의 관리자로 로그인 상태를 설정
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Authentication authentication = Mockito.mock(Authentication.class);
UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser);
SecurityContextHolder.setContext(securityContext);
when(securityContext.getAuthentication()).thenReturn(authentication);
when(authentication.getPrincipal()).thenReturn(adminUserDetails);
// when & then
assertThrows(IllegalArgumentException.class,
() -> adminService.deleteUser(3L, any(User.class)),
"회원을 찾을 수 없습니다.");
}
}
}
Java
복사
Controller 단위 테스트 코드
요청으로부터 정상적으로 요청에 따라 Controller가 작동하는지 테스트 코드이다. 이것도 Service 단위 테스트와 마찬가지로 Mockito 모의 객체를 활용해서 AdminController의 작동 부분에 대한 테스트를 생각해서 작성해보았다. 이것도 마찬가지로 첫 단순 로직 테스트를 먼저 작성해보고, 그에 따라 실제 AdminController.java를 구성, 이후 로그인을 고려하여 다시 발전시키는 방법으로 해보았다.
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@SpringBootTest
@AutoConfigureTestDatabase
@AutoConfigureMockMvc(addFilters = false)
class AdminControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private AdminService adminService;
@Autowired
private final ObjectMapper objectMapper = new ObjectMapper();
User user1 = User.builder()
.userId(1L)
.username("user1")
.password("1234")
.role(UserRoleEnum.USER)
.build();
User user2 = User.builder()
.userId(2L)
.username("user2")
.password("1234")
.role(UserRoleEnum.USER)
.build();
User adminUser = User.builder()
.userId(3L)
.username("admin")
.password("1234")
.role(UserRoleEnum.ADMIN)
.build();
UserDetails authenticateUser(User user) {
UserDetailsImpl userDetails = new UserDetailsImpl(user);
Authentication authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
return userDetails;
}
@Nested
@DisplayName("AdminController - 단순 로직 작동 여부 테스트")
class BasicControllerReadTest {
@Test
@DisplayName("단순_회원조회_테스트")
void 단순_회원조회_테스트() throws Exception {
// given
List<AdminResponseDto> userList = new ArrayList<>();
userList.add(new AdminResponseDto(user1));
userList.add(new AdminResponseDto(user2));
userList.add(new AdminResponseDto(adminUser));
// when
when(adminService.getAllUsers())
.thenReturn(userList);
// then
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk())
.andDo(print());
}
@Test
@DisplayName("단순_회원삭제_테스트")
void 단순_회원삭제_테스트() throws Exception {
// given
MessageDto msg = MessageDto.builder()
.msg("회원 삭제 성공")
.build();
// when
when(adminService.deleteUser(user1.getUserId(), user1)).thenReturn(msg);
// then
mockMvc.perform(delete("/api/admin/{userId}", user1.getUserId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(authenticateUser(user1))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.msg").value("회원 삭제 성공"));
}
}
@Nested
@DisplayName("AdminController - 로그인된 회원의 ROLE에 대한 테스트")
class LoginnedControllerTest {
@Test
@DisplayName("회원 조회 성공 - ADMIN으로 로그인한 경우에 성공")
void 관리자_회원목록조회_테스트() throws Exception {
// given
List<AdminResponseDto> userList = new ArrayList<>();
userList.add(new AdminResponseDto(user1));
userList.add(new AdminResponseDto(user2));
userList.add(new AdminResponseDto(adminUser));
// 가상의 ADMIN으로 로그인 상태를 설정
UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser);
Authentication authentication = new UsernamePasswordAuthenticationToken(adminUserDetails, null, adminUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
// when
when(adminService.getAllUsers())
.thenReturn(userList);
// then
mockMvc.perform(get("/api/admin"))
.andExpect(status().isOk())
.andDo(print());
}
@Test
@DisplayName("회원 삭제 성공 - ADMIN으로 로그인한 경우에 성공")
void 관리자_회원삭제_테스트() throws Exception {
// given
MessageDto msg = MessageDto.builder()
.msg("관리자 권한 회원 삭제 성공")
.build();
// 가상의 ADMIN으로 로그인 상태를 설정
UserDetailsImpl adminUserDetails = new UserDetailsImpl(adminUser);
Authentication authentication = new UsernamePasswordAuthenticationToken(adminUserDetails, null, adminUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
// when
when(adminService.deleteUser(user1.getUserId(), adminUser)).thenReturn(msg);
// then
mockMvc.perform(delete("/api/admin/{userId}", 1L)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(authenticateUser(adminUser))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.msg").value("관리자 권한 회원 삭제 성공"));
}
}
}
Java
복사