diff --git a/src/main/java/com/mangkyu/employment/interview/app/common/erros/errorcode/CommonErrorCode.java b/src/main/java/com/mangkyu/employment/interview/app/common/erros/errorcode/CommonErrorCode.java new file mode 100644 index 0000000..5672d03 --- /dev/null +++ b/src/main/java/com/mangkyu/employment/interview/app/common/erros/errorcode/CommonErrorCode.java @@ -0,0 +1,19 @@ +package com.mangkyu.employment.interview.app.common.erros.errorcode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CommonErrorCode implements ErrorCode { + + INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid parameter included"), + RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource not exists"), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"), + ; + + private final HttpStatus httpStatus; + private final String message; + +} \ No newline at end of file diff --git a/src/main/java/com/mangkyu/employment/interview/app/common/erros/errorcode/CustomErrorCode.java b/src/main/java/com/mangkyu/employment/interview/app/common/erros/errorcode/CustomErrorCode.java new file mode 100644 index 0000000..6f3f7a1 --- /dev/null +++ b/src/main/java/com/mangkyu/employment/interview/app/common/erros/errorcode/CustomErrorCode.java @@ -0,0 +1,18 @@ +package com.mangkyu.employment.interview.app.common.erros.errorcode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CustomErrorCode implements ErrorCode { + + INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid parameter included"), + RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Cannot find resource."), + ; + + private final HttpStatus httpStatus; + private final String message; + +} \ No newline at end of file diff --git a/src/main/java/com/mangkyu/employment/interview/app/common/erros/errorcode/ErrorCode.java b/src/main/java/com/mangkyu/employment/interview/app/common/erros/errorcode/ErrorCode.java new file mode 100644 index 0000000..44dc902 --- /dev/null +++ b/src/main/java/com/mangkyu/employment/interview/app/common/erros/errorcode/ErrorCode.java @@ -0,0 +1,13 @@ +package com.mangkyu.employment.interview.app.common.erros.errorcode; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + + String name(); + + HttpStatus getHttpStatus(); + + String getMessage(); + +} diff --git a/src/main/java/com/mangkyu/employment/interview/app/common/erros/exception/QuizException.java b/src/main/java/com/mangkyu/employment/interview/app/common/erros/exception/QuizException.java new file mode 100644 index 0000000..f94af6b --- /dev/null +++ b/src/main/java/com/mangkyu/employment/interview/app/common/erros/exception/QuizException.java @@ -0,0 +1,14 @@ +package com.mangkyu.employment.interview.app.common.erros.exception; + + +import com.mangkyu.employment.interview.app.common.erros.errorcode.ErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class QuizException extends Exception { + + private final ErrorCode errorCode; + +} diff --git a/src/main/java/com/mangkyu/employment/interview/app/common/erros/handler/GlobalExceptionHandler.java b/src/main/java/com/mangkyu/employment/interview/app/common/erros/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..7841e83 --- /dev/null +++ b/src/main/java/com/mangkyu/employment/interview/app/common/erros/handler/GlobalExceptionHandler.java @@ -0,0 +1,119 @@ +package com.mangkyu.employment.interview.app.common.erros.handler; + +import com.mangkyu.employment.interview.app.common.erros.errorcode.CommonErrorCode; +import com.mangkyu.employment.interview.app.common.erros.errorcode.ErrorCode; +import com.mangkyu.employment.interview.app.common.erros.response.ErrorResponse; +import com.mangkyu.employment.interview.app.common.erros.exception.QuizException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.List; +import java.util.stream.Collectors; + +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler(QuizException.class) + public ResponseEntity handleQuizException(final QuizException e) { + final ErrorCode errorCode = e.getErrorCode(); + return handleExceptionInternal(errorCode); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgument(final IllegalArgumentException e) { + final ErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER; + return handleExceptionInternal(errorCode, e.getMessage()); + } + + @Override + public ResponseEntity handleMethodArgumentNotValid( + final MethodArgumentNotValidException e, + final HttpHeaders headers, + final HttpStatus status, + final WebRequest request) { + + final ErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER; + return handleExceptionInternal(e, errorCode); + } + +// @ExceptionHandler({ConstraintViolationException.class}) +// public ResponseEntity handleConstraintViolation(final ConstraintViolationException e) { +// final ErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER; +// return handleExceptionInternal(e, errorCode); +// } + + @ExceptionHandler({Exception.class}) + public ResponseEntity handleAllException(final Exception ex) { + final ErrorCode errorCode = CommonErrorCode.INTERNAL_SERVER_ERROR; + return handleExceptionInternal(errorCode); + } + + private ResponseEntity handleExceptionInternal(final ErrorCode errorCode) { + return ResponseEntity.status(errorCode.getHttpStatus()) + .body(makeErrorResponse(errorCode)); + } + + private ErrorResponse makeErrorResponse(final ErrorCode errorCode) { + return ErrorResponse.builder() + .code(errorCode.name()) + .message(errorCode.getMessage()) + .build(); + } + + private ResponseEntity handleExceptionInternal(final ErrorCode errorCode, final String message) { + return ResponseEntity.status(errorCode.getHttpStatus()) + .body(makeErrorResponse(errorCode, message)); + } + + private ErrorResponse makeErrorResponse(final ErrorCode errorCode, final String message) { + return ErrorResponse.builder() + .code(errorCode.name()) + .message(message) + .build(); + } + + private ResponseEntity handleExceptionInternal(final BindException e, final ErrorCode errorCode) { + return ResponseEntity.status(errorCode.getHttpStatus()) + .body(makeErrorResponse(e, errorCode)); + } + + private ErrorResponse makeErrorResponse(final BindException e, final ErrorCode errorCode) { + final List validationErrorList = e.getBindingResult() + .getFieldErrors() + .stream() + .map(ErrorResponse.ValidationError::of) + .collect(Collectors.toList()); + + return ErrorResponse.builder() + .code(errorCode.name()) + .message(errorCode.getMessage()) + .errors(validationErrorList) + .build(); + } + +// private ResponseEntity handleExceptionInternal(final ConstraintViolationException e, final ErrorCode errorCode) { +// return ResponseEntity.status(errorCode.getHttpStatus()) +// .body(makeErrorResponse(e, errorCode)); +// } + +// private ErrorResponse makeErrorResponse(final ConstraintViolationException e, final ErrorCode errorCode) { +// final List validationErrorList = e.getConstraintViolations() +// .stream() +// .map(ErrorResponse.ValidationError::of) +// .collect(Collectors.toList()); +// +// return ErrorResponse.builder() +// .code(errorCode.name()) +// .message(errorCode.getMessage()) +// .errors(validationErrorList) +// .build(); +// } + +} \ No newline at end of file diff --git a/src/main/java/com/mangkyu/employment/interview/app/common/erros/response/ErrorResponse.java b/src/main/java/com/mangkyu/employment/interview/app/common/erros/response/ErrorResponse.java new file mode 100644 index 0000000..00644c6 --- /dev/null +++ b/src/main/java/com/mangkyu/employment/interview/app/common/erros/response/ErrorResponse.java @@ -0,0 +1,46 @@ +package com.mangkyu.employment.interview.app.common.erros.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.FieldError; + +import java.util.List; + +@Getter +@Builder +@RequiredArgsConstructor +public class ErrorResponse { + + private final String code; + private final String message; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private final List errors; + + @Getter + @Builder + @RequiredArgsConstructor + public static class ValidationError { + private final String field; + private final String message; + + public static ValidationError of(final FieldError fieldError) { + return ValidationError.builder() + .field(fieldError.getField()) + .message(fieldError.getDefaultMessage()) + .build(); + } + +// public static ValidationError of(final ConstraintViolation violation) { +// final String propertyPath = violation.getPropertyPath().toString(); +// +// return ValidationError.builder() +// .field(StringUtils.substringAfterLast(propertyPath, ".")) +// .message(violation.getMessage()) +// .build(); +// } + } + +} \ No newline at end of file diff --git a/src/test/java/com/mangkyu/employment/interview/app/common/erros/handler/GlobalExceptionHandlerTest.java b/src/test/java/com/mangkyu/employment/interview/app/common/erros/handler/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000..80de198 --- /dev/null +++ b/src/test/java/com/mangkyu/employment/interview/app/common/erros/handler/GlobalExceptionHandlerTest.java @@ -0,0 +1,129 @@ +package com.mangkyu.employment.interview.app.common.erros.handler; + +import com.google.gson.Gson; +import com.mangkyu.employment.interview.app.common.erros.errorcode.CommonErrorCode; +import com.mangkyu.employment.interview.app.common.erros.errorcode.ErrorCode; +import com.mangkyu.employment.interview.app.common.erros.response.ErrorResponse; +import com.mangkyu.employment.interview.app.common.erros.exception.QuizException; +import com.mangkyu.employment.interview.app.quiz.controller.QuizController; +import com.mangkyu.employment.interview.app.quiz.dto.AddQuizRequest; +import com.mangkyu.employment.interview.app.quiz.service.QuizService; +import com.mangkyu.employment.interview.enums.value.QuizLevel; +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.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.validation.Validator; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doThrow; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class GlobalExceptionHandlerTest { + + @InjectMocks + private QuizController target; + + @Mock + private QuizService quizService; + + private MockMvc mockMvc; + @Spy + private Validator validator = new LocalValidatorFactoryBean(); + + @BeforeEach + public void init() { + mockMvc = MockMvcBuilders.standaloneSetup(target) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + } + + @Test + public void handleQuizException() throws Exception { + // given + final long quizId = -1; + final String url = "/quiz/" + quizId; + final ErrorCode errorCode = CommonErrorCode.RESOURCE_NOT_FOUND; + + doThrow(new QuizException(errorCode)).when(quizService).getQuiz(quizId); + + // when + final ResultActions result = mockMvc.perform( + MockMvcRequestBuilders.get(url) + ); + + // then + final ResultActions resultActions = result.andExpect(status().isNotFound()); + final String stringResponse = resultActions.andReturn().getResponse().getContentAsString(); + final ErrorResponse errorResponse = new Gson().fromJson(stringResponse, ErrorResponse.class); + + + assertThat(errorResponse.getCode()).isEqualTo(errorCode.name()); + assertThat(errorResponse.getMessage()).isEqualTo(errorCode.getMessage()); + } + + @Test + public void handleIllegalArgument() throws Exception { + // given + final long quizId = -1; + final String url = "/quiz/" + quizId; + final String message = "message"; + + doThrow(new IllegalArgumentException(message)).when(quizService).getQuiz(quizId); + + // when + final ResultActions result = mockMvc.perform( + MockMvcRequestBuilders.get(url) + ); + + // then + final ResultActions resultActions = result.andExpect(status().isBadRequest()); + final String stringResponse = resultActions.andReturn().getResponse().getContentAsString(); + final ErrorResponse errorResponse = new Gson().fromJson(stringResponse, ErrorResponse.class); + + + assertThat(errorResponse.getCode()).isEqualTo(CommonErrorCode.INVALID_PARAMETER.name()); + assertThat(errorResponse.getMessage()).isEqualTo(message); + } + + @Test + public void handleMethodArgumentNotValid() throws Exception { + // given + final String url = "/quiz"; + final ErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER; + + final AddQuizRequest addQuizRequest = AddQuizRequest.builder() + .title("Title") + .quizLevel(Collections.singletonList(QuizLevel.NEW)) + .build(); + + // when + final ResultActions result = mockMvc.perform( + MockMvcRequestBuilders.post(url) + .content(new Gson().toJson(addQuizRequest)) + .contentType(MediaType.APPLICATION_JSON) + ); + + // then + final ResultActions resultActions = result.andExpect(status().isBadRequest()); + final String stringResponse = resultActions.andReturn().getResponse().getContentAsString(); + final ErrorResponse errorResponse = new Gson().fromJson(stringResponse, ErrorResponse.class); + + assertThat(errorResponse.getCode()).isEqualTo(errorCode.name()); + assertThat(errorResponse.getMessage()).isEqualTo(errorCode.getMessage()); + assertThat(errorResponse.getErrors().size()).isEqualTo(1); + } + +} \ No newline at end of file