diff --git a/collaboration-manager-interface/src/main/protobuf/collaboration.model.proto b/collaboration-manager-interface/src/main/protobuf/collaboration.model.proto index 8666ef1741a63fc287746088b92e9f3b211d2d1c..2add82e8addc986fe55016290c1c3e47782c07fa 100644 --- a/collaboration-manager-interface/src/main/protobuf/collaboration.model.proto +++ b/collaboration-manager-interface/src/main/protobuf/collaboration.model.proto @@ -120,4 +120,13 @@ message GrpcFormField { string name = 1; string label = 2; string value = 3; +} + +message GrpcGetFileContentRequest { + string samlToken = 1; + string id = 2; +} + +message GrpcGetFileContentResponse { + bytes fileContent = 1; } \ No newline at end of file diff --git a/collaboration-manager-interface/src/main/protobuf/collaboration.proto b/collaboration-manager-interface/src/main/protobuf/collaboration.proto index 33cf53ed2f4e93a8cc2169a15350fcc054b2d442..fae46694d42cd9da6f5d3f7d3ec7e9a11c6d9991 100644 --- a/collaboration-manager-interface/src/main/protobuf/collaboration.proto +++ b/collaboration-manager-interface/src/main/protobuf/collaboration.proto @@ -35,4 +35,7 @@ service CollaborationService { rpc FindVorgang(GrpcFindVorgangRequest) returns (GrpcFindVorgangResponse) { } + + rpc GetFileContent(GrpcGetFileContentRequest) returns (stream GrpcGetFileContentResponse) { + } } \ No newline at end of file diff --git a/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationEventListener.java b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationEventListener.java index bedfa508a2bf83651c5d5abcdfbae1801fd8efdb..f7e3362bb1c9cf7ad5df00a9929628be7c936740 100644 --- a/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationEventListener.java +++ b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationEventListener.java @@ -48,8 +48,7 @@ class CollaborationEventListener { public static final String CREATE_COLLABORATION_REQUEST_ORDER = "CREATE_COLLABORATION_REQUEST"; public static final Predicate<Command> IS_CREATE_COLLABORATION_REQUEST_COMMAND = command -> CREATE_COLLABORATION_REQUEST_ORDER.equals( command.getOrder()); - private static final String IS_CREATE_COLLABORATION_REQUEST = - "{T(de.ozgcloud.collaboration.CollaborationEventListener).IS_CREATE_COLLABORATION_REQUEST_COMMAND.test(event.getSource())}"; + private static final String IS_CREATE_COLLABORATION_REQUEST = "{T(de.ozgcloud.collaboration.CollaborationEventListener).IS_CREATE_COLLABORATION_REQUEST_COMMAND.test(event.getSource())}"; private static final String LOG_MESSAGE_TEMPLATE = "{}. Command failed."; private static final String ERROR_MESSAGE_TEMPLATE = "Error on executing %s Command (id: %s)."; diff --git a/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationGrpcService.java b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationGrpcService.java index 17d5c1c2bda129376973019c196c08d8673f37e1..cc18107d4389788104f2de800e1d70c12aaf85b0 100644 --- a/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationGrpcService.java +++ b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationGrpcService.java @@ -23,9 +23,16 @@ */ package de.ozgcloud.collaboration; +import org.springframework.core.task.TaskExecutor; + +import com.google.protobuf.ByteString; + +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.collaboration.common.grpc.GrpcDownloader; import de.ozgcloud.collaboration.vorgang.CollaborationVorgangMapper; import de.ozgcloud.collaboration.vorgang.Vorgang; import de.ozgcloud.collaboration.vorgang.VorgangService; +import io.grpc.stub.CallStreamObserver; import io.grpc.stub.StreamObserver; import lombok.RequiredArgsConstructor; import net.devh.boot.grpc.server.service.GrpcService; @@ -34,13 +41,19 @@ import net.devh.boot.grpc.server.service.GrpcService; @RequiredArgsConstructor public class CollaborationGrpcService extends CollaborationServiceGrpc.CollaborationServiceImplBase { - private final VorgangService vorgangService; + public static final int CHUNK_SIZE = 255 * 1024; + private final CollaborationService service; private final CollaborationVorgangMapper vorgangMapper; + private final VorgangService vorgangService; + + private final TaskExecutor taskExecutor; + @Override public void findVorgang(GrpcFindVorgangRequest request, StreamObserver<GrpcFindVorgangResponse> responseObserver) { var vorgang = vorgangService.getVorgang(request.getVorgangId()); + responseObserver.onNext(buildFindVorgangResponse(vorgang)); responseObserver.onCompleted(); } @@ -50,4 +63,23 @@ public class CollaborationGrpcService extends CollaborationServiceGrpc.Collabora .setVorgang(vorgangMapper.toGrpc(vorgang)) .build(); } -} + + @Override + public void getFileContent(GrpcGetFileContentRequest request, StreamObserver<GrpcGetFileContentResponse> responseObserver) { + buildDownloader(request.getId(), responseObserver).start(); + } + + GrpcDownloader<GrpcGetFileContentResponse> buildDownloader(String fileId, + StreamObserver<GrpcGetFileContentResponse> responseObserver) { + return GrpcDownloader.<GrpcGetFileContentResponse>builder() + .callObserver((CallStreamObserver<GrpcGetFileContentResponse>) responseObserver) + .downloadConsumer(outputStream -> service.writeFileContent(OzgCloudFileId.from(fileId), outputStream)) + .chunkBuilder(this::buildChunkResponse) + .taskExecutor(taskExecutor) + .build(); + } + + GrpcGetFileContentResponse buildChunkResponse(ByteString bytes) { + return GrpcGetFileContentResponse.newBuilder().setFileContent(bytes).build(); + } +} \ No newline at end of file diff --git a/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationService.java b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationService.java index aefb5b81224ba56834d9ea7d44e02677c1aed33e..40e51fc27c73e3163fe5ddfe082ee5da34b7fd66 100644 --- a/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationService.java +++ b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/CollaborationService.java @@ -23,6 +23,8 @@ */ package de.ozgcloud.collaboration; +import java.io.OutputStream; + import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Qualifier; @@ -31,6 +33,8 @@ import org.springframework.validation.annotation.Validated; import de.ozgcloud.apilib.common.command.OzgCloudCommandService; import de.ozgcloud.apilib.common.command.OzgCloudCreateSubCommandsRequest; +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.apilib.file.OzgCloudFileService; import de.ozgcloud.collaboration.common.user.UserProfileService; import de.ozgcloud.collaboration.vorgang.VorgangService; import lombok.RequiredArgsConstructor; @@ -52,6 +56,8 @@ public class CollaborationService { @Qualifier(CollaborationManagerConfiguration.USER_PROFILE_SERVICE_NAME) // NOSONAR private final UserProfileService userProfileService; + private final OzgCloudFileService fileService; + private final CollaborationManagerCollaborationRequestMapper collaborationRequestMapper; public void createCollaborationRequest(@Valid CollaborationRequest collaborationRequest) { @@ -76,4 +82,7 @@ public class CollaborationService { .build(); } -} + public void writeFileContent(OzgCloudFileId fileId, OutputStream out) { + fileService.writeFileDataToStream(fileId, out); + } +} \ No newline at end of file diff --git a/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/common/grpc/ExceptionRunnable.java b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/common/grpc/ExceptionRunnable.java new file mode 100644 index 0000000000000000000000000000000000000000..0fc69b2046870612318ed74500e3eb83c6884f89 --- /dev/null +++ b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/common/grpc/ExceptionRunnable.java @@ -0,0 +1,7 @@ +package de.ozgcloud.collaboration.common.grpc; + +import java.io.IOException; + +interface ExceptionRunnable { + void run() throws IOException; // NOSONAR +} \ No newline at end of file diff --git a/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/common/grpc/GrpcDownloader.java b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/common/grpc/GrpcDownloader.java new file mode 100644 index 0000000000000000000000000000000000000000..6d777066a4cea56cb6df3276760a5f44df47b567 --- /dev/null +++ b/collaboration-manager-server/src/main/java/de/ozgcloud/collaboration/common/grpc/GrpcDownloader.java @@ -0,0 +1,109 @@ +package de.ozgcloud.collaboration.common.grpc; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.springframework.core.task.TaskExecutor; + +import com.google.protobuf.ByteString; + +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.common.errorhandling.TechnicalException; +import io.grpc.stub.CallStreamObserver; +import lombok.Builder; + +public class GrpcDownloader<T> { + + private static final int CHUNK_SIZE = 255 * 1024; + + private CallStreamObserver<T> callObserver; + private Function<ByteString, T> chunkBuilder; + private Consumer<OutputStream> downloadConsumer; + private TaskExecutor taskExecutor; + + private final byte[] buffer = new byte[GrpcDownloader.CHUNK_SIZE]; + private final AtomicBoolean downloadInProgress = new AtomicBoolean(false); + + private PipedInputStream inputStream; + private PipedOutputStream outputStream; + + @Builder + public GrpcDownloader(CallStreamObserver<T> callObserver, OzgCloudFileId fileId, Function<ByteString, T> chunkBuilder, + Consumer<OutputStream> downloadConsumer, TaskExecutor taskExecutor) { + this.callObserver = callObserver; + this.chunkBuilder = chunkBuilder; + this.downloadConsumer = downloadConsumer; + this.taskExecutor = taskExecutor; + } + + public void start() { + handleSafety(this::doStart); + } + + void doStart() throws IOException { + setupStreams(); + taskExecutor.execute(this::startDownload); + callObserver.setOnReadyHandler(this::onReadyHandler); + } + + void setupStreams() throws IOException { + outputStream = new PipedOutputStream(); + inputStream = new PipedInputStream(GrpcDownloader.CHUNK_SIZE); + outputStream.connect(inputStream); + } + + void startDownload() { + handleSafety(this::doDownload); + } + + void doDownload() throws IOException { + downloadInProgress.set(true); + downloadConsumer.accept(outputStream); + downloadInProgress.set(false); + outputStream.close(); + } + + synchronized void onReadyHandler() { + if (callObserver.isReady()) { + sendChunks(); + } + } + + void sendChunks() { + handleSafety(this::doSendChunks); + } + + void doSendChunks() throws IOException { + int bytesRead; + while (callObserver.isReady() && (bytesRead = inputStream.read(buffer)) != -1) { + callObserver.onNext(chunkBuilder.apply(ByteString.copyFrom(buffer, 0, bytesRead))); + } + if (!downloadInProgress.get()) { + inputStream.close(); + callObserver.onCompleted(); + } + } + + void handleSafety(ExceptionRunnable runnable) { + try { + runnable.run(); + } catch (IOException e) { + try { + inputStream.close(); + outputStream.close(); + } catch (IOException e1) { + throwException(e1); + } + throwException(e); + } + } + + private void throwException(IOException e) { + throw new TechnicalException("Error occurred during downloading file content download.", e); + } +} \ No newline at end of file diff --git a/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationEventListenerITCase.java b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationEventListenerITCase.java index 641aec972ec11ef3a6bcc27022c91556b2206f5f..fbb35a1212b534eeed6a723475225b7742e2e902 100644 --- a/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationEventListenerITCase.java +++ b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationEventListenerITCase.java @@ -28,9 +28,11 @@ import static org.mockito.Mockito.*; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.context.ApplicationEventPublisher; +import de.ozgcloud.apilib.file.OzgCloudFileService; import de.ozgcloud.command.Command; import de.ozgcloud.command.CommandCreatedEventTestFactory; import de.ozgcloud.command.CommandTestFactory; @@ -45,6 +47,9 @@ class CollaborationEventListenerITCase { @Autowired private ApplicationEventPublisher publisher; + @MockBean + private OzgCloudFileService fileService; + @Nested class TestOnCreateCollaborationRequest { diff --git a/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationGrpcServiceTest.java b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationGrpcServiceTest.java index b95b5e952486d72dfd99a17ac67f60b37ed76a45..7614a782ae107dcd1fdd6d3e5c521d6777b98e36 100644 --- a/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationGrpcServiceTest.java +++ b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationGrpcServiceTest.java @@ -27,19 +27,31 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.io.OutputStream; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; + import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; 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 org.springframework.core.task.TaskExecutor; +import org.springframework.test.util.ReflectionTestUtils; + +import com.google.protobuf.ByteString; +import de.ozgcloud.collaboration.common.grpc.GrpcDownloader; import de.ozgcloud.collaboration.vorgang.CollaborationVorgangMapper; import de.ozgcloud.collaboration.vorgang.GrpcVorgangTestFactory; import de.ozgcloud.collaboration.vorgang.Vorgang; import de.ozgcloud.collaboration.vorgang.VorgangService; import de.ozgcloud.collaboration.vorgang.VorgangTestFactory; import de.ozgcloud.command.CommandTestFactory; +import io.grpc.stub.CallStreamObserver; import io.grpc.stub.StreamObserver; class CollaborationGrpcServiceTest { @@ -47,11 +59,16 @@ class CollaborationGrpcServiceTest { @Spy @InjectMocks private CollaborationGrpcService grpcService; + @Mock + private CollaborationService service; + @Mock + private CollaborationVorgangMapper vorgangMapper; @Mock private VorgangService vorgangService; + @Mock - private CollaborationVorgangMapper vorgangMapper; + private TaskExecutor taskExecutor; @Nested class TestFindVorgang { @@ -131,4 +148,93 @@ class CollaborationGrpcServiceTest { return grpcService.buildFindVorgangResponse(vorgang); } } + + @DisplayName("Get file content") + @Nested + class TestGetFileContent { + + @Mock + private StreamObserver<GrpcGetFileContentResponse> responseObserver; + @Mock + private GrpcDownloader<GrpcGetFileContentResponse> grpcDownloader; + + private final String fileId = UUID.randomUUID().toString(); + private final GrpcGetFileContentRequest request = GrpcGetFileContentRequest.newBuilder().setId(fileId).build(); + + @BeforeEach + void mock() { + doReturn(grpcDownloader).when(grpcService).buildDownloader(any(), any()); + } + + @Test + void shouldCreateDownloader() { + grpcService.getFileContent(request, responseObserver); + + verify(grpcService).buildDownloader(fileId, responseObserver); + } + + @Test + void shouldStartDownload() { + grpcService.getFileContent(request, responseObserver); + + verify(grpcDownloader).start(); + } + } + + @DisplayName("build downloader") + @Nested + class TestBuildDownloader { + + private final String fileId = UUID.randomUUID().toString(); + + @Mock + private CallStreamObserver<GrpcGetFileContentResponse> callStreamObserver; + + @Test + void shouldContainCallObserver() { + var downloader = grpcService.buildDownloader(fileId, callStreamObserver); + + assertThat(getCallObserver(downloader)).isEqualTo(callStreamObserver); + } + + @Test + void shouldContainDownloadConsumer() { + var downloader = grpcService.buildDownloader(fileId, callStreamObserver); + + assertThat(getDownloadConsumer(downloader)).isNotNull(); + } + + @Test + void shouldContainChunkBuilder() { + var downloader = grpcService.buildDownloader(fileId, callStreamObserver); + + assertThat(getChunkBuilder(downloader)).isNotNull(); + } + + @Test + void shouldContainTaskBuilder() { + var downloader = grpcService.buildDownloader(fileId, callStreamObserver); + + assertThat(getTaskExecuter(downloader)).isEqualTo(taskExecutor); + } + + @SuppressWarnings("unchecked") + private CallStreamObserver<GrpcGetFileContentResponse> getCallObserver(GrpcDownloader<GrpcGetFileContentResponse> downloader) { + return (CallStreamObserver<GrpcGetFileContentResponse>) ReflectionTestUtils.getField(downloader, "callObserver"); + } + + @SuppressWarnings("unchecked") + private Consumer<OutputStream> getDownloadConsumer(GrpcDownloader<GrpcGetFileContentResponse> downloader) { + return (Consumer<OutputStream>) ReflectionTestUtils.getField(downloader, "downloadConsumer"); + } + + @SuppressWarnings("unchecked") + private Function<ByteString, GrpcGetFileContentResponse> getChunkBuilder(GrpcDownloader<GrpcGetFileContentResponse> downloader) { + return (Function<ByteString, GrpcGetFileContentResponse>) ReflectionTestUtils.getField(downloader, "chunkBuilder"); + } + + private TaskExecutor getTaskExecuter(GrpcDownloader<GrpcGetFileContentResponse> downloader) { + return (TaskExecutor) ReflectionTestUtils.getField(downloader, "taskExecutor"); + } + } } \ No newline at end of file diff --git a/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationServiceITCase.java b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationServiceITCase.java index 79b329d35e24adf9632866c45f9f0276f04f8f91..f76bc81cecb3e09161f1713ea87bb52e9bea2171 100644 --- a/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationServiceITCase.java +++ b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationServiceITCase.java @@ -33,7 +33,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import de.ozgcloud.apilib.file.OzgCloudFileService; import de.ozgcloud.common.test.ITCase; @ITCase @@ -42,6 +44,9 @@ class CollaborationServiceITCase { @Autowired private CollaborationService service; + @MockBean + private OzgCloudFileService fileService; + @Nested class TestCreateCollaborationRequest { diff --git a/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationServiceTest.java b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationServiceTest.java index 964039613ad6589b6cc9977d516b34a6e1984c9c..68b30f7562caebea5f60a864dc746a1c88d2ee11 100644 --- a/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationServiceTest.java +++ b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/CollaborationServiceTest.java @@ -27,7 +27,11 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.io.OutputStream; +import java.util.UUID; + import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -37,6 +41,8 @@ import org.mockito.Spy; import de.ozgcloud.apilib.common.command.OzgCloudCommand; import de.ozgcloud.apilib.common.command.OzgCloudCommandService; import de.ozgcloud.apilib.common.command.OzgCloudCreateSubCommandsRequest; +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.apilib.file.OzgCloudFileService; import de.ozgcloud.collaboration.common.user.UserId; import de.ozgcloud.collaboration.common.user.UserProfile; import de.ozgcloud.collaboration.common.user.UserProfileService; @@ -57,12 +63,14 @@ class CollaborationServiceTest { private UserProfileService userProfileService; @Mock private CollaborationManagerCollaborationRequestMapper collaborationRequestMapper; + @Mock + private OzgCloudFileService fileService; @Nested class TestCreateCollaborationRequest { - private static final CollaborationRequest COLLABORATION_REQUEST = - CollaborationRequestTestFactory.createBuilder().collaborationVorgangId(null).createdBy(null).build(); + private static final CollaborationRequest COLLABORATION_REQUEST = CollaborationRequestTestFactory.createBuilder().collaborationVorgangId(null) + .createdBy(null).build(); private static final CollaborationRequest ENRICHED_COLLABORATION_REQUEST = CollaborationRequestTestFactory.create(); @Mock @@ -175,8 +183,8 @@ class CollaborationServiceTest { @Nested class TestBuildCreateCollaborationRequestRequest { - private static final CollaborationRequest COLLABORATION_REQUEST = - CollaborationRequestTestFactory.createBuilder().collaborationVorgangId(null).createdBy(null).build(); + private static final CollaborationRequest COLLABORATION_REQUEST = CollaborationRequestTestFactory.createBuilder().collaborationVorgangId(null) + .createdBy(null).build(); @Mock private OzgCloudCommand subCommand; @@ -225,4 +233,21 @@ class CollaborationServiceTest { return service.buildCreateCollaborationRequestRequest(COLLABORATION_REQUEST); } } + + @DisplayName("Write file content") + @Nested + class TestWriteFileContent { + + @Mock + private OutputStream stream; + + private final OzgCloudFileId fileId = OzgCloudFileId.from(UUID.randomUUID().toString()); + + @Test + void shouldCallFileService() { + service.writeFileContent(fileId, stream); + + verify(fileService).writeFileDataToStream(fileId, stream); + } + } } \ No newline at end of file diff --git a/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/common/grpc/GrpcDownloaderTest.java b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/common/grpc/GrpcDownloaderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5a20435f5a258fb348bcc0c11a5abf3b9a5e5372 --- /dev/null +++ b/collaboration-manager-server/src/test/java/de/ozgcloud/collaboration/common/grpc/GrpcDownloaderTest.java @@ -0,0 +1,335 @@ +package de.ozgcloud.collaboration.common.grpc; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.springframework.core.task.TaskExecutor; +import org.springframework.test.util.ReflectionTestUtils; + +import com.google.protobuf.ByteString; + +import de.ozgcloud.apilib.file.OzgCloudFileId; +import de.ozgcloud.collaboration.vorgang.FileTestFactory; +import de.ozgcloud.common.errorhandling.TechnicalException; +import io.grpc.stub.ClientCallStreamObserver; +import lombok.SneakyThrows; + +class GrpcDownloaderTest { + + @SuppressWarnings("unchecked") + private ClientCallStreamObserver<GrpcResponseDummy> callObserver = Mockito.mock(ClientCallStreamObserver.class); + @SuppressWarnings("unchecked") + private Function<ByteString, GrpcResponseDummy> chunkBuilder = Mockito.mock(Function.class); + @SuppressWarnings("unchecked") + private Consumer<OutputStream> downloadConsumer = Mockito.mock(Consumer.class); + private TaskExecutor taskExecutor = mock(TaskExecutor.class); + + private OzgCloudFileId fileId = FileTestFactory.ID; + + @Spy + private GrpcDownloader<GrpcResponseDummy> downloader = GrpcDownloader.<GrpcResponseDummy>builder() + .callObserver(callObserver) + .fileId(fileId) + .downloadConsumer(downloadConsumer) + .chunkBuilder(chunkBuilder) + .taskExecutor(taskExecutor) + .build(); + + @DisplayName("Start") + @Nested + class TestStart { + + @SneakyThrows + @Test + void shouldCallDoStart() { + doNothing().when(downloader).doStart(); + + downloader.start(); + + verify(downloader).doStart(); + } + + @SneakyThrows + @Test + void shouldCallHandleSafety() { + doNothing().when(downloader).doStart(); + + downloader.start(); + + verify(downloader).handleSafety(any(ExceptionRunnable.class)); + } + + @DisplayName("do") + @Nested + class TestDoStart { + + @Captor + private ArgumentCaptor<Runnable> runnableCaptor; + + @SneakyThrows + @Test + void shouldSetupStreams() { + downloader.doStart(); + + verify(downloader).setupStreams(); + } + + @SneakyThrows + @Test + void shouldCallTaskExecutor() { + downloader.doStart(); + + verify(taskExecutor).execute(any()); + } + + @SneakyThrows + @Test + void shouldStartDownload() { + doNothing().when(downloader).startDownload(); + + downloader.doStart(); + + verify(taskExecutor).execute(runnableCaptor.capture()); + runnableCaptor.getValue().run(); + verify(downloader).startDownload(); + } + + @SneakyThrows + @Test + void shouldSetOnReadyHandler() { + downloader.doStart(); + + verify(callObserver).setOnReadyHandler(runnableCaptor.capture()); + assertThat(runnableCaptor.getValue()).isNotNull(); + } + } + } + + @DisplayName("Start download") + @Nested + class TestStartDownload { + + @SneakyThrows + @Test + void shouldCallHandleSafety() { + doNothing().when(downloader).doDownload(); + + downloader.startDownload(); + + verify(downloader).handleSafety(any(ExceptionRunnable.class)); + } + + @SneakyThrows + @Test + void shouldCallDoDownoad() { + doNothing().when(downloader).doDownload(); + + downloader.startDownload(); + + verify(downloader).doDownload(); + } + + @DisplayName("do") + @Nested + class TestDoDownload { + + @Mock + private PipedOutputStream outputStream; + + @BeforeEach + void mock() { + ReflectionTestUtils.setField(downloader, "outputStream", outputStream); + } + + @SneakyThrows + @Test + void shouldCallDownloadConsumer() { + downloader.doDownload(); + + verify(downloadConsumer).accept(outputStream); + } + + @SneakyThrows + @Test + void shouldCloseOutputstream() { + downloader.doDownload(); + + verify(outputStream).close(); + } + } + } + + @DisplayName("On ready handler") + @Nested + class TestOnReadyHandler { + + @Test + void shouldSendChunksIfCallObserverIsReady() { + when(callObserver.isReady()).thenReturn(true); + doNothing().when(downloader).sendChunks(); + + downloader.onReadyHandler(); + + verify(downloader).sendChunks(); + } + } + + @DisplayName("Send chunks") + @Nested + class TestSendChunks { + + @SneakyThrows + @Test + void shouldCallHandleSafety() { + doNothing().when(downloader).doSendChunks(); + + downloader.sendChunks(); + + verify(downloader).handleSafety(any(ExceptionRunnable.class)); + } + + @SneakyThrows + @Test + void shouldCallDoDownoad() { + doNothing().when(downloader).doSendChunks(); + + downloader.sendChunks(); + + verify(downloader).doSendChunks(); + } + + @DisplayName("do") + @Nested + class TestDoSendChunks { + + @Mock + private PipedInputStream inputStream; + + private final int data = 20; + + @BeforeEach + void mock() { + ReflectionTestUtils.setField(downloader, "inputStream", inputStream); + } + + @SneakyThrows + @DisplayName("should send next chunk if callObserver is ready and stream already received data") + @Test + void shouldCallOnNext() { + when(callObserver.isReady()).thenReturn(true); + when(inputStream.read(any())).thenReturn(data, -1); + + downloader.doSendChunks(); + + verify(callObserver).onNext(any()); + } + + @SneakyThrows + @DisplayName("should call complete grpc stream if download has finished and stream has no data left") + @Test + void shouldCallOnCompleted() { + ReflectionTestUtils.setField(downloader, "downloadInProgress", new AtomicBoolean(false)); + + downloader.doSendChunks(); + + verify(callObserver).onCompleted(); + } + + @SneakyThrows + @Test + void shouldCloseInputStream() { + ReflectionTestUtils.setField(downloader, "downloadInProgress", new AtomicBoolean(false)); + + downloader.doSendChunks(); + + verify(inputStream).close(); + } + } + } + + @DisplayName("Handle safety") + @Nested + class TestHandleSafety { + + @DisplayName("on exception") + @Nested + class TestOnException { + + @Mock + private PipedOutputStream outputStream; + @Mock + private PipedInputStream inputStream; + + private final IOException exception = new IOException(); + + @SneakyThrows + @BeforeEach + void mock() { + ReflectionTestUtils.setField(downloader, "inputStream", inputStream); + ReflectionTestUtils.setField(downloader, "outputStream", outputStream); + } + + @SneakyThrows + @Test + void shouldThrowTechnicalException() { + assertThatThrownBy(this::handleSafety) + .isInstanceOf(TechnicalException.class) + .extracting(exception -> exception.getCause()).isEqualTo(exception); + } + + @SneakyThrows + @Test + void shouldCloseOutputStream() { + try { + handleSafety(); + } catch (Exception e) { + // do nothing + } + + verify(outputStream).close(); + } + + @SneakyThrows + @Test + void shouldCloseInputStream() { + try { + handleSafety(); + } catch (Exception e) { + // do nothing + } + + verify(inputStream).close(); + } + + private void handleSafety() { + downloader.handleSafety(this::dummyMethodThrowingException); + } + + private void dummyMethodThrowingException() throws IOException { + throw exception; + } + } + + } + + class GrpcResponseDummy { + } +} \ No newline at end of file