diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/ExceptionController.java b/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/ExceptionController.java index feab7026e5b982591c57e6a1fcd67971e1c534d0..7612064c206d475f795ab4a02c65ee12117056df 100644 --- a/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/ExceptionController.java +++ b/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/ExceptionController.java @@ -28,12 +28,16 @@ public class ExceptionController { private static final Set<String> IGNORABLE_CONSTRAINT_VIOLATION_ATTRIBUTES = new HashSet<>(Arrays.asList("groups", "payload", "message")); + static final String RUNTIME_EXCEPTION_MESSAGE_CODE = "generale.server_error"; + static final String RESOURCE_NOT_FOUNT_EXCEPTION_MESSAGE_CODE = "resource.not_found"; + @ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) @ResponseBody public ApiError handleResourceNotFoundException(ResourceNotFoundException e) { LOG.debug("EntityModel not found: {}", e.getMessage()); - return ApiError.builder().issue(Issue.builder().message(e.getMessage()).messageCode("resource.not_found").build()).build(); + return ApiError.builder().issue(Issue.builder().message(e.getMessage()).messageCode(RESOURCE_NOT_FOUNT_EXCEPTION_MESSAGE_CODE).build()) + .build(); } @ExceptionHandler(RuntimeException.class) @@ -41,7 +45,7 @@ public class ExceptionController { @ResponseBody public ApiError handleRuntimeException(RuntimeException e) { LOG.error("RuntimeException on Request", e); - return ApiError.builder().issue(Issue.builder().messageCode("generale.server_error").message(e.getMessage()).build()).build(); + return ApiError.builder().issue(Issue.builder().messageCode(RUNTIME_EXCEPTION_MESSAGE_CODE).message(e.getMessage()).build()).build(); } @ExceptionHandler(FunctionalException.class) @@ -55,7 +59,7 @@ public class ExceptionController { @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) @ResponseBody - public ApiError handleConstaintViolationException(ConstraintViolationException e) { + public ApiError handleConstraintViolationException(ConstraintViolationException e) { LOG.info("Validation Exception: {}", e.getMessage()); LOG.debug("Validation Exception", e); diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionController.java b/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionController.java new file mode 100644 index 0000000000000000000000000000000000000000..a12f90b2b9d8a0a88ac65cf72a50f7e2e49c1181 --- /dev/null +++ b/goofy-server/src/main/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionController.java @@ -0,0 +1,63 @@ +package de.itvsh.goofy.common.errorhandling; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.Status; +import io.grpc.StatusException; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@ControllerAdvice +public class GrpcExceptionController { + + static final String GRPC_INTERNAL_ERROR_CODE_KEY = "grpc." + ExceptionController.RUNTIME_EXCEPTION_MESSAGE_CODE; + static final String GRPC_NOT_FOUND_ERROR_CODE_KEY = "grpc." + ExceptionController.RESOURCE_NOT_FOUNT_EXCEPTION_MESSAGE_CODE; + + @ExceptionHandler(StatusException.class) + public ResponseEntity<ApiError> handleStatusException(StatusException e) { + LOG.debug("Grpc StatusException: {}", e.getMessage()); + return new ResponseEntity<>(handleGrpcStatusException(e), mapToHttpStatus(e.getStatus())); + } + + private ApiError handleGrpcStatusException(StatusException e) { + if (e.getStatus().getCode() == Status.INTERNAL.getCode()) { + return buildInternalApiError(e); + } + return ApiError.builder().issue(Issue.builder().message(e.getMessage()).messageCode(GRPC_NOT_FOUND_ERROR_CODE_KEY).build()).build(); + } + + private ApiError buildInternalApiError(StatusException e) { + return Objects.isNull(e.getTrailers()) + ? ApiError.builder().issue(buildInternalErrorIssueBuilder(e).build()).build() + : ApiError.builder().issue(buildInternalErrorIssueBuilder(e).parameters(mapToIssueParams(e.getTrailers())).build()).build(); + } + + private Issue.IssueBuilder buildInternalErrorIssueBuilder(StatusException e) { + return Issue.builder().message(e.getMessage()).messageCode(GRPC_INTERNAL_ERROR_CODE_KEY); + } + + private List<IssueParam> mapToIssueParams(Metadata metaData) { + List<IssueParam> params = new ArrayList<>(); + metaData.keys().forEach(key -> params.add(buildIssueParam(metaData, key))); + return params; + } + + private IssueParam buildIssueParam(Metadata metaData, String key) { + return IssueParam.builder().name(key).value(metaData.get(Key.of(key, Metadata.ASCII_STRING_MARSHALLER))).build(); + } + + private HttpStatus mapToHttpStatus(Status status) { + return status.getCode() == Status.INTERNAL.getCode() + ? HttpStatus.INTERNAL_SERVER_ERROR + : HttpStatus.NOT_FOUND; + } +} \ No newline at end of file diff --git a/goofy-server/src/test/java/de/itvsh/goofy/common/command/CommandITCase.java b/goofy-server/src/test/java/de/itvsh/goofy/common/command/CommandITCase.java index 88e0201c8bf9130e30a539e6beb4356bb066f117..07c3103e03509aafafddbe29c2b0e7dda60bb9d9 100644 --- a/goofy-server/src/test/java/de/itvsh/goofy/common/command/CommandITCase.java +++ b/goofy-server/src/test/java/de/itvsh/goofy/common/command/CommandITCase.java @@ -25,7 +25,6 @@ import de.itvsh.goofy.vorgang.VorgangHeaderTestFactory; @AutoConfigureMockMvc @SpringBootTest - public class CommandITCase { @Autowired diff --git a/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/ExceptionControllerTest.java b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/ExceptionControllerTest.java index 9fac49cfc24c8a2b79649e8f4de8c80f58a994ba..1505f634bf4b3b97fa0b402228b26cb6c21ded0d 100644 --- a/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/ExceptionControllerTest.java +++ b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/ExceptionControllerTest.java @@ -9,6 +9,7 @@ import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.Path; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -19,40 +20,45 @@ class ExceptionControllerTest { @Test void handleResourceNotFoundException() { - ApiError response = exceptionController.handleResourceNotFoundException(new ResourceNotFoundException(String.class, 42L)); + ApiError error = exceptionController.handleResourceNotFoundException(new ResourceNotFoundException(String.class, 42L)); - assertThat(response.getIssues()).hasSize(1); - assertThat(response.getIssues().get(0).getMessageCode()).isEqualTo("resource.not_found"); - assertThat(response.getIssues().get(0).getMessage()).isEqualTo("EntityModel 'String' with id '42' not found."); + assertThat(error.getIssues()).hasSize(1); + assertThat(error.getIssues().get(0).getMessageCode()).isEqualTo("resource.not_found"); + assertThat(error.getIssues().get(0).getMessage()).isEqualTo("EntityModel 'String' with id '42' not found."); } - @Test - void handleFieldInValidationException() { - ApiError error = exceptionController - .handleConstaintViolationException(new ConstraintViolationException(Collections.singleton(mockViolation()))); + @Nested + class TestContraintValidationException { - assertThat(error.getIssues()).hasSize(1); - Issue issue = error.getIssues().get(0); - assertThat(issue.getField()).isEqualTo("command.wiedervorlage.betreff"); - } + @Test + void handleFieldInValidationException() { + ApiError error = exceptionController + .handleConstraintViolationException(new ConstraintViolationException(Collections.singleton(mockViolation()))); - @Test - void handleMessageCodeInValidationException() { - ApiError error = exceptionController - .handleConstaintViolationException(new ConstraintViolationException(Collections.singleton(mockViolation()))); + assertThat(error.getIssues()).hasSize(1); + Issue issue = error.getIssues().get(0); + assertThat(issue.getField()).isEqualTo("command.wiedervorlage.betreff"); + } - assertThat(error.getIssues()).hasSize(1); - Issue issue = error.getIssues().get(0); - assertThat(issue.getMessageCode()).isEqualTo("huhu.code"); - } + @Test + void handleMessageCodeInValidationException() { + ApiError error = exceptionController + .handleConstraintViolationException(new ConstraintViolationException(Collections.singleton(mockViolation()))); - private ConstraintViolation<?> mockViolation() { - ConstraintViolation<?> violation = mock(ConstraintViolation.class); - Path path = mock(Path.class); - when(violation.getPropertyPath()).thenReturn(path); - when(path.toString()).thenReturn("createCommandByRelation.command.wiedervorlage.betreff"); - when(violation.getMessageTemplate()).thenReturn("{huhu.code}"); + assertThat(error.getIssues()).hasSize(1); + Issue issue = error.getIssues().get(0); + assertThat(issue.getMessageCode()).isEqualTo("huhu.code"); + } - return violation; + private ConstraintViolation<?> mockViolation() { + ConstraintViolation<?> violation = mock(ConstraintViolation.class); + Path path = mock(Path.class); + when(violation.getPropertyPath()).thenReturn(path); + when(path.toString()).thenReturn("createCommandByRelation.command.wiedervorlage.betreff"); + when(violation.getMessageTemplate()).thenReturn("{huhu.code}"); + + return violation; + } } + } \ No newline at end of file diff --git a/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionControllerTest.java b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..65b01db379e0a2b6757efed97257d953ade6ded6 --- /dev/null +++ b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionControllerTest.java @@ -0,0 +1,83 @@ +package de.itvsh.goofy.common.errorhandling; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.springframework.http.HttpStatus; + +class GrpcExceptionControllerTest { + + @InjectMocks + private GrpcExceptionController grpcExceptionController; + + @Nested + class TestNotFoundStatusException { + + @Test + void shouldHaveMessageCode() { + var response = grpcExceptionController.handleStatusException(GrpcExceptionTestFactory.createGrpcNotFoundStatusException()); + + assertThat(response.getBody().getIssues()).hasSize(1); + assertThat(response.getBody().getIssues().get(0).getMessageCode()).isEqualTo(GrpcExceptionController.GRPC_NOT_FOUND_ERROR_CODE_KEY); + } + + @Test + void shouldHaveMessage() { + var response = grpcExceptionController.handleStatusException(GrpcExceptionTestFactory.createGrpcNotFoundStatusException()); + + assertThat(response.getBody().getIssues()).hasSize(1); + assertThat(response.getBody().getIssues().get(0).getMessage()).isEqualTo("NOT_FOUND: " + GrpcExceptionTestFactory.NOT_FOUND_DESCRIPTION); + } + + @Test + void shouldHaveHttpStatusNotFound() { + var response = grpcExceptionController.handleStatusException(GrpcExceptionTestFactory.createGrpcNotFoundStatusException()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + } + + @Nested + class TestTechnicalStatusException { + + @Test + void shouldHaveMessageCode() { + var response = grpcExceptionController.handleStatusException(GrpcExceptionTestFactory.createGrpcInternalStatusException()); + + assertThat(response.getBody().getIssues()).hasSize(1); + assertThat(response.getBody().getIssues().get(0).getMessageCode()).isEqualTo(GrpcExceptionController.GRPC_INTERNAL_ERROR_CODE_KEY); + } + + @Test + void shouldHaveMessage() { + var response = grpcExceptionController.handleStatusException(GrpcExceptionTestFactory.createGrpcInternalStatusException()); + + assertThat(response.getBody().getIssues()).hasSize(1); + assertThat(response.getBody().getIssues().get(0).getMessage()).isEqualTo("INTERNAL: " + GrpcExceptionTestFactory.TECHNICAL_DESCRIPTION); + } + + @Test + void shouldHaveHttpStatusInternalServerError() { + var response = grpcExceptionController.handleStatusException(GrpcExceptionTestFactory.createGrpcInternalStatusException()); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + } + + @Nested + class TestWithMetaData { + + @Test + void shouldHaveIssueParams() { + var response = grpcExceptionController + .handleStatusException(GrpcExceptionTestFactory.createGrpcInternalStatusException(GrpcExceptionTestFactory.createMetaData())); + + assertThat(response.getBody().getIssues()).hasSize(1); + assertThat(response.getBody().getIssues().get(0).getParameters().get(0).getName()).isEqualTo(GrpcExceptionTestFactory.METADATA_ENTRY_KEY); + assertThat(response.getBody().getIssues().get(0).getParameters().get(0).getValue()) + .isEqualTo(GrpcExceptionTestFactory.METADATA_ENTRY_VALUE); + } + } + } +} \ No newline at end of file diff --git a/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionTestFactory.java b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..ed134b4beab116d250e3886b87b58ede1b313864 --- /dev/null +++ b/goofy-server/src/test/java/de/itvsh/goofy/common/errorhandling/GrpcExceptionTestFactory.java @@ -0,0 +1,37 @@ +package de.itvsh.goofy.common.errorhandling; + +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.Status; +import io.grpc.StatusException; + +public class GrpcExceptionTestFactory { + + public static final String NOT_FOUND_DESCRIPTION = "Not Found Exception message"; + public static final String TECHNICAL_DESCRIPTION = "Technical Exception message"; + + public static final String METADATA_ENTRY_KEY = "param_key"; + public static final String METADATA_ENTRY_VALUE = "param_value"; + + public static StatusException createGrpcNotFoundStatusException() { + return new StatusException(Status.NOT_FOUND.withDescription(NOT_FOUND_DESCRIPTION).withCause(new RuntimeException())); + } + + public static StatusException createGrpcInternalStatusException(Metadata metaData) { + return new StatusException(buildInternalStatus(), metaData); + } + + public static StatusException createGrpcInternalStatusException() { + return new StatusException(buildInternalStatus()); + } + + private static Status buildInternalStatus() { + return Status.INTERNAL.withDescription(TECHNICAL_DESCRIPTION).withCause(new RuntimeException()); + } + + public static Metadata createMetaData() { + Metadata data = new Metadata(); + data.put(Key.of(METADATA_ENTRY_KEY, Metadata.ASCII_STRING_MARSHALLER), METADATA_ENTRY_VALUE); + return data; + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index a16aca917d1b2802827a34415f795b82982f0e67..682447054a2a58e051958fa148eae7f35fca3b64 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ <spring.boot.version>2.4.9</spring.boot.version> - <grpc.spring-boot-starter.version>2.10.1.RELEASE</grpc.spring-boot-starter.version> + <grpc.spring-boot-starter.version>2.12.0.RELEASE</grpc.spring-boot-starter.version> <spring-admin.version>2.3.1</spring-admin.version> <mapstruct.version>1.4.1.Final</mapstruct.version> <commons-io.version>2.11.0</commons-io.version>