Skip to content

Commit

Permalink
Merge branch 'refactor/#108' of https://github.com/woowacourse-teams/…
Browse files Browse the repository at this point in the history
…2024-devel-up into refactor/#108
  • Loading branch information
lilychoibb committed Jul 24, 2024
2 parents 85b6308 + 40cdb13 commit 4b3b6f1
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package develup.support;
package develup.support.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package develup.support;
package develup.support.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package develup.support.exception;

import org.springframework.http.HttpStatus;

public class DevelupException extends RuntimeException {

private final ExceptionType exceptionType;

public DevelupException(ExceptionType exceptionType) {
super(exceptionType.getMessage());
this.exceptionType = exceptionType;
}

public DevelupException(ExceptionType exceptionType, Throwable cause) {
super(exceptionType.getMessage(), cause);
this.exceptionType = exceptionType;
}

public HttpStatus getStatus() {
return exceptionType.getStatus();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package develup.support.exception;

import org.springframework.validation.FieldError;

public record ExceptionDetail(String field, String message) {

public ExceptionDetail(FieldError error) {
this(error.getField(), error.getDefaultMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package develup.support.exception;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.validation.FieldError;

public record ExceptionResponse(String message, List<ExceptionDetail> details) {

private static final String INVALID_VALUE_MESSAGE = "올바른 값을 입력해주세요.";

public ExceptionResponse(String message) {
this(message, Collections.emptyList());
}

public ExceptionResponse(FieldError[] errors) {
this(INVALID_VALUE_MESSAGE, Arrays.stream(errors).map(ExceptionDetail::new).toList());
}
}
24 changes: 24 additions & 0 deletions backend/src/main/java/develup/support/exception/ExceptionType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package develup.support.exception;

import org.springframework.http.HttpStatus;

public enum ExceptionType {

;

private final HttpStatus status;
private final String message;

ExceptionType(HttpStatus status, String message) {
this.status = status;
this.message = message;
}

public HttpStatus getStatus() {
return status;
}

public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package develup.support.exception;

import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ExceptionResponse> handle(MethodArgumentNotValidException e) {
log.warn("[MethodArgumentNotValidException] {}", e.getMessage(), e);

BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldError = bindingResult.getFieldErrors();

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse(fieldError.toArray(FieldError[]::new)));
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ExceptionResponse> handle(MethodArgumentTypeMismatchException e) {
log.warn("[MethodArgumentTypeMismatchException] {}", e.getMessage(), e);

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse("요청 값의 타입이 잘못되었습니다."));
}

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ExceptionResponse> handle(HttpRequestMethodNotSupportedException e) {
log.warn("[HttpRequestMethodNotSupportedException] {}", e.getMessage(), e);

return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(new ExceptionResponse("지원하지 않는 HTTP 메서드입니다."));
}

@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<ExceptionResponse> handle(NoResourceFoundException e) {
log.warn("[NoResourceFoundException] {}", e.getMessage(), e);

return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ExceptionResponse("요청하신 리소스를 찾을 수 없습니다."));
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ExceptionResponse> handle(HttpMessageNotReadableException e) {
log.warn("[HttpMessageNotReadableException] {}", e.getMessage(), e);

return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ExceptionResponse("요청을 읽을 수 없습니다."));
}

@ExceptionHandler(DevelupException.class)
public ResponseEntity<ExceptionResponse> handle(DevelupException e) {
log.warn("[DevelupException] {}", e.getMessage(), e);

return ResponseEntity.status(e.getStatus())
.body(new ExceptionResponse(e.getMessage()));
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ExceptionResponse> handle(Exception e) {
log.error("[Exception] {}", e.getMessage(), e);

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ExceptionResponse("서버 오류가 발생했습니다."));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package develup.support.exception;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.fasterxml.jackson.databind.ObjectMapper;
import develup.auth.AuthService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@WebMvcTest(GlobalExceptionHandlerTest.TestHandler.class)
class GlobalExceptionHandlerTest {

@MockBean
TestHandler target;

@MockBean
AuthService authService;

@Autowired
MockMvc mockMvc;

@Autowired
ObjectMapper objectMapper;

@Test
@DisplayName("DTO를 검증 예외를 처리한다.")
void methodArgumentNotValidException() throws Exception {
mockMvc.perform(
post("/")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new TestHandler.TestRequest("", "")))
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("올바른 값을 입력해주세요."))
.andExpect(jsonPath("$.details[?(@.field == 'name')]").exists())
.andExpect(jsonPath("$.details[?(@.message == '이름은 필수 값입니다.')]").exists())
.andExpect(jsonPath("$.details[?(@.field == 'email')]").exists())
.andExpect(jsonPath("$.details[?(@.message == '이메일은 필수 값입니다.')]").exists());
}

@Test
@DisplayName("존재하지 않는 리소스 요청을 처리한다.")
void unknownResource() throws Exception {
mockMvc.perform(get("/unknown/"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value("요청하신 리소스를 찾을 수 없습니다."));
}

@Test
@DisplayName("존재하지 않는 HTTP 메서드 요청을 처리한다.")
void unknownMethod() throws Exception {
mockMvc.perform(delete("/"))
.andExpect(status().isMethodNotAllowed())
.andExpect(jsonPath("$.message").value("지원하지 않는 HTTP 메서드입니다."));
}

@Test
@DisplayName("읽을 수 없는 HTTP 메시지를 처리한다.")
void notReadable() throws Exception {
mockMvc.perform(
post("/")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString("}{"))
)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("요청을 읽을 수 없습니다."));
}

@Test
@DisplayName("타입이 일치하지 않는 경우를 처리한다.")
void typeMismatch() throws Exception {
mockMvc.perform(get("/abc"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("요청 값의 타입이 잘못되었습니다."));
}

@Test
@DisplayName("예기치 못한 예외를 처리한다.")
void unExpectedException() throws Exception {
when(target.get()).thenThrow(new UnexpectedException());

mockMvc.perform(get("/"))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message").value("서버 오류가 발생했습니다."));
}

private static class UnexpectedException extends RuntimeException {
}

@Controller
static class TestHandler {

@GetMapping
ResponseEntity<String> get() {
return ResponseEntity.ok("sample");
}

@GetMapping("/{id}")
ResponseEntity<String> get(@PathVariable Long id) {
return ResponseEntity.ok("sample");
}

@PostMapping
ResponseEntity<Void> post(@Valid @RequestBody TestRequest request) {
return ResponseEntity.noContent().build();
}

record TestRequest(
@NotBlank(message = "이름은 필수 값입니다.")
String name,

@NotBlank(message = "이메일은 필수 값입니다.")
String email
) {
}
}
}

0 comments on commit 4b3b6f1

Please sign in to comment.