Skip to content

Commit

Permalink
fix ErrorHandling and LocalDateTime json serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
antonioT90 committed Feb 5, 2025
1 parent fc63356 commit 5dc4b2d
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 42 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ dependencies {

compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
testAnnotationProcessor("org.projectlombok:lombok")

// Testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package it.gov.pagopa.payhub.auth.exception;

import com.fasterxml.jackson.databind.JsonMappingException;
import it.gov.pagopa.payhub.auth.exception.custom.*;
import it.gov.pagopa.payhub.dto.generated.AuthErrorDTO;
import jakarta.servlet.ServletException;
Expand All @@ -12,11 +13,14 @@
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.ErrorResponse;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.stream.Collectors;


@RestControllerAdvice
@Slf4j
Expand Down Expand Up @@ -86,21 +90,45 @@ public ResponseEntity<AuthErrorDTO> handleRuntimeException(RuntimeException ex,
}

static ResponseEntity<AuthErrorDTO> handleAuthErrorException(Exception ex, HttpServletRequest request, HttpStatusCode httpStatus, AuthErrorDTO.ErrorEnum errorEnum) {
String message = logException(ex, request, httpStatus);
logException(ex, request, httpStatus);

String message = buildReturnedMessage(ex);

return ResponseEntity
.status(httpStatus)
.body(new AuthErrorDTO(errorEnum, message));
}

private static String logException(Exception ex, HttpServletRequest request, HttpStatusCode httpStatus) {
String message = ex.getMessage();
private static void logException(Exception ex, HttpServletRequest request, HttpStatusCode httpStatus) {
log.info("A {} occurred handling request {}: HttpStatus {} - {}",
ex.getClass(),
getRequestDetails(request),
httpStatus.value(),
message);
return message;
ex.getMessage());
}

private static String buildReturnedMessage(Exception ex) {
if (ex instanceof HttpMessageNotReadableException) {
if(ex.getCause() instanceof JsonMappingException jsonMappingException){
return "Cannot parse body: " +
jsonMappingException.getPath().stream()
.map(JsonMappingException.Reference::getFieldName)
.collect(Collectors.joining(".")) +
": " + jsonMappingException.getOriginalMessage();
}
return "Required request body is missing";
} else if (ex instanceof MethodArgumentNotValidException methodArgumentNotValidException) {
return "Invalid request content:" +
methodArgumentNotValidException.getBindingResult()
.getAllErrors().stream()
.map(e -> " " +
(e instanceof FieldError fieldError? fieldError.getField(): e.getObjectName()) +
": " + e.getDefaultMessage())
.sorted()
.collect(Collectors.joining(";"));
} else {
return ex.getMessage();
}
}

static String getRequestDetails(HttpServletRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,58 @@
package it.gov.pagopa.payhub.auth.exception;

import com.fasterxml.jackson.databind.ObjectMapper;
import it.gov.pagopa.payhub.auth.config.json.JsonConfig;
import it.gov.pagopa.payhub.auth.exception.custom.InvalidTokenException;
import it.gov.pagopa.payhub.auth.exception.custom.TokenExpiredException;
import jakarta.servlet.ServletException;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerErrorException;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

import java.time.LocalDateTime;
import java.util.Set;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;

@ExtendWith({SpringExtension.class, MockitoExtension.class})
@ExtendWith({SpringExtension.class})
@WebMvcTest(value = {AuthExceptionHandlerTest.TestController.class}, excludeAutoConfiguration = SecurityAutoConfiguration.class)
@ContextConfiguration(classes = {
AuthExceptionHandlerTest.TestController.class,
AuthExceptionHandler.class})
AuthExceptionHandler.class,
JsonConfig.class})
class AuthExceptionHandlerTest {

public static final String DATA = "data";
public static final TestRequestBody BODY = new TestRequestBody("bodyData", null, "abc", LocalDateTime.now());

@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;

@MockitoSpyBean
private TestController testControllerSpy;
Expand All @@ -48,21 +62,33 @@ class AuthExceptionHandlerTest {
@RestController
@Slf4j
static class TestController {

@GetMapping(value = "/test", produces = MediaType.APPLICATION_JSON_VALUE)
String testEndpoint(@RequestParam(DATA) String data) {
@PostMapping(value = "/test", produces = MediaType.APPLICATION_JSON_VALUE)
String testEndpoint(@RequestParam(DATA) String data, @Valid @RequestBody TestRequestBody body) {
return "OK";
}
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class TestRequestBody{
@NotNull
private String requiredField;
private String notRequiredField;
@Pattern(regexp = "[a-z]+")
private String lowerCaseAlphabeticField;
private LocalDateTime dateTimeField;
}

@Test
void handleInvalidTokenException() throws Exception {
doThrow(new InvalidTokenException("Error")).when(testControllerSpy).testEndpoint(DATA);
doThrow(new InvalidTokenException("Error")).when(testControllerSpy).testEndpoint(DATA, BODY);

mockMvc.perform(MockMvcRequestBuilders.get("/test")
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(BODY)))
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("invalid_grant"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("Error"));
Expand All @@ -71,12 +97,13 @@ void handleInvalidTokenException() throws Exception {

@Test
void handleTokenExpiredException() throws Exception {
doThrow(new TokenExpiredException("Error")).when(testControllerSpy).testEndpoint(DATA);
doThrow(new TokenExpiredException("Error")).when(testControllerSpy).testEndpoint(DATA, BODY);

mockMvc.perform(MockMvcRequestBuilders.get("/test")
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(BODY)))
.andExpect(MockMvcResultMatchers.status().isUnauthorized())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("invalid_grant"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("Error"));
Expand All @@ -86,9 +113,10 @@ void handleTokenExpiredException() throws Exception {
@Test
void handleMissingServletRequestParameterException() throws Exception {

mockMvc.perform(MockMvcRequestBuilders.get("/test")
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(BODY)))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("invalid_request"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("Required request parameter 'data' for method parameter type String is not present"));
Expand All @@ -97,12 +125,13 @@ void handleMissingServletRequestParameterException() throws Exception {

@Test
void handleRuntimeExceptionError() throws Exception {
doThrow(new RuntimeException("Error")).when(testControllerSpy).testEndpoint(DATA);
doThrow(new RuntimeException("Error")).when(testControllerSpy).testEndpoint(DATA, BODY);

mockMvc.perform(MockMvcRequestBuilders.get("/test")
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(BODY)))
.andExpect(MockMvcResultMatchers.status().isInternalServerError())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("AUTH_GENERIC_ERROR"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("Error"));
Expand All @@ -113,47 +142,85 @@ void handleGenericServletException() throws Exception {
doThrow(new ServletException("Error"))
.when(requestMappingHandlerAdapterSpy).handle(any(), any(), any());

mockMvc.perform(MockMvcRequestBuilders.get("/test")
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(BODY)))
.andExpect(MockMvcResultMatchers.status().isInternalServerError())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("AUTH_GENERIC_ERROR"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("Error"));
}

@Test
void handle4xxHttpServletException() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/test")
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.accept("application/hal+json"))
.accept("application/hal+json")
.content(objectMapper.writeValueAsString(BODY)))
.andExpect(MockMvcResultMatchers.status().isNotAcceptable())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("invalid_request"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("No acceptable representation"));
}

@Test
void handleNoBodyException() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("invalid_request"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("Required request body is missing"));
}

@Test
void handleInvalidBodyException() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content("{\"notRequiredField\":\"notRequired\",\"lowerCaseAlphabeticField\":\"ABC\"}"))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("invalid_request"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("Invalid request content: lowerCaseAlphabeticField: must match \"[a-z]+\"; requiredField: must not be null"));
}

