diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcService.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcService.java index b53be2a88c4aba70ef316d198fc15dd5c3c2adcb..a1b095a22501d26fff2970b0fca405abcc626fb7 100644 --- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcService.java +++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcService.java @@ -30,6 +30,9 @@ import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.time.ZonedDateTime; import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; import java.util.stream.Stream; import org.apache.commons.io.IOUtils; @@ -39,6 +42,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import com.google.protobuf.ByteString; +import de.ozgcloud.apilib.common.errorhandling.NotFoundException; import de.ozgcloud.apilib.file.OzgCloudFile; import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.nachrichten.NachrichtenManagerConfiguration; @@ -133,17 +137,22 @@ class AntragraumGrpcService extends AntragraumServiceGrpc.AntragraumServiceImplB @Override public void getAttachmentContent(GrpcGetAttachmentContentRequest request, StreamObserver<GrpcGetAttachmentContentResponse> responseObserver) { - try (var pipedInputStream = new PipedInputStream(); var pipedOutputStream = new PipedOutputStream(pipedInputStream)) { - getAttachmentFileContent(request, pipedOutputStream); + try (var pipedInputStream = new PipedInputStream(); + var pipedOutputStream = new PipedOutputStream(pipedInputStream); + var executor = Executors.newFixedThreadPool(1);) { + var task = new FutureTask<>(() -> getAttachmentFileContent(request, pipedOutputStream), null); + + executor.execute(task); sendFileContent(pipedInputStream, responseObserver); + handleExceptionOnRetrievingContent(task); + } catch (IOException e) { throw new TechnicalException("Error on sending attachment content!", e); } } void getAttachmentFileContent(GrpcGetAttachmentContentRequest request, PipedOutputStream pipedOutputStream) { - new Thread(() -> service.getAttachmentContent(attachmentFileRequestMapper.fromGrpcContentRequest(request), pipedOutputStream)) - .start(); + service.getAttachmentContent(attachmentFileRequestMapper.fromGrpcContentRequest(request), pipedOutputStream); } void sendFileContent(InputStream fileContent, StreamObserver<GrpcGetAttachmentContentResponse> responseObserver) { @@ -174,6 +183,22 @@ class AntragraumGrpcService extends AntragraumServiceGrpc.AntragraumServiceImplB throw new TechnicalException(message, e); } + void handleExceptionOnRetrievingContent(FutureTask<Object> task) { + try { + task.get(); + } catch (InterruptedException e) { // NOSONAR + throw new TechnicalException("Retrieving attachment content was interupted.", e); + } catch (ExecutionException e) { + if (e.getCause() instanceof SecurityException) { + throw (SecurityException) e.getCause(); + } + if (e.getCause() instanceof NotFoundException) { + throw (NotFoundException) e.getCause(); + } + throw new TechnicalException("Error on retrieving attachment content.", e); + } + } + @Override public void getAttachmentMetadata(GrpcGetAttachmentMetadataRequest request, StreamObserver<GrpcGetAttachmentMetadataResponse> responseObserver) { var attachment = service.getAttachmentMetadata(attachmentFileRequestMapper.fromGrpcMetadataRequest(request)); diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcServiceTest.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcServiceTest.java index 1d37a4c1baf60cf62359cdd322f615fb469c2d4c..6e99cae1ca29f0bf7b6298719a5eee7d70661826 100644 --- a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcServiceTest.java +++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcServiceTest.java @@ -36,6 +36,10 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; import java.util.stream.Stream; import org.apache.commons.io.IOUtils; @@ -53,9 +57,12 @@ import org.mockito.MockedStatic; import org.mockito.Spy; import com.google.protobuf.ByteString; +import com.thedeanda.lorem.LoremIpsum; +import de.ozgcloud.apilib.common.errorhandling.NotFoundException; import de.ozgcloud.apilib.file.OzgCloudFile; import de.ozgcloud.apilib.file.OzgCloudFileTestFactory; +import de.ozgcloud.common.datatype.StringBasedValue; import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.nachrichten.common.vorgang.GrpcServiceKontoTestFactory; import de.ozgcloud.nachrichten.common.vorgang.Vorgang; @@ -486,6 +493,14 @@ class AntragraumGrpcServiceTest { private MockedConstruction<PipedOutputStream> mockedConstructionOutput; private InputStream connectedInput; + private MockedConstruction<FutureTask> mockedConstructionFutureTask; // NOSONAR + private FutureTask<Object> futureTask; + private Runnable runnable; + + private MockedStatic<Executors> mockedStaticExecutors; + @Mock + private ExecutorService executor; + @BeforeEach void setUpMockedConstruction() { mockedConstructionInput = mockConstruction(PipedInputStream.class, (pipedInputStream, context) -> { @@ -496,12 +511,20 @@ class AntragraumGrpcServiceTest { this.pipedOutputStream = pipedOutputStream; connectedInput = (InputStream) context.arguments().get(0); }); + mockedConstructionFutureTask = mockConstruction(FutureTask.class, (futureTask, context) -> { + this.futureTask = futureTask; // NOSONAR + runnable = (Runnable) context.arguments().get(0); + }); + mockedStaticExecutors = mockStatic(Executors.class); + mockedStaticExecutors.when(() -> Executors.newFixedThreadPool(1)).thenReturn(executor); } @AfterEach void closeMocks() { mockedConstructionInput.close(); mockedConstructionOutput.close(); + mockedConstructionFutureTask.close(); + mockedStaticExecutors.close(); } @Test @@ -526,10 +549,24 @@ class AntragraumGrpcServiceTest { } @Test - void shouldCallGetAttachmentFileContent() { + void shouldCreateNewFixedThreadPool() { + callGetAttachmentContent(); + + mockedStaticExecutors.verify(() -> Executors.newFixedThreadPool(1)); + } + + @Test + void shouldConstructFutureTask() { + callGetAttachmentContent(); + + assertThat(mockedConstructionFutureTask.constructed()).hasSize(1); + } + + @Test + void shouldExecuteTask() { callGetAttachmentContent(); - verify(grpcService).getAttachmentFileContent(grpcRequest, pipedOutputStream); + verify(executor).execute(futureTask); } @Test @@ -538,6 +575,25 @@ class AntragraumGrpcServiceTest { verify(grpcService).sendFileContent(pipedInputStream, responseObserver); } + + @Test + void shouldCallHandleExceptionOnRetrievingContent() { + callGetAttachmentContent(); + + verify(grpcService).handleExceptionOnRetrievingContent(futureTask); + } + + @Nested + class TestTask { + @Test + void shouldCallGetAttachmentFileContent() { + callGetAttachmentContent(); + + runnable.run(); + + verify(grpcService).getAttachmentFileContent(grpcRequest, pipedOutputStream); + } + } } @Nested @@ -579,65 +635,26 @@ class AntragraumGrpcServiceTest { @Mock private PipedOutputStream pipedOutputStream; - private MockedConstruction<Thread> mockedConstructionThread; - private Runnable passedRunnable; - private Thread thread; - private final GrpcGetAttachmentContentRequest grpcRequest = GrpcGetAttachmentContentRequestTestFactory.create(); private final AttachmentFileRequest request = AttachmentFileRequestTestFactory.create(); @BeforeEach void mock() { - mockedConstructionThread = mockConstruction(Thread.class, (mock, context) -> { - passedRunnable = (Runnable) context.arguments().get(0); - thread = mock; - }); - } - - @AfterEach - void closeMock() { - mockedConstructionThread.close(); + when(attachmentFileRequestMapper.fromGrpcContentRequest(grpcRequest)).thenReturn(request); } @Test - void shouldConstructNewThread() { + void shouldMapRequest() { callGetRueckfrageAttachmentFile(); - assertThat(mockedConstructionThread.constructed()).hasSize(1); + verify(attachmentFileRequestMapper).fromGrpcContentRequest(grpcRequest); } @Test - void shouldStartThread() { + void shouldCallServiceToGetAttachmentContent() { callGetRueckfrageAttachmentFile(); - verify(thread).start(); - } - - @Nested - class TestRunnable { - @BeforeEach - void mock() { - when(attachmentFileRequestMapper.fromGrpcContentRequest(grpcRequest)).thenReturn(request); - } - - @Test - void shouldMapRequest() { - callGetRueckfrageAttachmentFile(); - - passedRunnable.run(); - - verify(attachmentFileRequestMapper).fromGrpcContentRequest(grpcRequest); - } - - @Test - void shouldCallServiceToGetAttachmentContent() { - callGetRueckfrageAttachmentFile(); - - passedRunnable.run(); - - verify(service).getAttachmentContent(request, pipedOutputStream); - } - + verify(service).getAttachmentContent(request, pipedOutputStream); } private void callGetRueckfrageAttachmentFile() { @@ -818,6 +835,71 @@ class AntragraumGrpcServiceTest { } + @Nested + class TestHandleExceptionOnRetrievingContent { + + @Mock + private FutureTask<Object> futureTask; + + private final StringBasedValue id = new StringBasedValue() { + }; + private final String entityName = LoremIpsum.getInstance().getWords(1); + + @Test + @SneakyThrows + void shouldGettaskResult() { + callHandleExceptionOnRetrievingContent(); + + verify(futureTask).get(); + } + + @Test + @SneakyThrows + void shouldThrowTechnicalExceptionOnInteruption() { + var interruptedException = new InterruptedException(); + when(futureTask.get()).thenThrow(interruptedException); + + assertThatThrownBy(() -> callHandleExceptionOnRetrievingContent()) + .isInstanceOf(TechnicalException.class) + .hasCause(interruptedException); + } + + @Test + @SneakyThrows + void shouldRethrowSecurityException() { + var securityException = new SecurityException(); + when(futureTask.get()).thenThrow(new ExecutionException(securityException)); + + assertThatThrownBy(() -> callHandleExceptionOnRetrievingContent()) + .isInstanceOf(SecurityException.class); + } + + @Test + @SneakyThrows + void shouldRethrowNotFoundException() { + var notFoundException = new NotFoundException(id, entityName); + when(futureTask.get()).thenThrow(new ExecutionException(notFoundException)); + + assertThatThrownBy(() -> callHandleExceptionOnRetrievingContent()) + .isInstanceOf(NotFoundException.class); + } + + @Test + @SneakyThrows + void shouldThrowTechnicalExceptionOtherExecutionException() { + var executionException = new ExecutionException(new Exception()); + when(futureTask.get()).thenThrow(executionException); + + assertThatThrownBy(() -> callHandleExceptionOnRetrievingContent()) + .isInstanceOf(TechnicalException.class) + .hasCause(executionException); + } + + private void callHandleExceptionOnRetrievingContent() { + grpcService.handleExceptionOnRetrievingContent(futureTask); + } + } + @Nested class TestGetAttachmentMetadata {