diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/DocumentManagerConfiguration.java b/document-manager-server/src/main/java/de/ozgcloud/document/DocumentManagerConfiguration.java index 8470d4db36c833c248901b269f1fd1e18a86c40f..3dfcad69044e784b0cb7accb5e8f19c8125d95ec 100644 --- a/document-manager-server/src/main/java/de/ozgcloud/document/DocumentManagerConfiguration.java +++ b/document-manager-server/src/main/java/de/ozgcloud/document/DocumentManagerConfiguration.java @@ -40,8 +40,9 @@ import net.devh.boot.grpc.client.inject.GrpcClient; @Configuration public class DocumentManagerConfiguration { - private static final String GRPC_USER_MANAGER_NAME = "user-manager"; - private static final String GRPC_COMMAND_MANAGER_NAME = "command-manager"; + public static final String GRPC_USER_MANAGER_NAME = "user-manager"; + public static final String GRPC_COMMAND_MANAGER_NAME = "command-manager"; + public static final String GRPC_VORGANG_MANAGER_NAME = "vorgang-manager"; public static final String COMMAND_SERVICE_NAME = "document_OzgCloudCommandService"; public static final String USER_PROFILE_SERVICE_NAME = "document_OzgCloudUserProfileService"; diff --git a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/binaryfile/BinaryFileRemoteService.java b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/binaryfile/BinaryFileRemoteService.java index 4ef6b761f7de2e7751311d815fddb04ae1f3c12c..12c75d382c407e5445fdad47149ea64354ae438f 100644 --- a/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/binaryfile/BinaryFileRemoteService.java +++ b/document-manager-server/src/main/java/de/ozgcloud/document/bescheid/binaryfile/BinaryFileRemoteService.java @@ -15,18 +15,17 @@ import org.springframework.stereotype.Service; import com.google.protobuf.ByteString; -import de.ozgcloud.document.DocumentManagerConfiguration; -import de.ozgcloud.document.bescheid.BescheidCallContextAttachingInterceptor; -import de.ozgcloud.document.bescheid.BescheidResponse; import de.ozgcloud.common.binaryfile.FileId; import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils.FileSender; import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.document.DocumentManagerConfiguration; +import de.ozgcloud.document.bescheid.BescheidCallContextAttachingInterceptor; +import de.ozgcloud.document.bescheid.BescheidResponse; import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc.BinaryFileServiceStub; import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileMetaData; import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileRequest; import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileResponse; -import de.ozgcloud.vorgang.grpc.command.GrpcCallContext; import io.grpc.stub.CallStreamObserver; import io.grpc.stub.StreamObserver; import lombok.NonNull; @@ -37,10 +36,9 @@ import net.devh.boot.grpc.client.inject.GrpcClient; @RequiredArgsConstructor class BinaryFileRemoteService { - private static final String CALL_CONTEXT_CLIENT = "bescheid-manager"; - private static final String VORGANG_ATTACHMENT_FIELD = "bescheid"; + static final String VORGANG_ATTACHMENT_FIELD = "bescheid"; - @GrpcClient("vorgang-manager") + @GrpcClient(DocumentManagerConfiguration.GRPC_VORGANG_MANAGER_NAME) private final BinaryFileServiceStub binaryFileRemoteStub; @Qualifier(DocumentManagerConfiguration.CALL_CONTEXT_CLIENT_INTERCEPTOR_NAME) private final BescheidCallContextAttachingInterceptor callContextInterceptor; @@ -58,7 +56,7 @@ class BinaryFileRemoteService { } } - private InputStream openFile(File file) { + InputStream openFile(File file) { try { return new FileInputStream(file); } catch (FileNotFoundException e) { @@ -66,11 +64,9 @@ class BinaryFileRemoteService { } } - private GrpcUploadBinaryFileRequest buildMetaDataRequest(BescheidResponse bescheid) { + GrpcUploadBinaryFileRequest buildMetaDataRequest(BescheidResponse bescheid) { return GrpcUploadBinaryFileRequest.newBuilder() .setMetadata(GrpcUploadBinaryFileMetaData.newBuilder() - // TODO remove context - check why needed! - .setContext(GrpcCallContext.newBuilder().setClient(CALL_CONTEXT_CLIENT).build()) .setVorgangId(bescheid.getVorgangId().toString()) .setField(VORGANG_ATTACHMENT_FIELD) .setContentType(bescheid.getContentType()) @@ -80,11 +76,11 @@ class BinaryFileRemoteService { .build(); } - private GrpcUploadBinaryFileRequest buildChunkRequest(byte[] bytes, Integer length) { + GrpcUploadBinaryFileRequest buildChunkRequest(byte[] bytes, Integer length) { return GrpcUploadBinaryFileRequest.newBuilder().setFileContent((ByteString.copyFrom(bytes, 0, length))).build(); } - private CallStreamObserver<GrpcUploadBinaryFileRequest> buildCallStreamObserver( + CallStreamObserver<GrpcUploadBinaryFileRequest> buildCallStreamObserver( StreamObserver<GrpcUploadBinaryFileResponse> responseObserver) { return (CallStreamObserver<GrpcUploadBinaryFileRequest>) binaryFileRemoteStub.withInterceptors(callContextInterceptor) .uploadBinaryFileAsStream(responseObserver); diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/binaryfile/BinaryFileRemoteServiceTest.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/binaryfile/BinaryFileRemoteServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b1d5b0cf4bf9f9a92d8b451a9640baaec3131732 --- /dev/null +++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/binaryfile/BinaryFileRemoteServiceTest.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.document.bescheid.binaryfile; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.InputStream; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; +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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +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.FileId; +import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.document.bescheid.BescheidCallContextAttachingInterceptor; +import de.ozgcloud.document.bescheid.BescheidResponse; +import de.ozgcloud.document.bescheid.BescheidResponseTestFactory; +import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc.BinaryFileServiceStub; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileRequest; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileResponse; +import io.grpc.stub.CallStreamObserver; +import io.grpc.stub.StreamObserver; +import lombok.SneakyThrows; + +class BinaryFileRemoteServiceTest { + + @Spy + @InjectMocks + private BinaryFileRemoteService remoteService; + + @Mock + private BinaryFileServiceStub binaryFileServiceStub; + @Mock + private BescheidCallContextAttachingInterceptor callContextInterceptor; + @Mock + private StreamObserver<GrpcUploadBinaryFileResponse> responseObserver; + @Mock + private GrpcUploadBinaryFileResponse grpcUploadResponse; + @Mock + private GrpcFileUploadUtils.FileSender<GrpcUploadBinaryFileRequest, GrpcUploadBinaryFileResponse> initialisedFileSender; + @Mock + private InputStream inputStream; + + @Nested + class TestUploadBescheidFile { + + private static final BescheidResponse BESCHEID_RESPONSE = BescheidResponseTestFactory.create(); + private static final FileId FILE_ID = FileId.createNew(); + + @Mock + private GrpcFileUploadUtils.FileSender<GrpcUploadBinaryFileRequest, GrpcUploadBinaryFileResponse> createdFileSender; + @Mock + private GrpcFileUploadUtils.FileSender<GrpcUploadBinaryFileRequest, GrpcUploadBinaryFileResponse> fileSenderWithMetadata; + @Mock + private CallStreamObserver<GrpcUploadBinaryFileRequest> requestObserver; + @Mock + private GrpcUploadBinaryFileRequest grpcUploadRequest; + + @Captor + private ArgumentCaptor<BiFunction<byte[], Integer, GrpcUploadBinaryFileRequest>> buildChunkRequestCaptor; + @Captor + private ArgumentCaptor<Function<StreamObserver<GrpcUploadBinaryFileResponse>, CallStreamObserver<GrpcUploadBinaryFileRequest>>> buildCallStreamObserverCaptor; + + private MockedStatic<GrpcFileUploadUtils> uploadUtilsMock; + + @BeforeEach + void init() { + doReturn(inputStream).when(remoteService).openFile(any()); + doReturn(grpcUploadResponse).when(remoteService).waitUntilFutureToComplete(any(), any()); + when(grpcUploadResponse.getFileId()).thenReturn(FILE_ID.toString()); + + uploadUtilsMock = mockStatic(GrpcFileUploadUtils.class); + when(createdFileSender.withMetaData(any())).thenReturn(fileSenderWithMetadata); + when(fileSenderWithMetadata.send()).thenReturn(initialisedFileSender); + uploadUtilsMock.when(() -> GrpcFileUploadUtils.createSender(any(), any(), any())).thenReturn(createdFileSender); + } + + @AfterEach + void cleanup() { + uploadUtilsMock.close(); + } + + @Test + void shouldCallOpenFile() { + uploadBescheidFile(); + + verify(remoteService).openFile(BescheidResponseTestFactory.BESCHEID_FILE); + } + + @Test + void shouldCallCreateSender() { + uploadBescheidFile(); + + uploadUtilsMock.verify(() -> GrpcFileUploadUtils.createSender(buildChunkRequestCaptor.capture(), eq(inputStream), + buildCallStreamObserverCaptor.capture())); + verifyCallBuildChunkRequest(); + verifyCallBuildCallStreamObserver(); + } + + @Test + void shouldCallBuildMetaDataRequest() { + uploadBescheidFile(); + + verify(remoteService).buildMetaDataRequest(BESCHEID_RESPONSE); + } + + @Test + void shouldCallWithMetaData() { + doReturn(grpcUploadRequest).when(remoteService).buildMetaDataRequest(any()); + + uploadBescheidFile(); + + verify(createdFileSender).withMetaData(grpcUploadRequest); + } + + @Test + void shouldCallSend() { + uploadBescheidFile(); + + verify(fileSenderWithMetadata).send(); + } + + @Test + void shouldCallWaitUntilFutureToComplete() { + uploadBescheidFile(); + + verify(remoteService).waitUntilFutureToComplete(initialisedFileSender, inputStream); + } + + @Test + void shouldReturnFileId() { + var result = uploadBescheidFile(); + + assertThat(result).isEqualTo(FILE_ID); + } + + private FileId uploadBescheidFile() { + return remoteService.uploadBescheidFile(BESCHEID_RESPONSE); + } + + private void verifyCallBuildChunkRequest() { + doReturn(grpcUploadRequest).when(remoteService).buildChunkRequest(any(), any()); + var bytes = new byte[1]; + var length = 1; + + buildChunkRequestCaptor.getValue().apply(bytes, length); + verify(remoteService).buildChunkRequest(bytes, length); + } + + private void verifyCallBuildCallStreamObserver() { + doReturn(requestObserver).when(remoteService).buildCallStreamObserver(any()); + + buildCallStreamObserverCaptor.getValue().apply(responseObserver); + verify(remoteService).buildCallStreamObserver(responseObserver); + } + } + + @Nested + class TestOpenFile { + + @Test + void shouldReturnInputStream() { + var result = remoteService.openFile(BescheidResponseTestFactory.BESCHEID_FILE); + + assertThat(result).hasBinaryContent(BescheidResponseTestFactory.BESCHEID_FILE_NAME.getBytes()); + } + } + + @Nested + class TestBuildMetaDataRequest { + + @Test + void shouldReturnGrpcUploadBinaryFileRequest() { + var requestMetadata = GrpcUploadBinaryFileMetaDataTestFactory.createBuilder().setField(BinaryFileRemoteService.VORGANG_ATTACHMENT_FIELD) + .build(); + + var result = remoteService.buildMetaDataRequest(BescheidResponseTestFactory.create()); + + assertThat(result.getMetadata()).usingRecursiveComparison().isEqualTo(requestMetadata); + assertThat(result.getFileContent()).isEmpty(); + } + } + + @Nested + class TestBuildChunkRequest { + + @Test + void shouldSetFileContent() { + var bytes = BescheidResponseTestFactory.BESCHEID_FILE_NAME.getBytes(); + var length = BescheidResponseTestFactory.BESCHEID_FILE_NAME.length(); + + var result = remoteService.buildChunkRequest(bytes, length); + + assertThat(result.getFileContent().toStringUtf8()).isEqualTo(BescheidResponseTestFactory.BESCHEID_FILE_NAME); + } + } + + @Nested + class TestBuildCallStreamObserver { + + @Mock + private BinaryFileServiceStub binaryFileServiceStubWithInterceptor; + + @BeforeEach + void init() { + when(binaryFileServiceStub.withInterceptors(callContextInterceptor)).thenReturn(binaryFileServiceStubWithInterceptor); + } + + @Test + void shouldSetInterceptor() { + buildCallStreamObserver(); + + verify(binaryFileServiceStub).withInterceptors(callContextInterceptor); + } + + @Test + void shouldCallUploadBinaryFileAsStream() { + buildCallStreamObserver(); + + verify(binaryFileServiceStubWithInterceptor).uploadBinaryFileAsStream(responseObserver); + } + + private CallStreamObserver<GrpcUploadBinaryFileRequest> buildCallStreamObserver() { + return remoteService.buildCallStreamObserver(responseObserver); + } + } + + @Nested + class TestWaitUntilFutureToComplete { + + @Mock + private GrpcUploadBinaryFileResponse grpcUploadResponse; + @Mock + private CompletableFuture<GrpcUploadBinaryFileResponse> responseFuture; + + @BeforeEach + void init() { + when(initialisedFileSender.getResultFuture()).thenReturn(responseFuture); + } + + @Nested + class TestOnSuccess { + + @SneakyThrows + @BeforeEach + void init() { + when(responseFuture.get(anyLong(), any())).thenReturn(grpcUploadResponse); + } + + @Test + void shouldReturnResponse() { + var result = waitUntilFutureToComplete(); + + assertThat(result).isSameAs(grpcUploadResponse); + } + + @Test + void shouldCloseStream() { + try (var ioUtilsMock = mockStatic(IOUtils.class)) { + waitUntilFutureToComplete(); + + ioUtilsMock.verify(() -> IOUtils.closeQuietly(inputStream)); + } + } + } + + @Nested + class TestOnInterruptedException { + + @Mock + private InterruptedException exception; + + @BeforeEach + void init() throws ExecutionException, InterruptedException, TimeoutException { + when(responseFuture.get(anyLong(), any())).thenThrow(exception); + } + + @Test + void shouldCallCancelOnError() { + assertThrows(TechnicalException.class, TestWaitUntilFutureToComplete.this::waitUntilFutureToComplete); + + verify(initialisedFileSender).cancelOnError(exception); + } + + @Test + void shouldCloseStream() { + try (var ioUtilsMock = mockStatic(IOUtils.class)) { + assertThrows(TechnicalException.class, TestWaitUntilFutureToComplete.this::waitUntilFutureToComplete); + + ioUtilsMock.verify(() -> IOUtils.closeQuietly(inputStream)); + } + } + } + + @Nested + class TestOnException { + + @SneakyThrows + @DisplayName("should cancel on timeout") + @ParameterizedTest(name = "when {0} is thrown") + @ValueSource(classes = { ExecutionException.class, TimeoutException.class }) + void shouldCancelOnTimeoutOnTimeoutException(Class<Exception> exceptionClass) { + when(responseFuture.get(anyLong(), any())).thenThrow(exceptionClass); + + assertThrows(TechnicalException.class, TestWaitUntilFutureToComplete.this::waitUntilFutureToComplete); + + verify(initialisedFileSender).cancelOnTimeout(); + } + + @SneakyThrows + @DisplayName("should close stream") + @ParameterizedTest(name = "when {0} is thrown") + @ValueSource(classes = { ExecutionException.class, TimeoutException.class }) + void shouldCloseStream(Class<Exception> exceptionClass) { + try (var ioUtilsMock = mockStatic(IOUtils.class)) { + when(responseFuture.get(anyLong(), any())).thenThrow(exceptionClass); + + assertThrows(TechnicalException.class, TestWaitUntilFutureToComplete.this::waitUntilFutureToComplete); + + ioUtilsMock.verify(() -> IOUtils.closeQuietly(inputStream)); + } + } + } + + private GrpcUploadBinaryFileResponse waitUntilFutureToComplete() { + return remoteService.waitUntilFutureToComplete(initialisedFileSender, inputStream); + } + } + +} \ No newline at end of file diff --git a/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/binaryfile/GrpcUploadBinaryFileMetaDataTestFactory.java b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/binaryfile/GrpcUploadBinaryFileMetaDataTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..9680d544810c33865349dad68392fe665d2bd7ed --- /dev/null +++ b/document-manager-server/src/test/java/de/ozgcloud/document/bescheid/binaryfile/GrpcUploadBinaryFileMetaDataTestFactory.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.document.bescheid.binaryfile; + +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.document.bescheid.BescheidResponseTestFactory; +import de.ozgcloud.document.bescheid.vorgang.VorgangTestFactory; +import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileMetaData; + +public class GrpcUploadBinaryFileMetaDataTestFactory { + + public static final String VORGANG_ID = VorgangTestFactory.ID_STR; + public static final String FIELD = LoremIpsum.getInstance().getWords(1); + public static final String CONTENT_TYPE = BescheidResponseTestFactory.CONTENT_TYPE; + public static final int SIZE = BescheidResponseTestFactory.BESCHEID_FILE_SIZE; + public static final String FILE_NAME = BescheidResponseTestFactory.BESCHEID_FILE_NAME; + + public static GrpcUploadBinaryFileMetaData create() { + return createBuilder().build(); + } + + public static GrpcUploadBinaryFileMetaData.Builder createBuilder() { + return GrpcUploadBinaryFileMetaData.newBuilder() + .setVorgangId(VORGANG_ID) + .setField(FIELD) + .setContentType(CONTENT_TYPE) + .setSize(SIZE) + .setFileName(FILE_NAME); + } +}