Skip to content

Commit

Permalink
SIVA-683 Add exception handlers to handle generic exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
ivoMattus committed Jul 3, 2024
1 parent cb38bb9 commit 9676cbe
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,21 @@
import ee.openeid.siva.validation.service.signature.policy.InvalidPolicyException;
import ee.openeid.siva.webapp.request.limitation.RequestSizeLimitExceededException;
import ee.openeid.siva.webapp.response.erroneus.RequestValidationError;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

@Slf4j
@RestControllerAdvice
public class ValidationExceptionHandler {

Expand Down Expand Up @@ -94,6 +100,47 @@ public RequestValidationError handleRequestSizeLimitExceededException(RequestSiz
return requestValidationError;
}

@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public RequestValidationError handleNoHandlerFoundException(NoHandlerFoundException e) {
RequestValidationError requestValidationError = new RequestValidationError();
requestValidationError.addFieldError("endpointNotFound", "Endpoint not found");
return requestValidationError;
}

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseStatus(value = HttpStatus.METHOD_NOT_ALLOWED)
public RequestValidationError handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
RequestValidationError requestValidationError = new RequestValidationError();
requestValidationError.addFieldError("methodNotAllowed", "Request method " + e.getMethod() + " is not supported");
return requestValidationError;
}

@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public RequestValidationError handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
RequestValidationError requestValidationError = new RequestValidationError();
requestValidationError.addFieldError("requestBodyNotReadable", "Request body is malformed and cannot be read");
return requestValidationError;
}

@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
@ResponseStatus(value = HttpStatus.UNSUPPORTED_MEDIA_TYPE)
public RequestValidationError handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
RequestValidationError requestValidationError = new RequestValidationError();
requestValidationError.addFieldError("contentTypeNotSupported", "Content-Type " + e.getContentType() + " is not supported");
return requestValidationError;
}

@ExceptionHandler(Exception.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public RequestValidationError handleAllOtherExceptions(Exception e) {
RequestValidationError requestValidationError = new RequestValidationError();
requestValidationError.addFieldError("unexpectedError", "An unexpected error has occurred");
log.error("Unexpected error: {}", e.getMessage());
return requestValidationError;
}


private String getMessage(String key) {
return messageSource.getMessage(key, null, null);
Expand Down
9 changes: 8 additions & 1 deletion siva-parent/siva-webapp/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,11 @@ management:
version:
enabled: true


spring:
mvc:
# Allow custom handler for NoHandlerFoundException
throw-exception-if-no-handler-found: true
web:
resources:
# Allow custom handler for NoHandlerFoundException
add-mappings: false
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,31 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.Arrays;
import java.util.Collections;
import java.util.stream.Stream;

import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -218,6 +227,144 @@ void testHashcodeValidationMalformedSignatureFileExceptionHandler() throws Excep
.andReturn();
}

@ParameterizedTest
@ValueSource(strings = {"/asd", "/!@#", "/", "/012", "/你好"})
void performPostRequest_WhenEndpointIsInvalid_ThrowsNoHandlerFoundException(String endpoint) throws Exception {
mockMvc.perform(post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(request().toString().getBytes()))
.andExpect(MockMvcResultMatchers.status().isNotFound())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("endpointNotFound")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString("Endpoint not found")))
.andReturn();
}

@ParameterizedTest
@MethodSource("provideHttpMethodsAndValidationEndpoints")
void performRequest_WhenEndpointIsSetToValidateOrHashcodeAndHttpMethodIsInvalid_ThrowsHttpRequestMethodNotSupportedException(HttpMethod httpMethod, String endpoint, String requestErrorMessage) throws Exception {
mockMvc.perform(MockMvcRequestBuilders.request(httpMethod, endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(request().toString().getBytes()))
.andExpect(MockMvcResultMatchers.status().isMethodNotAllowed())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("methodNotAllowed")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString(requestErrorMessage)))
.andReturn();
}