@Test
void handleNotParsableBodyException() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content("{\"notRequiredField\":\"notRequired\",\"dateTimeField\":\"2025-02-05\"}"))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("invalid_request"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("Cannot parse body: dateTimeField: Text '2025-02-05' could not be parsed at index 10"));
}

@Test
void handle5xxHttpServletException() throws Exception {
doThrow(new ServerErrorException("Error", new RuntimeException("Error")))
.when(requestMappingHandlerAdapterSpy).handle(any(), any(), any());

mockMvc.perform(MockMvcRequestBuilders.get("/test")
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(BODY)))
.andExpect(MockMvcResultMatchers.status().isInternalServerError())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("AUTH_GENERIC_ERROR"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("500 INTERNAL_SERVER_ERROR \"Error\""));
}

@Test
void handleViolationException() throws Exception {
doThrow(new ConstraintViolationException("Error", Set.of())).when(testControllerSpy).testEndpoint(DATA);
doThrow(new ConstraintViolationException("Error", Set.of())).when(testControllerSpy).testEndpoint(DATA, BODY);

mockMvc.perform(MockMvcRequestBuilders.get("/test")
mockMvc.perform(MockMvcRequestBuilders.post("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.accept(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(BODY)))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.error").value("invalid_request"))
.andExpect(MockMvcResultMatchers.jsonPath("$.error_description").value("Error"));
Expand Down
Loading

0 comments on commit 5dc4b2d

Please sign in to comment.