diff --git a/src/main/java/server/poptato/auth/exception/AuthException.java b/src/main/java/server/poptato/auth/exception/AuthException.java new file mode 100644 index 0000000..920e6dc --- /dev/null +++ b/src/main/java/server/poptato/auth/exception/AuthException.java @@ -0,0 +1,14 @@ +package server.poptato.auth.exception; + +import lombok.Getter; +import server.poptato.global.response.status.ResponseStatus; + +@Getter +public class AuthException extends RuntimeException{ + private final ResponseStatus exceptionStatus; + + public AuthException(ResponseStatus exceptionStatus) { + super(exceptionStatus.getMessage()); + this.exceptionStatus = exceptionStatus; + } +} diff --git a/src/main/java/server/poptato/auth/exception/errorcode/AuthExceptionErrorCode.java b/src/main/java/server/poptato/auth/exception/errorcode/AuthExceptionErrorCode.java new file mode 100644 index 0000000..9f25121 --- /dev/null +++ b/src/main/java/server/poptato/auth/exception/errorcode/AuthExceptionErrorCode.java @@ -0,0 +1,37 @@ +package server.poptato.auth.exception.errorcode; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import server.poptato.global.response.status.ResponseStatus; + +@RequiredArgsConstructor +public enum AuthExceptionErrorCode implements ResponseStatus { + + /** + * 6000: Auth 도메인 오류 + */ + + TOKEN_NOT_EXIST(6000, HttpStatus.BAD_REQUEST.value(), "토큰 값이 필요합니다."), + TOKEN_TIME_EXPIRED(6001, HttpStatus.BAD_REQUEST.value(), "토큰이 만료되었습니다"), + INVALID_TOKEN(6002, HttpStatus.BAD_REQUEST.value(), "토큰이 유효하지 않습니다"); + + private final int code; + private final int status; + private final String message; + + + @Override + public int getCode() { + return code; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/server/poptato/auth/exception/handler/AuthExceptionHandler.java b/src/main/java/server/poptato/auth/exception/handler/AuthExceptionHandler.java new file mode 100644 index 0000000..9b928a1 --- /dev/null +++ b/src/main/java/server/poptato/auth/exception/handler/AuthExceptionHandler.java @@ -0,0 +1,22 @@ +package server.poptato.auth.exception.handler; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import server.poptato.auth.exception.AuthException; +import server.poptato.global.response.BaseErrorResponse; + +@Slf4j +@Order(0) +@RestControllerAdvice +public class AuthExceptionHandler { + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(AuthException.class) + public BaseErrorResponse handleAuthException(AuthException e) { + log.error("[UserException: handle_UserException 호출]", e); + return new BaseErrorResponse(e.getExceptionStatus(), e.getMessage()); + } +} diff --git a/src/main/java/server/poptato/user/api/UserController.java b/src/main/java/server/poptato/user/api/UserController.java index bbe2bf2..056de09 100644 --- a/src/main/java/server/poptato/user/api/UserController.java +++ b/src/main/java/server/poptato/user/api/UserController.java @@ -1,10 +1,10 @@ package server.poptato.user.api; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; import server.poptato.global.response.BaseResponse; +import server.poptato.user.api.request.UserChangeNameRequestDto; import server.poptato.user.application.service.UserService; import server.poptato.user.resolver.UserId; @@ -20,6 +20,11 @@ public class UserController { @DeleteMapping public BaseResponse deleteUser(@UserId Long userId) { userService.deleteUser(userId); - return new BaseResponse(SUCCESS); + return new BaseResponse(); + } + @PatchMapping("/mypage") + public BaseResponse updateUserName(@UserId Long userId, @Validated @RequestBody UserChangeNameRequestDto request) { + userService.updateUserName(userId, request.getNewName()); + return new BaseResponse(); } } \ No newline at end of file diff --git a/src/main/java/server/poptato/user/api/request/UserChangeNameRequestDto.java b/src/main/java/server/poptato/user/api/request/UserChangeNameRequestDto.java new file mode 100644 index 0000000..442f590 --- /dev/null +++ b/src/main/java/server/poptato/user/api/request/UserChangeNameRequestDto.java @@ -0,0 +1,16 @@ +package server.poptato.user.api.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class UserChangeNameRequestDto { + @NotBlank(message = "이름은 빈 값일 수 없습니다.") + String newName; +} diff --git a/src/main/java/server/poptato/user/application/service/UserService.java b/src/main/java/server/poptato/user/application/service/UserService.java index 37f71e3..ea87864 100644 --- a/src/main/java/server/poptato/user/application/service/UserService.java +++ b/src/main/java/server/poptato/user/application/service/UserService.java @@ -40,4 +40,13 @@ public Optional findUserByKakaoId(String kakaoId) { entityManager.clear(); // 영속성 컨텍스트 초기화 return userRepository.findByKakaoId(kakaoId); } + @Transactional + public void updateUserName(Long userId, String newName) { + // 사용자 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserException(UserExceptionErrorCode.USER_NOT_EXIST)); + + // name 업데이트 + user.changeName(newName); + } } diff --git a/src/main/java/server/poptato/user/domain/entity/User.java b/src/main/java/server/poptato/user/domain/entity/User.java index 4a4d0b2..1d1eaaf 100644 --- a/src/main/java/server/poptato/user/domain/entity/User.java +++ b/src/main/java/server/poptato/user/domain/entity/User.java @@ -38,4 +38,8 @@ public class User { @LastModifiedDate // 엔티티가 수정될 때 시간 자동 저장 private LocalDateTime modifyDate; + // name 변경 메서드 + public void changeName(String newName) { + this.name = newName; + } } diff --git a/src/main/java/server/poptato/user/resolver/UserResolver.java b/src/main/java/server/poptato/user/resolver/UserResolver.java index f4907be..5ac874f 100644 --- a/src/main/java/server/poptato/user/resolver/UserResolver.java +++ b/src/main/java/server/poptato/user/resolver/UserResolver.java @@ -10,10 +10,12 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import server.poptato.auth.application.service.JwtService; +import server.poptato.auth.exception.AuthException; import server.poptato.global.exception.BaseException; import server.poptato.global.response.BaseErrorResponse; import server.poptato.user.resolver.UserId; +import static server.poptato.auth.exception.errorcode.AuthExceptionErrorCode.*; import static server.poptato.global.exception.errorcode.BaseExceptionErrorCode.*; @@ -32,17 +34,17 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m final HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); final String token = request.getHeader("Authorization"); if (token == null || token.isBlank() || !token.startsWith("Bearer ")) { - throw new BaseException(TOKEN_NOT_CONTAINED_EXCEPTION); + throw new AuthException(TOKEN_NOT_EXIST); } final String encodedUserId = token.substring("Bearer ".length()); if (!jwtService.verifyToken(encodedUserId)) { - throw new BaseException(TOKEN_TIME_EXPIRED_EXCEPTION); + throw new AuthException(TOKEN_TIME_EXPIRED); } final String decodedUserId = jwtService.getUserIdInToken(encodedUserId); try { return Long.parseLong(decodedUserId); } catch (NumberFormatException e) { - return new BaseErrorResponse(INVALID_TOKEN_EXCEPTION); + return new AuthException(INVALID_TOKEN); } } } diff --git a/src/test/java/server/poptato/user/api/UserControllerTest.java b/src/test/java/server/poptato/user/api/UserControllerTest.java index 3bcea5e..357cddb 100644 --- a/src/test/java/server/poptato/user/api/UserControllerTest.java +++ b/src/test/java/server/poptato/user/api/UserControllerTest.java @@ -1,5 +1,7 @@ package server.poptato.user.api; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.Validator; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -9,16 +11,17 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import server.poptato.auth.application.service.JwtService; -import server.poptato.todo.infra.repository.JpaTodoRepository; +import server.poptato.todo.application.TodoService; +import server.poptato.user.api.request.UserChangeNameRequestDto; import server.poptato.user.application.service.UserService; -import server.poptato.user.infra.repository.JpaUserRepository; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -28,44 +31,63 @@ class UserControllerTest { @Autowired private MockMvc mockMvc; - @MockBean - private JwtService jwtService; - + private TodoService todoService; @MockBean private UserService userService; - - @MockBean - private JpaUserRepository userRepository; - - @MockBean - private JpaTodoRepository todoRepository; - + @Autowired + private JwtService jwtService; @MockBean private RedisTemplate redisTemplate; - + private Validator validator; private String accessToken; - private final Long userId = 1L; + private final String userId = "1"; + private ObjectMapper objectMapper; @BeforeEach - void createAccessToken() { - accessToken = jwtService.createAccessToken(userId.toString()); + void userId가_1인_액세스토큰_생성() { + accessToken = jwtService.createAccessToken(userId); } @AfterEach - void deleteRefreshToken() { - jwtService.deleteRefreshToken(userId.toString()); + void 액세스토큰_비활성화() { + jwtService.deleteRefreshToken(userId); + } + @Test + @DisplayName("사용자 이름 변경 성공 테스트") + void updateUserName_ShouldReturnSuccess() throws Exception { + + // when & then + mockMvc.perform(patch("/user/mypage") + .header("Authorization", "Bearer " + accessToken) // JWT 토큰을 전달하는 부분 + .contentType(MediaType.APPLICATION_JSON) + .content("{\"newName\": \"NewName\"}")) // content를 "NewName"으로 수정 + .andExpect(status().isOk()); + + // userService의 updateUserName이 올바르게 호출되는지 확인 + verify(userService, times(1)).updateUserName(anyLong(), eq("NewName")); // "NewName"과 일치하도록 수정 } + @Test + @DisplayName("사용자 이름 변경 실패 - 이름이 빈 값일 때") + void updateUserName_ShouldReturnBadRequest_WhenNameIsEmpty() throws Exception { + // given + Long userId = 1L; + UserChangeNameRequestDto requestDto = new UserChangeNameRequestDto(""); // 빈 이름 + + // when & then + mockMvc.perform(patch("/user/mypage") + .header("Authorization", "Bearer "+accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"newName\": \"\"}")) + .andExpect(status().isBadRequest()); + + // userService가 호출되지 않는지 확인 + verify(userService, times(0)).updateUserName(anyLong(), anyString()); + } @Test @DisplayName("회원 탈퇴 성공 - 토큰 검증 후 응답 확인") void deleteUserSuccess() throws Exception { - // 토큰 검증 성공 - when(jwtService.verifyToken(anyString())).thenReturn(true); - when(jwtService.getUserIdInToken(anyString())).thenReturn(userId.toString()); - - // UserService의 deleteUser()가 호출되는지 확인 - doNothing().when(userService).deleteUser(userId); mockMvc.perform(MockMvcRequestBuilders.delete("/user") .header("Authorization", "Bearer " + accessToken)) @@ -86,9 +108,6 @@ void deleteUserFailureNoToken() throws Exception { void deleteUserFailureInvalidToken() throws Exception { String invalidToken = "invalidToken"; - // 토큰 검증 실패 - when(jwtService.verifyToken(anyString())).thenReturn(false); - mockMvc.perform(MockMvcRequestBuilders.delete("/user") .header("Authorization", "Bearer " + invalidToken)) .andExpect(status().isBadRequest()) diff --git a/src/test/java/server/poptato/user/application/UserServiceTest.java b/src/test/java/server/poptato/user/application/UserServiceTest.java new file mode 100644 index 0000000..6534b40 --- /dev/null +++ b/src/test/java/server/poptato/user/application/UserServiceTest.java @@ -0,0 +1,58 @@ +package server.poptato.user.application; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import server.poptato.user.application.service.UserService; +import server.poptato.user.domain.entity.User; +import server.poptato.user.domain.repository.UserRepository; +import server.poptato.user.exception.UserException; +import server.poptato.user.exception.errorcode.UserExceptionErrorCode; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +class UserServiceTest { + + @Autowired + private UserService userService; + + @Autowired + private UserRepository userRepository; + + @Test + @DisplayName("사용자 이름 변경 성공 테스트") + @Transactional + void updateUserName_ShouldChangeUserName() { + // given + Long userId = 1L; + String newName = "New Name"; + + // when + userService.updateUserName(userId, newName); + + // then + User updatedUser = userRepository.findById(userId).orElseThrow(); + assertThat(updatedUser.getName()).isEqualTo(newName); + } + + @Test + @DisplayName("존재하지 않는 사용자일 경우 예외 발생 테스트") + @Transactional + void updateUserName_ShouldThrowException_WhenUserNotFound() { + // given + Long nonExistentUserId = 999L; // 존재하지 않는 유저 ID + String newName = "New Name"; + + // when & then + UserException exception = assertThrows(UserException.class, () -> { + userService.updateUserName(nonExistentUserId, newName); + }); + + assertThat(exception.getMessage()).isEqualTo(UserExceptionErrorCode.USER_NOT_EXIST.getMessage()); + } +} +