From 9676cbe7ba764191e1a7f4c757d917953d37c1fd Mon Sep 17 00:00:00 2001 From: Ivo Mattus Date: Wed, 19 Jun 2024 16:02:59 +0300 Subject: [PATCH] SIVA-683 Add exception handlers to handle generic exceptions --- .../webapp/ValidationExceptionHandler.java | 47 ++++ .../src/main/resources/application.yml | 9 +- .../ValidationExceptionHandlerTest.java | 214 +++++++++++++++++- 3 files changed, 268 insertions(+), 2 deletions(-) diff --git a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/ValidationExceptionHandler.java b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/ValidationExceptionHandler.java index e2ac3c786..db3f9f14c 100644 --- a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/ValidationExceptionHandler.java +++ b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/ValidationExceptionHandler.java @@ -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 { @@ -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); diff --git a/siva-parent/siva-webapp/src/main/resources/application.yml b/siva-parent/siva-webapp/src/main/resources/application.yml index 42982e1c3..530a218fe 100644 --- a/siva-parent/siva-webapp/src/main/resources/application.yml +++ b/siva-parent/siva-webapp/src/main/resources/application.yml @@ -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 \ No newline at end of file diff --git a/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/ValidationExceptionHandlerTest.java b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/ValidationExceptionHandlerTest.java index b7677932e..d639bdde7 100644 --- a/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/ValidationExceptionHandlerTest.java +++ b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/ValidationExceptionHandlerTest.java @@ -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; @@ -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"); @@ -265,4 +412,69 @@ private JSONObject requestWithInvalidFormatSignatureFile() { jsonObject.put("datafiles", Arrays.asList(datafile)); return jsonObject; } + + private static Stream 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 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 provideInvalidJsonContentAndValidationEndpoints() { + return provideValidationEndpoints() + .flatMap(endpoint -> provideInvalidJsonContent() + .map(invalidJsonContent -> Arguments.of(endpoint, invalidJsonContent))); + } + + private static Stream 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 provideUnsupportedMediaTypesAndValidationEndpoints() { + return provideValidationEndpoints() + .map(Arguments::of) + .flatMap(endpointArgs -> provideUnsupportedMediaTypes() + .map(mediaTypeArgs -> appendArguments(endpointArgs, mediaTypeArgs))); + } + + private static Stream 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 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()); + } }