@ParameterizedTest
@MethodSource("provideHttpMethods")
void performRequest_WhenEndpointIsSetToGetDataFilesAndHttpMethodIsInvalid_ThrowsHttpRequestMethodNotSupportedException(HttpMethod httpMethod, String requestErrorMessage) throws Exception {
mockMvcDataFiles.perform(MockMvcRequestBuilders.request(httpMethod, GET_DATA_FILES_URL_TEMPLATE)
.contentType(MediaType.APPLICATION_JSON)
.content(dataFileRequest().toString().getBytes()))
.andExpect(MockMvcResultMatchers.status().isMethodNotAllowed())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("methodNotAllowed")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString(requestErrorMessage)))
.andReturn();
}

@ParameterizedTest
@MethodSource("provideInvalidJsonContentAndValidationEndpoints")
void performPostRequest_WhenEndpointIsSetToValidateOrHashcodeAndJsonContentIsInvalid_ThrowsHttpMessageNotReadableException(String endpoint, String invalidJsonContent) throws Exception {
mockMvc.perform(post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidJsonContent.getBytes()))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("requestBodyNotReadable")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString("Request body is malformed and cannot be read")))
.andReturn();
}

@ParameterizedTest
@MethodSource("provideInvalidJsonContent")
void performPostRequest_WhenEndpointIsSetToGetDataFilesAndJsonContentIsInvalid_ThrowsHttpMessageNotReadableException(String invalidJsonContent) throws Exception {
mockMvcDataFiles.perform(post(GET_DATA_FILES_URL_TEMPLATE)
.contentType(MediaType.APPLICATION_JSON)
.content(invalidJsonContent.getBytes()))
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("requestBodyNotReadable")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString("Request body is malformed and cannot be read")))
.andReturn();
}

@ParameterizedTest
@MethodSource("provideUnsupportedMediaTypesAndValidationEndpoints")
void performPostRequest_WhenEndpointIsSetToValidateOrHashcodeAndContentTypeIsInvalid_ThrowsHttpMediaTypeNotSupportedException(String endpoint, String mediaType, String requestErrorMessage) throws Exception {
mockMvc.perform(post(endpoint)
.contentType(mediaType)
.content(request().toString().getBytes()))
.andExpect(MockMvcResultMatchers.status().isUnsupportedMediaType())
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("contentTypeNotSupported")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString(requestErrorMessage)))
.andReturn();
}

@ParameterizedTest
@MethodSource("provideUnsupportedMediaTypes")
void performPostRequest_WhenEndpointIsSetToGetDataFilesAndContentTypeIsInvalid_ThrowsHttpMediaTypeNotSupportedException(String mediaType, String requestErrorMessage) throws Exception {
mockMvcDataFiles.perform(post(GET_DATA_FILES_URL_TEMPLATE)
.contentType(mediaType)
.content(dataFileRequest().toString().getBytes()))
.andExpect(MockMvcResultMatchers.status().isUnsupportedMediaType())
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("contentTypeNotSupported")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString(requestErrorMessage)))
.andReturn();
}

@Test
void performPostRequest_WhenValidationProxyThrowsNewRuntimeException_ThrowsException() throws Exception {
when(validationProxy.validate(any())).thenThrow(new RuntimeException("Unexpected error"));
mockMvc.perform(post(VALIDATE_URL_TEMPLATE)
.contentType(MediaType.APPLICATION_JSON)
.content(request().toString().getBytes()))
.andExpect(MockMvcResultMatchers.status().isInternalServerError())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("unexpectedError")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString("An unexpected error has occurred")))
.andReturn();
}

@Test
void performPostRequest_WhenHashcodeValidationProxyThrowsNewRuntimeException_ThrowsException() throws Exception {
when(hashcodeValidationProxy.validate(any())).thenThrow(new RuntimeException("Unexpected error"));
mockMvc.perform(post(HASHCODE_VALIDATION_URL_TEMPLATE)
.contentType(MediaType.APPLICATION_JSON)
.content(requestWithInvalidFormatSignatureFile().toString().getBytes()))
.andExpect(MockMvcResultMatchers.status().isInternalServerError())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("unexpectedError")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString("An unexpected error has occurred")))
.andReturn();
}

