Skip to content

Commit 5039ec6

Browse files
claude[bot]github-actions[bot]claude
authored
Maintenance: ControllerAdvice - Add validation exception handlers and tests (#114)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude <[email protected]>
1 parent 16061ad commit 5039ec6

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

src/main/java/spring/memewikibe/api/controller/ControllerAdvice.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
import org.slf4j.LoggerFactory;
55
import org.springframework.http.HttpStatus;
66
import org.springframework.http.ResponseEntity;
7+
import org.springframework.http.converter.HttpMessageNotReadableException;
8+
import org.springframework.validation.FieldError;
9+
import org.springframework.web.bind.MethodArgumentNotValidException;
710
import org.springframework.web.bind.annotation.ExceptionHandler;
811
import org.springframework.web.bind.annotation.RestControllerAdvice;
912
import org.springframework.web.servlet.resource.NoResourceFoundException;
1013
import spring.memewikibe.support.error.ErrorType;
1114
import spring.memewikibe.support.error.MemeWikiApplicationException;
1215
import spring.memewikibe.support.response.ApiResponse;
1316

17+
import java.util.HashMap;
18+
import java.util.Map;
19+
1420
@RestControllerAdvice
1521
public class ControllerAdvice {
1622

@@ -26,6 +32,31 @@ public ResponseEntity<ApiResponse<?>> handleCustomException(MemeWikiApplicationE
2632
return new ResponseEntity<>(ApiResponse.error(e.getErrorType(), e.getData()), e.getErrorType().getStatus());
2733
}
2834

35+
@ExceptionHandler(MethodArgumentNotValidException.class)
36+
public ResponseEntity<ApiResponse<?>> handleValidationException(MethodArgumentNotValidException e) {
37+
Map<String, String> errors = new HashMap<>();
38+
e.getBindingResult().getAllErrors().forEach(error -> {
39+
String fieldName = ((FieldError) error).getField();
40+
String errorMessage = error.getDefaultMessage();
41+
errors.put(fieldName, errorMessage);
42+
});
43+
44+
log.warn("Validation failed: {}", errors);
45+
return new ResponseEntity<>(
46+
ApiResponse.error(ErrorType.EXTERNAL_SERVICE_BAD_REQUEST, errors),
47+
HttpStatus.BAD_REQUEST
48+
);
49+
}
50+
51+
@ExceptionHandler(HttpMessageNotReadableException.class)
52+
public ResponseEntity<ApiResponse<?>> handleHttpMessageNotReadable(HttpMessageNotReadableException e) {
53+
log.warn("Malformed JSON request: {}", e.getMessage());
54+
return new ResponseEntity<>(
55+
ApiResponse.error(ErrorType.EXTERNAL_SERVICE_BAD_REQUEST, "올바르지 않은 요청 형식입니다."),
56+
HttpStatus.BAD_REQUEST
57+
);
58+
}
59+
2960
@ExceptionHandler(NoResourceFoundException.class)
3061
public ResponseEntity<Void> handleNoResourceFound(NoResourceFoundException e) {
3162
if (e.getMessage().contains("favicon.ico")) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package spring.memewikibe.api.controller;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import jakarta.validation.Valid;
5+
import jakarta.validation.constraints.NotBlank;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.http.MediaType;
10+
import org.springframework.test.web.servlet.MockMvc;
11+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
12+
import org.springframework.web.bind.annotation.PostMapping;
13+
import org.springframework.web.bind.annotation.RequestBody;
14+
import org.springframework.web.bind.annotation.RestController;
15+
import spring.memewikibe.support.error.ErrorType;
16+
import spring.memewikibe.support.error.MemeWikiApplicationException;
17+
import spring.memewikibe.support.response.ApiResponse;
18+
19+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
20+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
21+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
22+
23+
class ControllerAdviceTest {
24+
25+
private MockMvc mockMvc;
26+
private ObjectMapper objectMapper;
27+
28+
@BeforeEach
29+
void setUp() {
30+
objectMapper = new ObjectMapper();
31+
mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
32+
.setControllerAdvice(new ControllerAdvice())
33+
.build();
34+
}
35+
36+
@Test
37+
@DisplayName("Validation 실패 시 필드별 에러 메시지를 반환한다")
38+
void handleValidationException() throws Exception {
39+
// given
40+
TestRequest invalidRequest = new TestRequest("");
41+
42+
// when & then
43+
mockMvc.perform(post("/test/validate")
44+
.contentType(MediaType.APPLICATION_JSON)
45+
.content(objectMapper.writeValueAsString(invalidRequest)))
46+
.andExpect(status().isBadRequest())
47+
.andExpect(jsonPath("$.resultType").value("ERROR"))
48+
.andExpect(jsonPath("$.error.code").value("E400"))
49+
.andExpect(jsonPath("$.error.data.name").exists());
50+
}
51+
52+
@Test
53+
@DisplayName("잘못된 JSON 형식 요청 시 적절한 에러 메시지를 반환한다")
54+
void handleHttpMessageNotReadable() throws Exception {
55+
// given
56+
String malformedJson = "{invalid json}";
57+
58+
// when & then
59+
mockMvc.perform(post("/test/validate")
60+
.contentType(MediaType.APPLICATION_JSON)
61+
.content(malformedJson))
62+
.andExpect(status().isBadRequest())
63+
.andExpect(jsonPath("$.resultType").value("ERROR"))
64+
.andExpect(jsonPath("$.error.code").value("E400"));
65+
}
66+
67+
@Test
68+
@DisplayName("MemeWikiApplicationException 발생 시 적절한 에러 응답을 반환한다")
69+
void handleCustomException() throws Exception {
70+
// when & then
71+
mockMvc.perform(post("/test/custom-error")
72+
.contentType(MediaType.APPLICATION_JSON))
73+
.andExpect(status().isNotFound())
74+
.andExpect(jsonPath("$.resultType").value("ERROR"))
75+
.andExpect(jsonPath("$.error.code").value("E404"))
76+
.andExpect(jsonPath("$.error.message").value("존재하지 않는 밈입니다."));
77+
}
78+
79+
@Test
80+
@DisplayName("예상치 못한 예외 발생 시 기본 에러 응답을 반환한다")
81+
void handleUnexpectedException() throws Exception {
82+
// when & then
83+
mockMvc.perform(post("/test/unexpected-error")
84+
.contentType(MediaType.APPLICATION_JSON))
85+
.andExpect(status().isInternalServerError())
86+
.andExpect(jsonPath("$.resultType").value("ERROR"))
87+
.andExpect(jsonPath("$.error.code").value("E500"));
88+
}
89+
90+
// Test controller for exception handling tests
91+
@RestController
92+
static class TestController {
93+
94+
@PostMapping("/test/validate")
95+
public ApiResponse<String> validate(@Valid @RequestBody TestRequest request) {
96+
return ApiResponse.success("OK");
97+
}
98+
99+
@PostMapping("/test/custom-error")
100+
public ApiResponse<String> customError() {
101+
throw new MemeWikiApplicationException(ErrorType.MEME_NOT_FOUND);
102+
}
103+
104+
@PostMapping("/test/unexpected-error")
105+
public ApiResponse<String> unexpectedError() {
106+
throw new RuntimeException("Unexpected error occurred");
107+
}
108+
}
109+
110+
record TestRequest(@NotBlank(message = "이름은 필수입니다.") String name) {}
111+
}

0 commit comments

Comments
 (0)