diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarder.java b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarder.java index 2909ed3b5712195f45ae6aab8623b0db9998fd27..5fa1cc80c6f40e94cd5ebfcbe29cace8d6962564 100644 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarder.java +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarder.java @@ -26,6 +26,7 @@ import de.ozgcloud.vorgang.vorgang.IncomingFileGroup; import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; import io.grpc.stub.ClientCallStreamObserver; import io.grpc.stub.ClientResponseObserver; +import lombok.Getter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -39,9 +40,13 @@ class EingangForwarder { private ForwardingResponseObserver responseObserver; private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver; - public CompletableFuture<Void> forward(GrpcRouteForwarding grpcRouteForwarding, List<IncomingFileGroup> attachments, + @Getter + private CompletableFuture<Void> forwardFuture; + + public EingangForwarder forward(GrpcRouteForwarding grpcRouteForwarding, List<IncomingFileGroup> attachments, List<IncomingFile> representations) { - return CompletableFuture.allOf( + + forwardFuture = CompletableFuture.allOf( callService(), sendRouteForwarding(grpcRouteForwarding) .thenCompose(ignored -> sendAttachments(attachments)) @@ -54,6 +59,7 @@ class EingangForwarder { } }) ); + return this; } CompletableFuture<GrpcRouteForwardingResponse> callService() { @@ -67,11 +73,15 @@ class EingangForwarder { CompletableFuture<GrpcRouteForwardingResponse> sendRouteForwarding(GrpcRouteForwarding grpcRouteForwarding) { CompletableFuture<GrpcRouteForwardingResponse> future = new CompletableFuture<>(); - responseObserver.registerOnReadyHandler(() -> { + responseObserver.registerOnReadyHandler(getSendRouteForwardingRunnable(grpcRouteForwarding, future)); + return future; + } + + Runnable getSendRouteForwardingRunnable(GrpcRouteForwarding grpcRouteForwarding, CompletableFuture<GrpcRouteForwardingResponse> future) { + return () -> { requestObserver.onNext(GrpcRouteForwardingRequest.newBuilder().setRouteForwarding(grpcRouteForwarding).build()); future.complete(GrpcRouteForwardingResponse.newBuilder().build()); - }); - return future; + }; } CompletableFuture<GrpcRouteForwardingResponse> sendAttachments(List<IncomingFileGroup> attachments) { @@ -92,10 +102,12 @@ class EingangForwarder { return ignored -> sendAttachmentFile(groupName, file); } - private CompletableFuture<GrpcRouteForwardingResponse> sendAttachmentFile(String groupName, IncomingFile file) { + CompletableFuture<GrpcRouteForwardingResponse> sendAttachmentFile(String groupName, IncomingFile file) { var fileContentStream = fileService.getUploadedFileStream(file.getId()); var sender = createAttachmentFileSender(groupName, file, fileContentStream).send(); - return sender.getResultFuture(); + var future = sender.getResultFuture(); + configureToCancelIfForwardFutureCompleted(future); + return future; } StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender(String groupName, @@ -130,14 +142,17 @@ class EingangForwarder { ); } - private Function<GrpcRouteForwardingResponse, CompletableFuture<GrpcRouteForwardingResponse>> getSendRepresentationFileFunction(IncomingFile file) { + private Function<GrpcRouteForwardingResponse, CompletableFuture<GrpcRouteForwardingResponse>> getSendRepresentationFileFunction( + IncomingFile file) { return ignored -> sendRepresentationFile(file); } - private CompletableFuture<GrpcRouteForwardingResponse> sendRepresentationFile(IncomingFile file) { + CompletableFuture<GrpcRouteForwardingResponse> sendRepresentationFile(IncomingFile file) { var fileContentStream = fileService.getUploadedFileStream(file.getId()); var sender = createRepresentationFileSender(file, fileContentStream).send(); - return sender.getResultFuture(); + var future = sender.getResultFuture(); + configureToCancelIfForwardFutureCompleted(future); + return future; } StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender(IncomingFile file, @@ -161,6 +176,14 @@ class EingangForwarder { .build(); } + void configureToCancelIfForwardFutureCompleted(CompletableFuture<GrpcRouteForwardingResponse> future) { + forwardFuture.whenComplete((result, ex) -> { + if (forwardFuture.isDone() && !future.isDone()) { + future.cancel(true); + } + }); + } + GrpcFileContent buildGrpcFileContent(byte[] chunk, int length) { var fileContentBuilder = GrpcFileContent.newBuilder(); if (length <= 0) { @@ -173,7 +196,8 @@ class EingangForwarder { StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata( BiFunction<byte[], Integer, GrpcRouteForwardingRequest> chunkBuilder, InputStream fileContentStream) { - return GrpcFileUploadUtils.createStreamSharingSender(chunkBuilder, fileContentStream, requestObserver, responseObserver::registerOnReadyHandler); + return GrpcFileUploadUtils.createStreamSharingSender(chunkBuilder, fileContentStream, requestObserver, + responseObserver::registerOnReadyHandler); } @RequiredArgsConstructor diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteService.java b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteService.java index 371374eca99a57ee7d85200e254df8d20348851b..b76dde08cd2cf42fd85b5d7dbd830c8f9aec35b0 100644 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteService.java +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteService.java @@ -42,7 +42,7 @@ import net.devh.boot.grpc.client.inject.GrpcClient; @RequiredArgsConstructor class ForwardingRemoteService { - private static final int TIMEOUT_MINUTES = 10; + static final int TIMEOUT_MINUTES = 10; private final VorgangService vorgangService; private final ForwardingRequestMapper forwardingRequestMapper; @GrpcClient("forwarder") @@ -53,7 +53,8 @@ class ForwardingRemoteService { public void forward(ForwardingRequest request) { var eingang = vorgangService.getById(request.getVorgangId()).getEingangs().getFirst(); var grpcRouteForwarding = forwardingRequestMapper.toGrpcRouteForwarding(request, eingang); - var responseFuture = new EingangForwarder(serviceStub, fileService, incomingFileMapper).forward(grpcRouteForwarding, eingang.getAttachments(), eingang.getRepresentations()); + var responseFuture = new EingangForwarder(serviceStub, fileService, incomingFileMapper).forward(grpcRouteForwarding, eingang.getAttachments(), + eingang.getRepresentations()).getForwardFuture(); waitForCompletion(responseFuture); } diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/IncomingFileGroupTestFactory.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/IncomingFileGroupTestFactory.java index f95a612361b800020d17bc95b39bcb2e3dae6a57..4075d2c3bd063b6af34b47df2f3ce43f5ee723db 100644 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/IncomingFileGroupTestFactory.java +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/IncomingFileGroupTestFactory.java @@ -23,15 +23,23 @@ */ package de.ozgcloud.vorgang.vorgang; +import de.ozgcloud.vorgang.files.FileId; + public class IncomingFileGroupTestFactory { public static final String NAME = GrpcIncomingFileGroupTestFactory.NAME; public static final IncomingFile FILE = IncomingFileTestFactory.create(); + public static final IncomingFile FILE2 = IncomingFileTestFactory.createBuilder() + .id(FileId.createNew()).build(); public static IncomingFileGroup create() { return createBuilder().build(); } + public static IncomingFileGroup createWithTwoFiles() { + return createBuilder().file(FILE2).build(); + } + public static IncomingFileGroup.IncomingFileGroupBuilder createBuilder() { return IncomingFileGroup.builder() .name(NAME) diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarderTest.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..78f1837c3b657bd7221ba2e2c1a6d0a493d3fb2a --- /dev/null +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarderTest.java @@ -0,0 +1,689 @@ +package de.ozgcloud.vorgang.vorgang.redirect; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.eq; + +import java.io.InputStream; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +import org.apache.commons.lang3.RandomUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Spy; + +import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; +import de.ozgcloud.common.binaryfile.StreamingFileSender; +import de.ozgcloud.common.test.ReflectionTestUtils; +import de.ozgcloud.eingang.forwarder.RouteForwardingServiceGrpc; +import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding; +import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest; +import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingResponse; +import de.ozgcloud.vorgang.callcontext.VorgangManagerClientCallContextAttachingInterceptor; +import de.ozgcloud.vorgang.files.FileId; +import de.ozgcloud.vorgang.files.FileService; +import de.ozgcloud.vorgang.vorgang.IncomingFile; +import de.ozgcloud.vorgang.vorgang.IncomingFileGroup; +import de.ozgcloud.vorgang.vorgang.IncomingFileGroupTestFactory; +import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; +import de.ozgcloud.vorgang.vorgang.IncomingFileTestFactory; +import de.ozgcloud.vorgang.vorgang.redirect.EingangForwarder.ForwardingResponseObserver; +import io.grpc.stub.ClientCallStreamObserver; + +class EingangForwarderTest { + + @Mock + private RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; + @Mock + private FileService fileService; + @Mock + private IncomingFileMapper incomingFileMapper; + @InjectMocks + @Spy + private EingangForwarder forwarder; + + @Nested + class TestForward { + + @Mock + private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver; + @Mock + private GrpcRouteForwarding grpcRouteForwarding; + private final List<IncomingFileGroup> attachments = List.of(IncomingFileGroupTestFactory.create()); + private final List<IncomingFile> representations = List.of(IncomingFileTestFactory.create()); + + @BeforeEach + void init() { + setRequestObserverInForwarder(requestObserver); + } + + @Test + void shouldCallOnCompletedOnSuccess() { + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).callService(); + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).sendRouteForwarding(grpcRouteForwarding); + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).sendAttachments(attachments); + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).sendRepresentations(representations); + + CompletableFuture<Void> future = forwarder.forward(grpcRouteForwarding, attachments, representations).getForwardFuture(); + + assertOnCompletedCalled(future); + } + + @Test + void shouldCallOnErrorOnFailureInRouteForwarding() { + var error = new RuntimeException("Route forwarding failed"); + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).callService(); + doReturn(CompletableFuture.failedFuture(error)).when(forwarder).sendRouteForwarding(grpcRouteForwarding); + + var future = forwarder.forward(grpcRouteForwarding, attachments, representations).getForwardFuture(); + + assertOnErrorCalled(future, error); + } + + @Test + void shouldCallOnErrorOnFailureInSendAttachments() { + var error = new RuntimeException("Send attachments failed"); + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).callService(); + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).sendRouteForwarding(grpcRouteForwarding); + doReturn(CompletableFuture.failedFuture(error)).when(forwarder).sendAttachments(attachments); + + var future = forwarder.forward(grpcRouteForwarding, attachments, representations).getForwardFuture(); + + assertOnErrorCalled(future, error); + } + + @Test + void shouldCallOnErrorOnFailureInSendRepresentations() { + var error = new RuntimeException("Send representations failed"); + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).callService(); + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).sendRouteForwarding(grpcRouteForwarding); + doReturn(CompletableFuture.completedFuture(null)).when(forwarder).sendAttachments(attachments); + doReturn(CompletableFuture.failedFuture(error)).when(forwarder).sendRepresentations(representations); + + var future = forwarder.forward(grpcRouteForwarding, attachments, representations).getForwardFuture(); + + assertOnErrorCalled(future, error); + } + + private void assertOnCompletedCalled(CompletableFuture<Void> future) { + future.join(); + verify(requestObserver).onCompleted(); + verify(requestObserver, never()).onError(any()); + } + + private void assertOnErrorCalled(CompletableFuture<Void> future, Throwable error) { + future.handle((result, ex) -> { + verify(requestObserver).onError(argThat(e -> e instanceof CompletionException && e.getCause() == error)); + verify(requestObserver, never()).onCompleted(); + return null; + }).join(); + } + } + + @Nested + class TestCallService { + + @BeforeEach + void init() { + when(serviceStub.withInterceptors(any())).thenReturn(serviceStub); + } + + @Test + void shouldAttachClientCallContextToServiceStub() { + forwarder.callService(); + + verify(serviceStub).withInterceptors(any(VorgangManagerClientCallContextAttachingInterceptor.class)); + } + + @Test + void shouldCreateResponseObserver() { + forwarder.callService(); + + assertThat(getResponseObserverFromForwarder()).isNotNull(); + } + + @Test + void shouldMakeGrpcCallToRouteForwarding() { + forwarder.callService(); + + verify(serviceStub).routeForwarding(getResponseObserverFromForwarder()); + } + } + + @Nested + class TestSendRouteForwarding { + + private final GrpcRouteForwarding grpcRouteForwarding = GrpcRouteForwarding.newBuilder().build(); + @Mock + private ForwardingResponseObserver responseObserver; + @Mock + private Runnable onReadyHandler; + @Captor + private ArgumentCaptor<Runnable> onReadyHandlerCaptor; + + @BeforeEach + void init() { + setResponseObserverInForwarder(responseObserver); + doReturn(onReadyHandler).when(forwarder).getSendRouteForwardingRunnable(any(), any()); + } + + @Test + void shouldGetSendRouteForwardingRunnable() { + var future = forwarder.sendRouteForwarding(grpcRouteForwarding); + + verify(forwarder).getSendRouteForwardingRunnable(grpcRouteForwarding, future); + } + + @Test + void shouldRegisterOnReadyHandler() { + forwarder.sendRouteForwarding(grpcRouteForwarding); + + verify(responseObserver).registerOnReadyHandler(onReadyHandlerCaptor.capture()); + assertThatIsResultOfGetSendRouteForwardingRunnable(onReadyHandlerCaptor.getValue()); + } + + private void assertThatIsResultOfGetSendRouteForwardingRunnable(Runnable runnable) { + runnable.run(); + verify(onReadyHandler).run(); + } + } + + @Nested + class TestGetSendRouteForwardingRunnable { + + private final GrpcRouteForwarding grpcRouteForwarding = GrpcRouteForwardingTestFactory.create(); + @Mock + private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver; + @Mock + private CompletableFuture<GrpcRouteForwardingResponse> future; + + @BeforeEach + void init() { + setRequestObserverInForwarder(requestObserver); + } + + @Test + void shouldCallOnNext() { + forwarder.getSendRouteForwardingRunnable(grpcRouteForwarding, future).run(); + + verify(requestObserver).onNext(GrpcRouteForwardingRequestTestFactory.create()); + } + + @Test + void shouldCallOnComplete() { + forwarder.getSendRouteForwardingRunnable(grpcRouteForwarding, future).run(); + + verify(future).complete(GrpcRouteForwardingResponse.newBuilder().build()); + } + } + + @Nested + class TestSendAttachments { + + private final List<IncomingFileGroup> attachments = List.of(IncomingFileGroupTestFactory.createWithTwoFiles()); + private final CompletableFuture<GrpcRouteForwardingResponse> future = new CompletableFuture<>(); + private final CompletableFuture<GrpcRouteForwardingResponse> future2 = new CompletableFuture<>(); + + @BeforeEach + void init() { + doReturn(future, future2).when(forwarder).sendAttachmentFile(any(), any()); + } + + @Test + void shouldReturnFuture() { + var returned = forwarder.sendAttachments(attachments); + + assertThat(returned).isNotNull(); + } + + @Test + void shouldInitiallySendOnlyFirstFile() { + forwarder.sendAttachments(attachments); + + verify(forwarder).sendAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE); + verify(forwarder, times(1)).sendAttachmentFile(anyString(), any()); + } + + @Test + void shouldSendSecondFileAfterFirstFutureCompleted() { + forwarder.sendAttachments(attachments); + + future.complete(GrpcRouteForwardingResponse.newBuilder().build()); + + verify(forwarder).sendAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE2); + verify(forwarder, times(2)).sendAttachmentFile(anyString(), any()); + } + + @Test + void shouldReturnedFutureBeInitiallyIncomplete() { + var returned = forwarder.sendAttachments(attachments); + + assertThat(returned.isDone()).isFalse(); + } + + @Test + void shouldReturnedFutureBeIncompleteAfterSendingFirstFile() { + var returned = forwarder.sendAttachments(attachments); + + future.complete(GrpcRouteForwardingResponse.newBuilder().build()); + + assertThat(returned.isDone()).isFalse(); + } + + @Test + void shouldReturnedFutureBeDoneAfterSendingAllFiles() { + var returned = forwarder.sendAttachments(attachments); + + future.complete(GrpcRouteForwardingResponse.newBuilder().build()); + future2.complete(GrpcRouteForwardingResponse.newBuilder().build()); + + assertThat(returned.isDone()).isTrue(); + } + } + + @Nested + class TestSendAttachmentFile { + + @Mock + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; + private final CompletableFuture<GrpcRouteForwardingResponse> resultFuture = new CompletableFuture<>(); + @Mock + private InputStream fileContentStream; + + @BeforeEach + void init() { + when(fileService.getUploadedFileStream(any())).thenReturn(fileContentStream); + doReturn(fileSender).when(forwarder).createAttachmentFileSender(any(), any(), any()); + doReturn(fileSender).when(fileSender).send(); + when(fileSender.getResultFuture()).thenReturn(resultFuture); + doNothing().when(forwarder).configureToCancelIfForwardFutureCompleted(any()); + } + + @Test + void shouldGetUploadFileStream() { + sendAttachmentFile(); + + verify(fileService).getUploadedFileStream(IncomingFileGroupTestFactory.FILE.getId()); + } + + @Test + void shouldCreateAttachmentFileSender() { + sendAttachmentFile(); + + verify(forwarder).createAttachmentFileSender(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE, fileContentStream); + } + + @Test + void shouldSend() { + sendAttachmentFile(); + + verify(fileSender).send(); + } + + @Test + void shouldConfigureFutureToCancelIfForwardFutureCompleted() { + sendAttachmentFile(); + + verify(forwarder).configureToCancelIfForwardFutureCompleted(resultFuture); + } + + @Test + void shouldReturnResultFuture() { + var returned = sendAttachmentFile(); + + assertThat(returned).isSameAs(resultFuture); + } + + private CompletableFuture<GrpcRouteForwardingResponse> sendAttachmentFile() { + return forwarder.sendAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE); + } + } + + @Nested + class TestCreateAttachmentFileSender { + + @Mock + private InputStream inputStream; + @Mock + private GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; + @Mock + private GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSenderWithMetadata; + @Captor + private ArgumentCaptor<BiFunction<byte[], Integer, GrpcRouteForwardingRequest>> chunkBuilderCaptor; + + private final byte[] chunk = RandomUtils.insecure().randomBytes(5); + private final GrpcRouteForwardingRequest metadataRequest = GrpcRouteForwardingRequestTestFactory.create(); + + @BeforeEach + void init() { + doReturn(fileSender).when(forwarder).createSenderWithoutMetadata(any(), any()); + doReturn(metadataRequest).when(forwarder).buildGrpcAttachmentFile(any(), any()); + when(fileSender.withMetaData(any())).thenReturn(fileSenderWithMetadata); + } + + @Test + void shouldCallCreateSenderWithoutMetadata() { + createAttachmentFileSender(); + + verify(forwarder).createSenderWithoutMetadata(chunkBuilderCaptor.capture(), eq(inputStream)); + chunkBuilderCaptor.getValue().apply(chunk, chunk.length); + verify(forwarder).buildAttachmentChunk(chunk, chunk.length); + } + + @Test + void shouldCallBuildGrpcAttachmentFile() { + createAttachmentFileSender(); + + verify(forwarder).buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE); + } + + @Test + void shouldSetMetaData() { + createAttachmentFileSender(); + + verify(fileSender).withMetaData(metadataRequest); + } + + @Test + void shouldReturnBuiltFileSender() { + var returnedFileSender = createAttachmentFileSender(); + + assertThat(returnedFileSender).isSameAs(fileSenderWithMetadata); + } + + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender() { + return forwarder.createAttachmentFileSender(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE, inputStream); + } + } + + @Nested + class TestBuildAttachmentChunk { + + private final byte[] chunk = RandomUtils.insecure().randomBytes(5); + + @BeforeEach + void mock() { + doReturn(GrpcAttachmentTestFactory.CONTENT).when(forwarder).buildGrpcFileContent(any(), anyInt()); + } + + @Test + void shouldCallBuildGrpcFileContent() { + forwarder.buildAttachmentChunk(chunk, chunk.length); + + verify(forwarder).buildGrpcFileContent(chunk, chunk.length); + } + + @Test + void shouldReturnGrpcRouteForwardingRequest() { + var result = forwarder.buildAttachmentChunk(chunk, chunk.length); + + assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithAttachmentContent()); + } + } + + @Nested + class TestBuildGrpcAttachmentFile { + + private final IncomingFile file = IncomingFileTestFactory.create(); + + @BeforeEach + void mock() { + when(incomingFileMapper.toAttachmentFile(any(), any())).thenReturn(GrpcAttachmentFileTestFactory.create()); + } + + @Test + void shouldCallIncomingFileMapper() { + forwarder.buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, file); + + verify(incomingFileMapper).toAttachmentFile(IncomingFileGroupTestFactory.NAME, file); + } + + @Test + void shouldReturnAttachmentMetadataRequest() { + var result = forwarder.buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, file); + + assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithAttachmentMetadata()); + } + } + + @Nested + class TestSendRepresentations { + + private static final IncomingFile FILE = IncomingFileTestFactory.create(); + private static final IncomingFile FILE2 = IncomingFileTestFactory.createBuilder().id(FileId.createNew()).build(); + private final List<IncomingFile> representations = List.of(FILE, FILE2); + private final CompletableFuture<GrpcRouteForwardingResponse> future = new CompletableFuture<>(); + private final CompletableFuture<GrpcRouteForwardingResponse> future2 = new CompletableFuture<>(); + + @BeforeEach + void init() { + doReturn(future, future2).when(forwarder).sendRepresentationFile(any()); + } + + @Test + void shouldReturnFuture() { + var returned = forwarder.sendRepresentations(representations); + + assertThat(returned).isNotNull(); + } + } + + @Nested + class TestCreateRepresentationFileSender { + + @Mock + private InputStream inputStream; + @Mock + private GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; + @Mock + private GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSenderWithMetadata; + @Captor + private ArgumentCaptor<BiFunction<byte[], Integer, GrpcRouteForwardingRequest>> chunkBuilderCaptor; + + private final byte[] chunk = RandomUtils.insecure().randomBytes(5); + private final GrpcRouteForwardingRequest metadataRequest = GrpcRouteForwardingRequestTestFactory.create(); + private final IncomingFile incomingFile = IncomingFileTestFactory.create(); + + @BeforeEach + void init() { + doReturn(fileSender).when(forwarder).createSenderWithoutMetadata(any(), any()); + doReturn(metadataRequest).when(forwarder).buildGrpcRepresentationFile(any()); + when(fileSender.withMetaData(any())).thenReturn(fileSenderWithMetadata); + } + + @Test + void shouldCallCreateSenderWithoutMetadata() { + createRepresentationFileSender(); + + verify(forwarder).createSenderWithoutMetadata(chunkBuilderCaptor.capture(), eq(inputStream)); + chunkBuilderCaptor.getValue().apply(chunk, chunk.length); + verify(forwarder).buildRepresentationChunk(chunk, chunk.length); + } + + @Test + void shouldCallBuildGrpcRepresentationFile() { + createRepresentationFileSender(); + + verify(forwarder).buildGrpcRepresentationFile(incomingFile); + } + + @Test + void shouldSetMetaData() { + createRepresentationFileSender(); + + verify(fileSender).withMetaData(metadataRequest); + } + + @Test + void shouldReturnBuiltFileSender() { + var returnedFileSender = createRepresentationFileSender(); + + assertThat(returnedFileSender).isSameAs(fileSenderWithMetadata); + } + + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender() { + return forwarder.createRepresentationFileSender(incomingFile, inputStream); + } + } + + @Nested + class TestBuildRepresentationChunk { + + private final byte[] chunk = RandomUtils.insecure().randomBytes(5); + + @BeforeEach + void mock() { + doReturn(GrpcRepresentationTestFactory.CONTENT).when(forwarder).buildGrpcFileContent(any(), anyInt()); + } + + @Test + void shouldCallBuildGrpcFileContent() { + forwarder.buildRepresentationChunk(chunk, chunk.length); + + verify(forwarder).buildGrpcFileContent(chunk, chunk.length); + } + + @Test + void shouldReturnGrpcRouteForwardingRequest() { + var result = forwarder.buildRepresentationChunk(chunk, chunk.length); + + assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithRepresentationContent()); + } + } + + @Nested + class TestBuildGrpcRepresentationFile { + + private final IncomingFile file = IncomingFileTestFactory.create(); + + @BeforeEach + void mock() { + when(incomingFileMapper.toRepresentationFile(any())).thenReturn(GrpcRepresentationFileTestFactory.create()); + } + + @Test + void shouldCallIncomingFileMapper() { + forwarder.buildGrpcRepresentationFile(file); + + verify(incomingFileMapper).toRepresentationFile(file); + } + + @Test + void shouldReturnRepresentationMetadataRequest() { + var result = forwarder.buildGrpcRepresentationFile(file); + + assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithRepresentationMetadata()); + } + } + + @Nested + class TestBuildGrpcFileContent { + + @Nested + class TestOnEndOfFile { + + @Test + void shouldBuildEndOfFileChunk() { + var fileContent = forwarder.buildGrpcFileContent(new byte[0], -1); + + assertThat(fileContent).isEqualTo(GrpcFileContentTestFactory.createEndOfFile()); + } + } + + @Nested + class TestOnContentProvided { + + @Test + void shouldBuildEndOfFileChunk() { + var fileContent = forwarder.buildGrpcFileContent(GrpcFileContentTestFactory.CONTENT, GrpcFileContentTestFactory.CONTENT.length); + + assertThat(fileContent).isEqualTo(GrpcFileContentTestFactory.create()); + } + } + } + + @Nested + class TestCreateSenderWithoutMetadata { + + private MockedStatic<GrpcFileUploadUtils> grpcFileUploadUtilsMock; + @Mock + private BiFunction<byte[], Integer, GrpcRouteForwardingRequest> chunkBuilder; + @Mock + private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver; + @Mock + private ForwardingResponseObserver responseObserver; + @Mock + private InputStream inputStream; + @Mock + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; + @Mock + private Runnable onReadyHandler; + @Captor + private ArgumentCaptor<Consumer<Runnable>> onReadyHandlerCaptor; + + @BeforeEach + void init() { + grpcFileUploadUtilsMock = mockStatic(GrpcFileUploadUtils.class); + grpcFileUploadUtilsMock.when(() -> GrpcFileUploadUtils.createStreamSharingSender(any(), any(), any(), any())).thenReturn(fileSender); + ReflectionTestUtils.setField(forwarder, "responseObserver", responseObserver); + ReflectionTestUtils.setField(forwarder, "requestObserver", requestObserver); + } + + @AfterEach + void tearDown() { + grpcFileUploadUtilsMock.close(); + } + + @Test + void shouldCreateFileSender() { + + createSenderWithoutMetadata(); + + grpcFileUploadUtilsMock + .verify(() -> GrpcFileUploadUtils.<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse>createStreamSharingSender( + eq(chunkBuilder), eq(inputStream), eq(requestObserver), onReadyHandlerCaptor.capture())); + assertIsRegisterOnReadyHandler(onReadyHandlerCaptor); + } + + @Test + void shouldReturnCreatedFileSender() { + var returnedFileSender = createSenderWithoutMetadata(); + + assertThat(returnedFileSender).isSameAs(fileSender); + } + + private void assertIsRegisterOnReadyHandler(ArgumentCaptor<Consumer<Runnable>> captor) { + captor.getValue().accept(onReadyHandler); + verify(responseObserver).registerOnReadyHandler(onReadyHandler); + } + + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata() { + return forwarder.createSenderWithoutMetadata(chunkBuilder, inputStream); + } + } + + private void setResponseObserverInForwarder(ForwardingResponseObserver responseObserver) { + ReflectionTestUtils.setField(forwarder, "responseObserver", responseObserver); + } + + private ForwardingResponseObserver getResponseObserverFromForwarder() { + return ReflectionTestUtils.getField(forwarder, "responseObserver", ForwardingResponseObserver.class); + } + + private void setRequestObserverInForwarder(ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver) { + ReflectionTestUtils.setField(forwarder, "requestObserver", requestObserver); + } +} diff --git a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteServiceTest.java b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3df3152b6a388d150b384d6cd918639d79f24f7d --- /dev/null +++ b/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteServiceTest.java @@ -0,0 +1,123 @@ +package de.ozgcloud.vorgang.vorgang.redirect; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.eingang.forwarder.RouteForwardingServiceGrpc; +import de.ozgcloud.vorgang.files.FileService; +import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; +import de.ozgcloud.vorgang.vorgang.VorgangService; +import lombok.SneakyThrows; + +class ForwardingRemoteServiceTest { + + @Mock + private VorgangService vorgangService; + @Mock + private ForwardingRequestMapper forwardingRequestMapper; + @Mock + private RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; + @Mock + private FileService fileService; + @Mock + private IncomingFileMapper incomingFileMapper; + @InjectMocks + @Spy + private ForwardingRemoteService service; + + @Nested + class TestWaitForCompletion { + + @Mock + private CompletableFuture<Void> future; + + @SneakyThrows + @Test + void shouldGetFromFuture() { + waitForCompletion(); + + verify(future).get(ForwardingRemoteService.TIMEOUT_MINUTES, TimeUnit.MINUTES); + } + + @Nested + class TestOnInterruptedException { + + private final InterruptedException exception = new InterruptedException(); + + @BeforeEach + @SneakyThrows + void mock() { + when(future.get(anyLong(), any())).thenThrow(exception); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, de.ozgcloud.vorgang.vorgang.redirect.ForwardingRemoteServiceTest.TestWaitForCompletion.this::waitForCompletion); + } + + @Test + void shouldInterruptThread() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + assertThat(Thread.currentThread().isInterrupted()).isTrue(); + } + } + + @Nested + class TestOnExecutionException { + + private final ExecutionException exception = new ExecutionException(new Exception()); + + @BeforeEach + @SneakyThrows + void mock() { + when(future.get(anyLong(), any())).thenThrow(exception); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, de.ozgcloud.vorgang.vorgang.redirect.ForwardingRemoteServiceTest.TestWaitForCompletion.this::waitForCompletion); + } + } + + @Nested + class TestOnTimeoutException { + + private final TimeoutException exception = new TimeoutException(); + + @BeforeEach + @SneakyThrows + void mock() { + when(future.get(anyLong(), any())).thenThrow(exception); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, de.ozgcloud.vorgang.vorgang.redirect.ForwardingRemoteServiceTest.TestWaitForCompletion.this::waitForCompletion); + } + } + + private void waitForCompletion() { + service.waitForCompletion(future); + } + } +}