@Test
void performPostRequest_WhenDataFilesProxyThrowsNewRuntimeException_ThrowsException() throws Exception {
when(dataFilesProxy.getDataFiles(any(ProxyDocument.class))).thenThrow(new RuntimeException("Unexpected error"));
mockMvcDataFiles.perform(post(GET_DATA_FILES_URL_TEMPLATE)
.contentType(MediaType.APPLICATION_JSON)
.content(dataFileRequest().toString().getBytes()))
.andExpect(MockMvcResultMatchers.status().isInternalServerError())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors", hasSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].key", is("unexpectedError")))
.andExpect(MockMvcResultMatchers.jsonPath("$.requestErrors[0].message", containsString("An unexpected error has occurred")))
.andReturn();
}

private JSONObject request() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("document", "dGVzdA0K");
Expand Down Expand Up @@ -265,4 +412,69 @@ private JSONObject requestWithInvalidFormatSignatureFile() {
jsonObject.put("datafiles", Arrays.asList(datafile));
return jsonObject;
}

private static Stream<Arguments> provideHttpMethodsAndValidationEndpoints() {
return provideValidationEndpoints()
.flatMap(endpoint -> provideHttpMethods()
.map(args -> Arguments.of(args.get()[0], endpoint, "Request method " + ((HttpMethod) args.get()[0]).name() + " is not supported")));
}

private static Stream<Arguments> provideHttpMethods() {
return Stream.of(
Arguments.of(HttpMethod.GET, "Request method GET is not supported"),
Arguments.of(HttpMethod.PUT, "Request method PUT is not supported"),
Arguments.of(HttpMethod.DELETE, "Request method DELETE is not supported"),
Arguments.of(HttpMethod.PATCH, "Request method PATCH is not supported"),
Arguments.of(HttpMethod.HEAD, "Request method HEAD is not supported")
);
}

private static Stream<Arguments> provideInvalidJsonContentAndValidationEndpoints() {
return provideValidationEndpoints()
.flatMap(endpoint -> provideInvalidJsonContent()
.map(invalidJsonContent -> Arguments.of(endpoint, invalidJsonContent)));
}

private static Stream<String> provideInvalidJsonContent() {
return Stream.of(
"",
"{filename: \"\"}",
"{\"filename\": ",
"{\"filename\": \"file\"",
"{\"filename\": \"file\", ",
"{\"filename\": \"file\" \"reportType\": simple}",
"{\"filename\": \"file\", \"reportType\": simple",
"{\"filename\": \"file\", \"reportType\": simple, ",
"{filename: \"file\", \"reportType\": simple}",
"{\"filename\": \"file\", \"reportType\": }",
"{\"filename\": \"file\", \"reportType\": simple, \"document\": }"
);
}

private static Stream<Arguments> provideUnsupportedMediaTypesAndValidationEndpoints() {
return provideValidationEndpoints()
.map(Arguments::of)
.flatMap(endpointArgs -> provideUnsupportedMediaTypes()
.map(mediaTypeArgs -> appendArguments(endpointArgs, mediaTypeArgs)));
}

private static Stream<Arguments> provideUnsupportedMediaTypes() {
return Stream.of(
Arguments.of(MediaType.APPLICATION_XML_VALUE, "Content-Type application/xml is not supported"),
Arguments.of(MediaType.TEXT_PLAIN_VALUE, "Content-Type text/plain is not supported"),
Arguments.of("application/x-yaml", "Content-Type application/x-yaml is not supported"),
Arguments.of(MediaType.TEXT_HTML_VALUE, "Content-Type text/html is not supported"),
Arguments.of("", "Content-Type application/octet-stream is not supported")
);
}

private static Stream<String> provideValidationEndpoints() {
return Stream.of(VALIDATE_URL_TEMPLATE, HASHCODE_VALIDATION_URL_TEMPLATE);
}

private static Arguments appendArguments(Arguments... arguments) {
return Arguments.of(Stream.of(arguments)
.flatMap(args -> Stream.of(args.get()))
.toArray());
}
}

0 comments on commit 9676cbe

Please sign in to comment.