diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/AdminService.java b/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/AdminService.java new file mode 100644 index 00000000..3060dfe3 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/AdminService.java @@ -0,0 +1,33 @@ +package org.ezcode.codetest.application.usermanagement.user.service; + +import org.ezcode.codetest.application.usermanagement.user.dto.response.GrantAdminRoleResponse; +import org.ezcode.codetest.domain.user.exception.AdminException; +import org.ezcode.codetest.domain.user.exception.code.AdminExceptionCode; +import org.ezcode.codetest.domain.user.model.entity.AuthUser; +import org.ezcode.codetest.domain.user.model.entity.User; +import org.ezcode.codetest.domain.user.model.enums.UserRole; +import org.ezcode.codetest.domain.user.service.UserDomainService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AdminService { + private final UserDomainService userDomainService; + + @Transactional + public GrantAdminRoleResponse grantAdminRole(AuthUser authUser, Long userId) { + if (authUser.getId().equals(userId)) { + throw new AdminException(AdminExceptionCode.GRANT_ADMIN_SELF); + } + User user = userDomainService.getUserById(userId); + if (user.getRole().equals(UserRole.ADMIN)) { + throw new AdminException(AdminExceptionCode.ALREADY_ADMIN_USER); + } + user.modifyUserRole(UserRole.ADMIN); + + return new GrantAdminRoleResponse("ADMIN 권한을 부여합니다"); + } +} diff --git a/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java b/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java index 317c5617..f97311bb 100644 --- a/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java +++ b/src/main/java/org/ezcode/codetest/application/usermanagement/user/service/UserService.java @@ -176,18 +176,4 @@ public UserProfileImageResponse deleteUserProfileImage(AuthUser authUser) { return new UserProfileImageResponse(null); } - - @Transactional - public GrantAdminRoleResponse grantAdminRole(AuthUser authUser, Long userId) { - if (authUser.getId().equals(userId)) { - throw new UserException(UserExceptionCode.GRANT_ADMIN_SELF); - } - User user = userDomainService.getUserById(userId); - if (user.getRole().equals(UserRole.ADMIN)) { - throw new UserException(UserExceptionCode.ALREADY_ADMIN_USER); - } - user.modifyUserRole(UserRole.ADMIN); - - return new GrantAdminRoleResponse("ADMIN 권한을 부여합니다"); - } } diff --git a/src/main/java/org/ezcode/codetest/domain/user/exception/AdminException.java b/src/main/java/org/ezcode/codetest/domain/user/exception/AdminException.java new file mode 100644 index 00000000..cf5c6ca0 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/domain/user/exception/AdminException.java @@ -0,0 +1,20 @@ +package org.ezcode.codetest.domain.user.exception; + +import org.ezcode.codetest.common.base.exception.ResponseCode; +import org.ezcode.codetest.domain.user.exception.code.AdminExceptionCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; + +@Getter +public class AdminException extends RuntimeException { + private final AdminExceptionCode responseCode; + private final HttpStatus httpStatus; + private final String message; + + public AdminException(AdminExceptionCode responseCode) { + this.responseCode = responseCode; + this.httpStatus = responseCode.getStatus(); + this.message = responseCode.getMessage(); + } +} diff --git a/src/main/java/org/ezcode/codetest/domain/user/exception/code/AdminExceptionCode.java b/src/main/java/org/ezcode/codetest/domain/user/exception/code/AdminExceptionCode.java new file mode 100644 index 00000000..3b4495aa --- /dev/null +++ b/src/main/java/org/ezcode/codetest/domain/user/exception/code/AdminExceptionCode.java @@ -0,0 +1,18 @@ +package org.ezcode.codetest.domain.user.exception.code; + +import org.ezcode.codetest.common.base.exception.ResponseCode; +import org.springframework.http.HttpStatus; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AdminExceptionCode implements ResponseCode { + GRANT_ADMIN_SELF(false, HttpStatus.BAD_REQUEST, "본인에게 ADMIN 권한을 부여할 수 없습니다."), + ALREADY_ADMIN_USER(false, HttpStatus.BAD_REQUEST, "이미 ADMIN 권한을 가진 유저입니다."); + + private final boolean success; + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/org/ezcode/codetest/domain/user/exception/code/UserExceptionCode.java b/src/main/java/org/ezcode/codetest/domain/user/exception/code/UserExceptionCode.java index 6e896edc..d9638efb 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/exception/code/UserExceptionCode.java +++ b/src/main/java/org/ezcode/codetest/domain/user/exception/code/UserExceptionCode.java @@ -14,9 +14,9 @@ public enum UserExceptionCode implements ResponseCode { NOT_MATCH_CODE(false, HttpStatus.BAD_REQUEST, "이메일 인증 코드가 일치하지 않습니다."), NO_GITHUB_INFO(false, HttpStatus.BAD_REQUEST, "깃허브 정보가 없습니다."), NO_GITHUB_REPO(false, HttpStatus.BAD_REQUEST, "해당하는 Repository를 찾을 수 없습니다."), - GRANT_ADMIN_SELF(false, HttpStatus.BAD_REQUEST, "본인에게 ADMIN 권한을 부여할 수 없습니다."), - ALREADY_ADMIN_USER(false, HttpStatus.BAD_REQUEST, "이미 ADMIN 권한을 가진 유저입니다."); + + ; private final boolean success; private final HttpStatus status; private final String message; diff --git a/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java b/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java index 1fafd203..7764d2a5 100644 --- a/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java +++ b/src/main/java/org/ezcode/codetest/domain/user/service/UserDomainService.java @@ -7,7 +7,6 @@ import org.ezcode.codetest.domain.user.exception.UserException; import org.ezcode.codetest.domain.user.exception.code.UserExceptionCode; import org.ezcode.codetest.domain.user.model.entity.UserAuthType; -import org.ezcode.codetest.domain.user.model.entity.UserGithubInfo; import org.ezcode.codetest.domain.user.model.enums.Adjective; import org.ezcode.codetest.domain.user.model.enums.AuthType; import org.ezcode.codetest.domain.user.model.enums.Noun; @@ -15,7 +14,6 @@ import org.ezcode.codetest.domain.user.exception.code.AuthExceptionCode; import org.ezcode.codetest.domain.user.model.entity.User; import org.ezcode.codetest.domain.user.repository.UserAuthTypeRepository; -import org.ezcode.codetest.domain.user.repository.UserGithubInfoRepository; import org.ezcode.codetest.domain.user.repository.UserRepository; import org.ezcode.codetest.common.security.util.PasswordEncoder; import org.springframework.stereotype.Service; diff --git a/src/main/java/org/ezcode/codetest/presentation/usermanagement/AdminController.java b/src/main/java/org/ezcode/codetest/presentation/usermanagement/AdminController.java new file mode 100644 index 00000000..c1e75e93 --- /dev/null +++ b/src/main/java/org/ezcode/codetest/presentation/usermanagement/AdminController.java @@ -0,0 +1,33 @@ +package org.ezcode.codetest.presentation.usermanagement; + +import org.ezcode.codetest.application.usermanagement.user.dto.response.GrantAdminRoleResponse; +import org.ezcode.codetest.application.usermanagement.user.service.AdminService; +import org.ezcode.codetest.domain.user.model.entity.AuthUser; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/admin") +@RequiredArgsConstructor +@Tag(name = "관리자(Admin) 전용 기능", description = "관리자 권한을 가진 유저만 접근 가능한 기능입니다") +public class AdminController { + private final AdminService adminService; + + @Operation(summary = "관리자로 전환", description = "관리자 권한을 가지고 있는 유저는 다른 유저의 권한을 관리자로 수정할 수 있습니다.") + @PostMapping("/users/{userId}/grant-admin") + public ResponseEntity grantAdminRole( + @AuthenticationPrincipal AuthUser authUser, + @PathVariable Long userId + ){ + return ResponseEntity.status(HttpStatus.OK).body(adminService.grantAdminRole(authUser, userId)); + } +} diff --git a/src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java b/src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java index 1266aba5..8b4e8128 100644 --- a/src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java +++ b/src/main/java/org/ezcode/codetest/presentation/usermanagement/UserController.java @@ -94,12 +94,4 @@ public ResponseEntity withdraw( return ResponseEntity.status(HttpStatus.OK).body(userService.withdrawUser(authUser)); } - @Operation(summary = "유저 권한 전환", description = "관리자 권한을 가지고 있는 유저는 다른 유저의 권한을 수정할 수 있습니다.") - @PostMapping("/admin/users/{userId}/grant-admin") - public ResponseEntity grantAdminRole( - @AuthenticationPrincipal AuthUser authUser, - @PathVariable Long userId - ){ - return ResponseEntity.status(HttpStatus.OK).body(userService.grantAdminRole(authUser, userId)); - } } diff --git a/src/test/java/org/ezcode/codetest/domain/user/UserDomainServiceTest.java b/src/test/java/org/ezcode/codetest/domain/user/UserDomainServiceTest.java new file mode 100644 index 00000000..9b228299 --- /dev/null +++ b/src/test/java/org/ezcode/codetest/domain/user/UserDomainServiceTest.java @@ -0,0 +1,176 @@ +package org.ezcode.codetest.domain.user; + +import org.ezcode.codetest.common.security.util.PasswordEncoder; +import org.ezcode.codetest.domain.user.exception.AuthException; +import org.ezcode.codetest.domain.user.exception.UserException; +import org.ezcode.codetest.domain.user.exception.code.AuthExceptionCode; +import org.ezcode.codetest.domain.user.exception.code.UserExceptionCode; +import org.ezcode.codetest.domain.user.model.entity.User; +import org.ezcode.codetest.domain.user.model.entity.UserAuthType; +import org.ezcode.codetest.domain.user.model.enums.AuthType; +import org.ezcode.codetest.domain.user.model.enums.Tier; +import org.ezcode.codetest.domain.user.model.enums.UserRole; +import org.ezcode.codetest.domain.user.repository.UserAuthTypeRepository; +import org.ezcode.codetest.domain.user.repository.UserRepository; +import org.ezcode.codetest.domain.user.service.UserDomainService; +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 java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class UserDomainServiceTest { + + @Mock + private UserRepository userRepository; + + @Mock + private UserAuthTypeRepository userAuthTypeRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + + @InjectMocks + private UserDomainService userDomainService; + + // 테스트 유저 정보 설정 + private final User testUser = new User( + "test@example.com", + "hashedPassword", + "testUser", + "TestNick", + 30, + Tier.NEWBIE, + UserRole.USER, + false, // isDeleted + true, // verified + "https://github.com/test", + false // gitPushStatus + ) { + public Long getId() { return 1L; } + public int getReviewToken() { return 5; } + public int getZeroReviewToken() { return 0; } + }; + private final UserAuthType testAuthType = new UserAuthType(testUser, AuthType.EMAIL); + + // 1. 이메일 존재 여부 테스트 + @Test + void checkEmailUnique_shouldPassWhenEmailAvailable() { + when(userRepository.findByEmail("new@example.com")).thenReturn(Optional.empty()); + assertDoesNotThrow(() -> userDomainService.checkEmailUnique("new@example.com")); + } + + @Test + void checkEmailUnique_shouldThrowWhenEmailExistsWithAuthType() { + when(userRepository.findByEmail("existing@example.com")).thenReturn(Optional.of(testUser)); + when(userAuthTypeRepository.getUserAuthType(testUser)).thenReturn(List.of(AuthType.EMAIL)); + + AuthException exception = assertThrows(AuthException.class, + () -> userDomainService.checkEmailUnique("existing@example.com")); + assertEquals(AuthExceptionCode.ALREADY_EXIST_USER, exception.getResponseCode()); + } + + // 2. 유저 생성 테스트 + @Test + void createUser_shouldCallRepository() { + userDomainService.createUser(testUser); + verify(userRepository).createUser(testUser); + } + + @Test + void createUserAuthType_shouldCallRepository() { + userDomainService.createUserAuthType(testAuthType); + verify(userAuthTypeRepository).createUserAuthType(testAuthType); + } + + // 3. 유저 존재 테스트 + @Test + void getUser_shouldReturnUserWhenExists() { + when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(testUser)); + User result = userDomainService.getUser("test@example.com"); + assertEquals(testUser, result); + } + + @Test + void getUser_shouldThrowWhenNotFound() { + when(userRepository.findByEmail("unknown@example.com")).thenReturn(Optional.empty()); + assertThrows(AuthException.class, () -> userDomainService.getUser("unknown@example.com")); + } + + // 4. 비번 검증 테스트 + @Test + void userPasswordCheck_shouldPassWhenValid() { + when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(testUser)); + when(passwordEncoder.matches("correct", "hashedPassword")).thenReturn(true); + + assertDoesNotThrow(() -> + userDomainService.userPasswordCheck("test@example.com", "correct")); + } + + @Test + void userPasswordCheck_shouldThrowWhenInvalid() { + when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false); + + when(userRepository.findByEmail("test@example.com")).thenReturn(Optional.of(testUser)); + + AuthException exception = assertThrows(AuthException.class, + () -> userDomainService.userPasswordCheck("test@example.com", "wrong")); + + assertEquals(AuthExceptionCode.PASSWORD_NOT_MATCH, exception.getResponseCode()); + } + + + // 5. 비번 인코딩 테스트 + @Test + void encodePassword_shouldReturnEncodedValue() { + when(passwordEncoder.encode("rawPassword")).thenReturn("encodedPassword"); + assertEquals("encodedPassword", userDomainService.encodePassword("rawPassword")); + } + + // 6. 인증 타입 테스트 + @Test + void getUserAuthTypes_shouldReturnAuthTypes() { + List expectedTypes = List.of(AuthType.EMAIL, AuthType.GOOGLE); + when(userAuthTypeRepository.getUserAuthType(testUser)).thenReturn(expectedTypes); + + assertEquals(expectedTypes, userDomainService.getUserAuthTypes(testUser)); + } + + // 7. 비번 검증 테스트 + @Test + void passwordComparison_shouldThrowWhenSame() { + when(passwordEncoder.matches("newPass", "oldHashed")).thenReturn(true); + assertThrows(AuthException.class, + () -> userDomainService.passwordComparison("newPass", "oldHashed")); + } + + @Test + void passwordComparison_shouldPassWhenDifferent() { + when(passwordEncoder.matches("newPass", "oldHashed")).thenReturn(false); + assertDoesNotThrow(() -> + userDomainService.passwordComparison("newPass", "oldHashed")); + } + + // 8. 탈퇴 회원 테스트 + @Test + void isDeletedUser_shouldThrowWhenDeleted() { + User deletedUser = new User("email@gmail.com","Aa12345**", "username", + "full@week.com", 100, Tier.CODER, UserRole.USER, true, true, "gitUrl.com", true); + assertThrows(AuthException.class, () -> userDomainService.isDeletedUser(deletedUser)); + } + + @Test + void isDeletedUser_shouldPassWhenActive() { + assertDoesNotThrow(() -> userDomainService.isDeletedUser(testUser)); + } + +}