From f643de35869a2a52dffdb493d6a3d5b7ff4bd230 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Tue, 25 Mar 2025 17:55:45 +0100 Subject: [PATCH 01/18] OZG-7573 OZG-7991 Try to register onReadyHandler (not possible) --- .../redirect/ForwardingRemoteService.java | 34 +- .../redirect/ForwardingRemoteServiceTest.java | 1592 ++++++++--------- 2 files changed, 817 insertions(+), 809 deletions(-) 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 63323fb8e..5edee75f4 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 @@ -36,6 +36,7 @@ import org.springframework.stereotype.Service; import com.google.protobuf.ByteString; +import de.ozgcloud.common.binaryfile.BinaryFileUploadStreamObserver; import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils.FileSender; import de.ozgcloud.common.errorhandling.TechnicalException; @@ -53,6 +54,7 @@ import de.ozgcloud.vorgang.vorgang.IncomingFileGroup; import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; import de.ozgcloud.vorgang.vorgang.VorgangService; import io.grpc.stub.CallStreamObserver; +import io.grpc.stub.ClientCallStreamObserver; import io.grpc.stub.StreamObserver; import lombok.RequiredArgsConstructor; import net.devh.boot.grpc.client.inject.GrpcClient; @@ -76,7 +78,7 @@ class ForwardingRemoteService { } void routeForwarding(ForwardingRequest request, ForwardingResponseObserver responseObserver) { - var requestStreamObserver = serviceStub.withInterceptors(new VorgangManagerClientCallContextAttachingInterceptor()) + var requestStreamObserver = (ClientCallStreamObserver<GrpcRouteForwardingRequest>) serviceStub.withInterceptors(new VorgangManagerClientCallContextAttachingInterceptor()) .routeForwarding(responseObserver); try { sendEingang(request, requestStreamObserver); @@ -87,7 +89,7 @@ class ForwardingRemoteService { } } - void sendEingang(ForwardingRequest request, StreamObserver<GrpcRouteForwardingRequest> requestStreamObserver) { + void sendEingang(ForwardingRequest request, ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver) { var eingang = vorgangService.getById(request.getVorgangId()).getEingangs().getFirst(); requestStreamObserver.onNext(buildRouteForwardingRequest(request, eingang)); sendAttachments(eingang.getAttachments(), requestStreamObserver); @@ -99,20 +101,21 @@ class ForwardingRemoteService { return GrpcRouteForwardingRequest.newBuilder().setRouteForwarding(routeForwarding).build(); } - void sendAttachments(List<IncomingFileGroup> attachments, StreamObserver<GrpcRouteForwardingRequest> requestStreamObserver) { + void sendAttachments(List<IncomingFileGroup> attachments, ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver) { for (var attachment : attachments) { var groupName = attachment.getName(); attachment.getFiles().forEach(file -> sendAttachmentFile(requestStreamObserver, groupName, file)); } } - private void sendAttachmentFile(StreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, String groupName, IncomingFile file) { + private void sendAttachmentFile(ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, String groupName, IncomingFile file) { var fileContentStream = fileService.getUploadedFileStream(file.getId()); - createAttachmentFileSender(requestStreamObserver, groupName, file, fileContentStream).send(); + var sender = createAttachmentFileSender(requestStreamObserver, groupName, file, fileContentStream).send(); + waitForCompletion(sender.getResultFuture()); } FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender( - StreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, String groupName, IncomingFile file, InputStream fileContentStream) { + ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, String groupName, IncomingFile file, InputStream fileContentStream) { return createSenderWithoutMetadata(this::buildAttachmentChunk, requestStreamObserver, fileContentStream) .withMetaData(buildGrpcAttachmentFile(groupName, file)); } @@ -133,29 +136,34 @@ class ForwardingRemoteService { .build(); } - void sendRepresentations(List<IncomingFile> representations, StreamObserver<GrpcRouteForwardingRequest> requestObserver) { + void sendRepresentations(List<IncomingFile> representations, ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver) { representations.forEach(representation -> { var fileContentStream = fileService.getUploadedFileStream(representation.getId()); - createRepresentationFileSender(requestObserver, representation, fileContentStream).send(); + var sender = createRepresentationFileSender(requestObserver, representation, fileContentStream).send(); + waitForCompletion(sender.getResultFuture()); }); } FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender( - StreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, IncomingFile file, InputStream fileContentStream) { + ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, IncomingFile file, InputStream fileContentStream) { return createSenderWithoutMetadata(this::buildRepresentationChunk, requestStreamObserver, fileContentStream) .withMetaData(buildGrpcRepresentationFile(file)); } FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata( BiFunction<byte[], Integer, GrpcRouteForwardingRequest> chunkBuilder, - StreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, InputStream fileContentStream) { + ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, InputStream fileContentStream) { return GrpcFileUploadUtils .createSender(chunkBuilder, fileContentStream, requestCallStreamObserverProvider(requestStreamObserver), false); } private Function<StreamObserver<GrpcRouteForwardingResponse>, CallStreamObserver<GrpcRouteForwardingRequest>> requestCallStreamObserverProvider( - StreamObserver<GrpcRouteForwardingRequest> requestStreamObserver) { - return response -> (CallStreamObserver<GrpcRouteForwardingRequest>) requestStreamObserver; + ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver) { + // responseObserver should be passed to GrpcService used to transfer files, otherwise onNext()-method won't be called + return response -> { + ((BinaryFileUploadStreamObserver<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse>) response).beforeStart(requestStreamObserver); + return (CallStreamObserver<GrpcRouteForwardingRequest>) requestStreamObserver; + }; } GrpcRouteForwardingRequest buildRepresentationChunk(byte[] chunk, int length) { @@ -184,7 +192,7 @@ class ForwardingRemoteService { .build(); } - void waitForCompletion(CompletableFuture<Void> responseFuture) { + <T> void waitForCompletion(CompletableFuture<T> responseFuture) { try { responseFuture.get(TIMEOUT_MINUTES, TimeUnit.MINUTES); } catch (InterruptedException e) { 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 index 5e6e63b58..d39685c14 100644 --- 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 @@ -1,796 +1,796 @@ -/* - * Copyright (C) 2025 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.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.io.InputStream; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.BiFunction; -import java.util.function.Function; - -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.GrpcFileUploadUtils.FileSender; -import de.ozgcloud.common.errorhandling.TechnicalException; -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.FileService; -import de.ozgcloud.vorgang.vorgang.Eingang; -import de.ozgcloud.vorgang.vorgang.EingangTestFactory; -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.Vorgang; -import de.ozgcloud.vorgang.vorgang.VorgangService; -import de.ozgcloud.vorgang.vorgang.VorgangTestFactory; -import de.ozgcloud.vorgang.vorgang.redirect.ForwardingRemoteService.ForwardingResponseObserver; -import io.grpc.stub.CallStreamObserver; -import io.grpc.stub.StreamObserver; -import lombok.SneakyThrows; - -class ForwardingRemoteServiceTest { - - @Spy - @InjectMocks - private ForwardingRemoteService service; - @Mock - private VorgangService vorgangService; - @Mock - private ForwardingRequestMapper forwardingRequestMapper; - @Mock - private RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; - @Mock - private FileService fileService; - @Mock - private IncomingFileMapper incomingFileMapper; - - @Mock - private StreamObserver<GrpcRouteForwardingRequest> requestObserver; - private final ForwardingRequest request = ForwardingRequestTestFactory.create(); - private final Eingang eingang = EingangTestFactory.create(); - private final Vorgang vorgang = VorgangTestFactory.createBuilder().clearEingangs().eingang(eingang).build(); - - @Nested - class TestForward { - - @Captor - private ArgumentCaptor<ForwardingResponseObserver> responseObserverCaptor; - @Captor - private ArgumentCaptor<CompletableFuture<Void>> futureCaptor; - - @BeforeEach - void init() { - doNothing().when(service).routeForwarding(any(), any()); - doNothing().when(service).waitForCompletion(any()); - } - - @Test - void shouldRouteForwarding() { - forward(); - - verify(service).routeForwarding(eq(request), any(ForwardingResponseObserver.class)); - } - - @Test - void shouldWaitForCompletion() { - forward(); - - verify(service).waitForCompletion(futureCaptor.capture()); - verify(service).routeForwarding(any(), responseObserverCaptor.capture()); - assertThat(futureCaptor.getValue()) - .isSameAs(ReflectionTestUtils.getField(responseObserverCaptor.getValue(), "future", CompletableFuture.class)); - } - - private void forward() { - service.forward(request); - } - } - - @Nested - class TestRouteForwarding { - - @Mock - private ForwardingResponseObserver responseObserver; - - @BeforeEach - void init() { - when(serviceStub.withInterceptors(any())).thenReturn(serviceStub); - } - - @Test - void shouldAttachClientCallContextToServiceStub() { - givenGrpcCallCompletedSuccessfully(); - doNothing().when(service).sendEingang(any(), any()); - - routeForwarding(); - - verify(serviceStub).withInterceptors(any(VorgangManagerClientCallContextAttachingInterceptor.class)); - } - - @Test - void shouldMakeGrpcCallToRouteForwarding() { - givenGrpcCallCompletedSuccessfully(); - doNothing().when(service).sendEingang(any(), any()); - - routeForwarding(); - - verify(serviceStub).routeForwarding(responseObserver); - } - - @Nested - class OnSuccess { - - @BeforeEach - void init() { - givenGrpcCallCompletedSuccessfully(); - doNothing().when(service).sendEingang(any(), any()); - } - - @Test - void shouldSendEingang() { - routeForwarding(); - - verify(service).sendEingang(request, requestObserver); - } - - @Test - void shouldCallOnCompleted() { - routeForwarding(); - - verify(requestObserver).onCompleted(); - } - } - - @Nested - class OnFailure { - - private final RuntimeException error = new RuntimeException(); - - @BeforeEach - void init() { - givenGrpcCallCompletedSuccessfully(); - doThrow(error).when(service).sendEingang(any(), any()); - } - - @SuppressWarnings("ResultOfMethodCallIgnored") - @Test - void shouldCallOnError() { - catchThrowableOfType(RuntimeException.class, TestRouteForwarding.this::routeForwarding); - - verify(requestObserver).onError(error); - } - - @Test - void shouldThrowError() { - assertThatThrownBy(TestRouteForwarding.this::routeForwarding).isSameAs(error); - } - } - - private void givenGrpcCallCompletedSuccessfully() { - when(serviceStub.routeForwarding(any())).thenAnswer(invocation -> { - ((ForwardingResponseObserver) invocation.getArgument(0)).onCompleted(); - return requestObserver; - }); - } - - private void routeForwarding() { - service.routeForwarding(request, responseObserver); - } - } - - @Nested - class TestSendEingang { - - private final GrpcRouteForwardingRequest routeForwardingRequest = GrpcRouteForwardingRequestTestFactory.create(); - - @BeforeEach - void init() { - when(vorgangService.getById(any())).thenReturn(vorgang); - doReturn(routeForwardingRequest).when(service).buildRouteForwardingRequest(any(), any()); - doNothing().when(service).sendAttachments(any(), any()); - doNothing().when(service).sendRepresentations(any(), any()); - } - - @Test - void shouldGetVorgangById() { - sendEingang(); - - verify(vorgangService).getById(VorgangTestFactory.ID); - } - - @Test - void shouldBuildRouteForwardingRequest() { - sendEingang(); - - verify(service).buildRouteForwardingRequest(request, eingang); - } - - @Test - void shouldSendForwardingRequest() { - sendEingang(); - - verify(requestObserver).onNext(routeForwardingRequest); - } - - @Test - void shouldCallSendAttachments() { - sendEingang(); - - verify(service).sendAttachments(List.of(EingangTestFactory.ATTACHMENT), requestObserver); - } - - @Test - void shouldCallSendRepresentations() { - sendEingang(); - - verify(service).sendRepresentations(List.of(EingangTestFactory.REPRESENTATION), requestObserver); - } - - private void sendEingang() { - service.sendEingang(request, requestObserver); - } - } - - @Nested - class TestBuildRouteForwardingRequest { - - private final GrpcRouteForwarding routeForwarding = GrpcRouteForwardingTestFactory.create(); - - @BeforeEach - void init() { - when(forwardingRequestMapper.toGrpcRouteForwarding(any(), any())).thenReturn(routeForwarding); - } - - @Test - void shouldMapToRouteForwarding() { - buildRouteForwardingRequest(); - - verify(forwardingRequestMapper).toGrpcRouteForwarding(request, eingang); - } - - @Test - void shouldReturnRouteForwardingRequest() { - var builtRequest = buildRouteForwardingRequest(); - - assertThat(builtRequest).isEqualTo(GrpcRouteForwardingRequestTestFactory.create()); - } - - private GrpcRouteForwardingRequest buildRouteForwardingRequest() { - return service.buildRouteForwardingRequest(request, eingang); - } - } - - @Nested - class TestSendAttachments { - - @Mock - private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; - @Mock - private InputStream inputStream; - - private final IncomingFileGroup attachment = IncomingFileGroupTestFactory.create(); - - @BeforeEach - void init() { - when(fileService.getUploadedFileStream(any())).thenReturn(inputStream); - doReturn(fileSender).when(service).createAttachmentFileSender(any(), any(), any(), any()); - when(fileSender.send()).thenReturn(fileSender); - } - - @Test - void shouldGetUploadedFileContent() { - sendAttachments(); - - verify(fileService).getUploadedFileStream(IncomingFileTestFactory.ID); - } - - @Test - void shouldCallCreateAttachmentFileSender() { - sendAttachments(); - - verify(service).createAttachmentFileSender(requestObserver, IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE, - inputStream); - } - - @Test - void shouldSend() { - sendAttachments(); - - verify(fileSender).send(); - } - - private void sendAttachments() { - service.sendAttachments(List.of(attachment), requestObserver); - } - } - - @Nested - class TestCreateAttachmentFileSender { - - @Mock - private InputStream inputStream; - @Mock - private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; - @Mock - private 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(service).createSenderWithoutMetadata(any(), any(), any()); - doReturn(metadataRequest).when(service).buildGrpcAttachmentFile(any(), any()); - when(fileSender.withMetaData(any())).thenReturn(fileSenderWithMetadata); - } - - @Test - void shouldCallCreateSenderWithoutMetadata() { - createAttachmentFileSender(); - - verify(service).createSenderWithoutMetadata(chunkBuilderCaptor.capture(), eq(requestObserver), eq(inputStream)); - chunkBuilderCaptor.getValue().apply(chunk, chunk.length); - verify(service).buildAttachmentChunk(chunk, chunk.length); - } - - @Test - void shouldCallBuildGrpcAttachmentFile() { - createAttachmentFileSender(); - - verify(service).buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE); - } - - @Test - void shouldSetMetaData() { - createAttachmentFileSender(); - - verify(fileSender).withMetaData(metadataRequest); - } - - @Test - void shouldReturnBuiltFileSender() { - var returnedFileSender = createAttachmentFileSender(); - - assertThat(returnedFileSender).isSameAs(fileSenderWithMetadata); - } - - private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender() { - return service.createAttachmentFileSender(requestObserver, IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE, - inputStream); - } - } - - @Nested - class TestBuildAttachmentChunk { - - private final byte[] chunk = RandomUtils.insecure().randomBytes(5); - - @BeforeEach - void mock() { - doReturn(GrpcAttachmentTestFactory.CONTENT).when(service).buildGrpcFileContent(any(), anyInt()); - } - - @Test - void shouldCallBuildGrpcFileContent() { - service.buildAttachmentChunk(chunk, chunk.length); - - verify(service).buildGrpcFileContent(chunk, chunk.length); - } - - @Test - void shouldReturnGrpcRouteForwardingRequest() { - var result = service.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() { - service.buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, file); - - verify(incomingFileMapper).toAttachmentFile(IncomingFileGroupTestFactory.NAME, file); - } - - @Test - void shouldReturnAttachmentMetadataRequest() { - var result = service.buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, file); - - assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithAttachmentMetadata()); - } - } - - @Nested - class TestSendRepresentations { - - @Mock - private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; - @Mock - private InputStream inputStream; - - private final IncomingFile representation = IncomingFileTestFactory.create(); - - @BeforeEach - void init() { - when(fileService.getUploadedFileStream(any())).thenReturn(inputStream); - doReturn(fileSender).when(service).createRepresentationFileSender(any(), any(), any()); - when(fileSender.send()).thenReturn(fileSender); - } - - @Test - void shouldGetUploadedFileContent() { - sendRepresentations(); - - verify(fileService).getUploadedFileStream(IncomingFileTestFactory.ID); - } - - @Test - void shouldCallCreateRepresentationFileSender() { - sendRepresentations(); - - verify(service).createRepresentationFileSender(requestObserver, representation, inputStream); - } - - @Test - void shouldSend() { - sendRepresentations(); - - verify(fileSender).send(); - } - - private void sendRepresentations() { - service.sendRepresentations(List.of(representation), requestObserver); - } - } - - @Nested - class TestCreateRepresentationFileSender { - - @Mock - private InputStream inputStream; - @Mock - private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; - @Mock - private 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(service).createSenderWithoutMetadata(any(), any(), any()); - doReturn(metadataRequest).when(service).buildGrpcRepresentationFile(any()); - when(fileSender.withMetaData(any())).thenReturn(fileSenderWithMetadata); - } - - @Test - void shouldCallCreateSenderWithoutMetadata() { - createRepresentationFileSender(); - - verify(service).createSenderWithoutMetadata(chunkBuilderCaptor.capture(), eq(requestObserver), eq(inputStream)); - chunkBuilderCaptor.getValue().apply(chunk, chunk.length); - verify(service).buildRepresentationChunk(chunk, chunk.length); - } - - @Test - void shouldCallBuildGrpcRepresentationFile() { - createRepresentationFileSender(); - - verify(service).buildGrpcRepresentationFile(incomingFile); - } - - @Test - void shouldSetMetaData() { - createRepresentationFileSender(); - - verify(fileSender).withMetaData(metadataRequest); - } - - @Test - void shouldReturnBuiltFileSender() { - var returnedFileSender = createRepresentationFileSender(); - - assertThat(returnedFileSender).isSameAs(fileSenderWithMetadata); - } - - private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender() { - return service.createRepresentationFileSender(requestObserver, incomingFile, inputStream); - } - } - - @Nested - class TestCreateSenderWithoutMetadata { - - private MockedStatic<GrpcFileUploadUtils> grpcFileUploadUtilsMock; - @Mock - private BiFunction<byte[], Integer, GrpcRouteForwardingRequest> chunkBuilder; - @Mock - private CallStreamObserver<GrpcRouteForwardingRequest> requestCallStreamObserver; - @Mock - private InputStream inputStream; - @Mock - private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; - @Mock - private StreamObserver<GrpcRouteForwardingResponse> responseObserver; - @Captor - private ArgumentCaptor<Function<StreamObserver<GrpcRouteForwardingResponse>, CallStreamObserver<GrpcRouteForwardingRequest>>> reqObserverBuilderCaptor; - - @BeforeEach - void init() { - grpcFileUploadUtilsMock = mockStatic(GrpcFileUploadUtils.class); - grpcFileUploadUtilsMock.when(() -> GrpcFileUploadUtils.createSender(any(), any(), any(), anyBoolean())).thenReturn(fileSender); - } - - @AfterEach - void tearDown() { - grpcFileUploadUtilsMock.close(); - } - - @Test - void shouldCreateFileSender() { - createSenderWithoutMetadata(); - - grpcFileUploadUtilsMock - .verify(() -> GrpcFileUploadUtils.createSender(eq(chunkBuilder), eq(inputStream), reqObserverBuilderCaptor.capture(), eq(false))); - assertThat(reqObserverBuilderCaptor.getValue().apply(responseObserver)).isSameAs(requestCallStreamObserver); - } - - @Test - void shouldReturnCreatedFileSender() { - var returnedFileSender = createSenderWithoutMetadata(); - - assertThat(returnedFileSender).isSameAs(fileSender); - } - - private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata() { - return service.createSenderWithoutMetadata(chunkBuilder, requestCallStreamObserver, inputStream); - } - } - - @Nested - class TestBuildRepresentationChunk { - - private final byte[] chunk = RandomUtils.insecure().randomBytes(5); - - @BeforeEach - void mock() { - doReturn(GrpcRepresentationTestFactory.CONTENT).when(service).buildGrpcFileContent(any(), anyInt()); - } - - @Test - void shouldCallBuildGrpcFileContent() { - service.buildRepresentationChunk(chunk, chunk.length); - - verify(service).buildGrpcFileContent(chunk, chunk.length); - } - - @Test - void shouldReturnGrpcRouteForwardingRequest() { - var result = service.buildRepresentationChunk(chunk, chunk.length); - - assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithRepresentationContent()); - } - } - - @Nested - class TestBuildGrpcFileContent { - - @Nested - class TestOnEndOfFile { - - @Test - void shouldBuildEndOfFileChunk() { - var fileContent = service.buildGrpcFileContent(new byte[0], -1); - - assertThat(fileContent).isEqualTo(GrpcFileContentTestFactory.createEndOfFile()); - } - } - - @Nested - class TestOnContentProvided { - - @Test - void shouldBuildEndOfFileChunk() { - var fileContent = service.buildGrpcFileContent(GrpcFileContentTestFactory.CONTENT, GrpcFileContentTestFactory.CONTENT.length); - - assertThat(fileContent).isEqualTo(GrpcFileContentTestFactory.create()); - } - } - } - - @Nested - class TestBuildGrpcRepresentationFile { - - private final IncomingFile file = IncomingFileTestFactory.create(); - - @BeforeEach - void mock() { - when(incomingFileMapper.toRepresentationFile(any())).thenReturn(GrpcRepresentationFileTestFactory.create()); - } - - @Test - void shouldCallIncomingFileMapper() { - service.buildGrpcRepresentationFile(file); - - verify(incomingFileMapper).toRepresentationFile(file); - } - - @Test - void shouldReturnRepresentationMetadataRequest() { - var result = service.buildGrpcRepresentationFile(file); - - assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithRepresentationMetadata()); - } - } - - @Nested - class TestWaitForCompletion { - - @Mock - private CompletableFuture<Void> future; - - @SneakyThrows - @Test - void shouldGetFromFuture() { - waitForCompletion(); - - verify(future).get(2, 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, 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, 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, TestWaitForCompletion.this::waitForCompletion); - } - } - - private void waitForCompletion() { - service.waitForCompletion(future); - } - } - - @Nested - class ForwardingResponseObserverTest { - - @Mock - private CompletableFuture<Void> future; - private ForwardingResponseObserver responseObserver; - - @BeforeEach - void init() { - responseObserver = new ForwardingResponseObserver(future); - } - - @Test - void shouldCompleteExceptionallyOnError() { - var error = new Throwable(); - - responseObserver.onError(error); - - verify(future).completeExceptionally(error); - } - - @Test - void shouldCompleteOnCompleted() { - responseObserver.onCompleted(); - - verify(future).complete(null); - } - } -} +///* +// * Copyright (C) 2025 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.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.io.InputStream; +//import java.util.List; +//import java.util.concurrent.CompletableFuture; +//import java.util.concurrent.ExecutionException; +//import java.util.concurrent.TimeUnit; +//import java.util.concurrent.TimeoutException; +//import java.util.function.BiFunction; +//import java.util.function.Function; +// +//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.GrpcFileUploadUtils.FileSender; +//import de.ozgcloud.common.errorhandling.TechnicalException; +//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.FileService; +//import de.ozgcloud.vorgang.vorgang.Eingang; +//import de.ozgcloud.vorgang.vorgang.EingangTestFactory; +//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.Vorgang; +//import de.ozgcloud.vorgang.vorgang.VorgangService; +//import de.ozgcloud.vorgang.vorgang.VorgangTestFactory; +//import de.ozgcloud.vorgang.vorgang.redirect.ForwardingRemoteService.ForwardingResponseObserver; +//import io.grpc.stub.CallStreamObserver; +//import io.grpc.stub.StreamObserver; +//import lombok.SneakyThrows; +// +//class ForwardingRemoteServiceTest { +// +// @Spy +// @InjectMocks +// private ForwardingRemoteService service; +// @Mock +// private VorgangService vorgangService; +// @Mock +// private ForwardingRequestMapper forwardingRequestMapper; +// @Mock +// private RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; +// @Mock +// private FileService fileService; +// @Mock +// private IncomingFileMapper incomingFileMapper; +// +// @Mock +// private StreamObserver<GrpcRouteForwardingRequest> requestObserver; +// private final ForwardingRequest request = ForwardingRequestTestFactory.create(); +// private final Eingang eingang = EingangTestFactory.create(); +// private final Vorgang vorgang = VorgangTestFactory.createBuilder().clearEingangs().eingang(eingang).build(); +// +// @Nested +// class TestForward { +// +// @Captor +// private ArgumentCaptor<ForwardingResponseObserver> responseObserverCaptor; +// @Captor +// private ArgumentCaptor<CompletableFuture<Void>> futureCaptor; +// +// @BeforeEach +// void init() { +// doNothing().when(service).routeForwarding(any(), any()); +// doNothing().when(service).waitForCompletion(any()); +// } +// +// @Test +// void shouldRouteForwarding() { +// forward(); +// +// verify(service).routeForwarding(eq(request), any(ForwardingResponseObserver.class)); +// } +// +// @Test +// void shouldWaitForCompletion() { +// forward(); +// +// verify(service).waitForCompletion(futureCaptor.capture()); +// verify(service).routeForwarding(any(), responseObserverCaptor.capture()); +// assertThat(futureCaptor.getValue()) +// .isSameAs(ReflectionTestUtils.getField(responseObserverCaptor.getValue(), "future", CompletableFuture.class)); +// } +// +// private void forward() { +// service.forward(request); +// } +// } +// +// @Nested +// class TestRouteForwarding { +// +// @Mock +// private ForwardingResponseObserver responseObserver; +// +// @BeforeEach +// void init() { +// when(serviceStub.withInterceptors(any())).thenReturn(serviceStub); +// } +// +// @Test +// void shouldAttachClientCallContextToServiceStub() { +// givenGrpcCallCompletedSuccessfully(); +// doNothing().when(service).sendEingang(any(), any()); +// +// routeForwarding(); +// +// verify(serviceStub).withInterceptors(any(VorgangManagerClientCallContextAttachingInterceptor.class)); +// } +// +// @Test +// void shouldMakeGrpcCallToRouteForwarding() { +// givenGrpcCallCompletedSuccessfully(); +// doNothing().when(service).sendEingang(any(), any()); +// +// routeForwarding(); +// +// verify(serviceStub).routeForwarding(responseObserver); +// } +// +// @Nested +// class OnSuccess { +// +// @BeforeEach +// void init() { +// givenGrpcCallCompletedSuccessfully(); +// doNothing().when(service).sendEingang(any(), any()); +// } +// +// @Test +// void shouldSendEingang() { +// routeForwarding(); +// +// verify(service).sendEingang(request, requestObserver); +// } +// +// @Test +// void shouldCallOnCompleted() { +// routeForwarding(); +// +// verify(requestObserver).onCompleted(); +// } +// } +// +// @Nested +// class OnFailure { +// +// private final RuntimeException error = new RuntimeException(); +// +// @BeforeEach +// void init() { +// givenGrpcCallCompletedSuccessfully(); +// doThrow(error).when(service).sendEingang(any(), any()); +// } +// +// @SuppressWarnings("ResultOfMethodCallIgnored") +// @Test +// void shouldCallOnError() { +// catchThrowableOfType(RuntimeException.class, TestRouteForwarding.this::routeForwarding); +// +// verify(requestObserver).onError(error); +// } +// +// @Test +// void shouldThrowError() { +// assertThatThrownBy(TestRouteForwarding.this::routeForwarding).isSameAs(error); +// } +// } +// +// private void givenGrpcCallCompletedSuccessfully() { +// when(serviceStub.routeForwarding(any())).thenAnswer(invocation -> { +// ((ForwardingResponseObserver) invocation.getArgument(0)).onCompleted(); +// return requestObserver; +// }); +// } +// +// private void routeForwarding() { +// service.routeForwarding(request, responseObserver); +// } +// } +// +// @Nested +// class TestSendEingang { +// +// private final GrpcRouteForwardingRequest routeForwardingRequest = GrpcRouteForwardingRequestTestFactory.create(); +// +// @BeforeEach +// void init() { +// when(vorgangService.getById(any())).thenReturn(vorgang); +// doReturn(routeForwardingRequest).when(service).buildRouteForwardingRequest(any(), any()); +// doNothing().when(service).sendAttachments(any(), any()); +// doNothing().when(service).sendRepresentations(any(), any()); +// } +// +// @Test +// void shouldGetVorgangById() { +// sendEingang(); +// +// verify(vorgangService).getById(VorgangTestFactory.ID); +// } +// +// @Test +// void shouldBuildRouteForwardingRequest() { +// sendEingang(); +// +// verify(service).buildRouteForwardingRequest(request, eingang); +// } +// +// @Test +// void shouldSendForwardingRequest() { +// sendEingang(); +// +// verify(requestObserver).onNext(routeForwardingRequest); +// } +// +// @Test +// void shouldCallSendAttachments() { +// sendEingang(); +// +// verify(service).sendAttachments(List.of(EingangTestFactory.ATTACHMENT), requestObserver); +// } +// +// @Test +// void shouldCallSendRepresentations() { +// sendEingang(); +// +// verify(service).sendRepresentations(List.of(EingangTestFactory.REPRESENTATION), requestObserver); +// } +// +// private void sendEingang() { +// service.sendEingang(request, requestObserver); +// } +// } +// +// @Nested +// class TestBuildRouteForwardingRequest { +// +// private final GrpcRouteForwarding routeForwarding = GrpcRouteForwardingTestFactory.create(); +// +// @BeforeEach +// void init() { +// when(forwardingRequestMapper.toGrpcRouteForwarding(any(), any())).thenReturn(routeForwarding); +// } +// +// @Test +// void shouldMapToRouteForwarding() { +// buildRouteForwardingRequest(); +// +// verify(forwardingRequestMapper).toGrpcRouteForwarding(request, eingang); +// } +// +// @Test +// void shouldReturnRouteForwardingRequest() { +// var builtRequest = buildRouteForwardingRequest(); +// +// assertThat(builtRequest).isEqualTo(GrpcRouteForwardingRequestTestFactory.create()); +// } +// +// private GrpcRouteForwardingRequest buildRouteForwardingRequest() { +// return service.buildRouteForwardingRequest(request, eingang); +// } +// } +// +// @Nested +// class TestSendAttachments { +// +// @Mock +// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; +// @Mock +// private InputStream inputStream; +// +// private final IncomingFileGroup attachment = IncomingFileGroupTestFactory.create(); +// +// @BeforeEach +// void init() { +// when(fileService.getUploadedFileStream(any())).thenReturn(inputStream); +// doReturn(fileSender).when(service).createAttachmentFileSender(any(), any(), any(), any()); +// when(fileSender.send()).thenReturn(fileSender); +// } +// +// @Test +// void shouldGetUploadedFileContent() { +// sendAttachments(); +// +// verify(fileService).getUploadedFileStream(IncomingFileTestFactory.ID); +// } +// +// @Test +// void shouldCallCreateAttachmentFileSender() { +// sendAttachments(); +// +// verify(service).createAttachmentFileSender(requestObserver, IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE, +// inputStream); +// } +// +// @Test +// void shouldSend() { +// sendAttachments(); +// +// verify(fileSender).send(); +// } +// +// private void sendAttachments() { +// service.sendAttachments(List.of(attachment), requestObserver); +// } +// } +// +// @Nested +// class TestCreateAttachmentFileSender { +// +// @Mock +// private InputStream inputStream; +// @Mock +// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; +// @Mock +// private 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(service).createSenderWithoutMetadata(any(), any(), any()); +// doReturn(metadataRequest).when(service).buildGrpcAttachmentFile(any(), any()); +// when(fileSender.withMetaData(any())).thenReturn(fileSenderWithMetadata); +// } +// +// @Test +// void shouldCallCreateSenderWithoutMetadata() { +// createAttachmentFileSender(); +// +// verify(service).createSenderWithoutMetadata(chunkBuilderCaptor.capture(), eq(requestObserver), eq(inputStream)); +// chunkBuilderCaptor.getValue().apply(chunk, chunk.length); +// verify(service).buildAttachmentChunk(chunk, chunk.length); +// } +// +// @Test +// void shouldCallBuildGrpcAttachmentFile() { +// createAttachmentFileSender(); +// +// verify(service).buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE); +// } +// +// @Test +// void shouldSetMetaData() { +// createAttachmentFileSender(); +// +// verify(fileSender).withMetaData(metadataRequest); +// } +// +// @Test +// void shouldReturnBuiltFileSender() { +// var returnedFileSender = createAttachmentFileSender(); +// +// assertThat(returnedFileSender).isSameAs(fileSenderWithMetadata); +// } +// +// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender() { +// return service.createAttachmentFileSender(requestObserver, IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE, +// inputStream); +// } +// } +// +// @Nested +// class TestBuildAttachmentChunk { +// +// private final byte[] chunk = RandomUtils.insecure().randomBytes(5); +// +// @BeforeEach +// void mock() { +// doReturn(GrpcAttachmentTestFactory.CONTENT).when(service).buildGrpcFileContent(any(), anyInt()); +// } +// +// @Test +// void shouldCallBuildGrpcFileContent() { +// service.buildAttachmentChunk(chunk, chunk.length); +// +// verify(service).buildGrpcFileContent(chunk, chunk.length); +// } +// +// @Test +// void shouldReturnGrpcRouteForwardingRequest() { +// var result = service.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() { +// service.buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, file); +// +// verify(incomingFileMapper).toAttachmentFile(IncomingFileGroupTestFactory.NAME, file); +// } +// +// @Test +// void shouldReturnAttachmentMetadataRequest() { +// var result = service.buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, file); +// +// assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithAttachmentMetadata()); +// } +// } +// +// @Nested +// class TestSendRepresentations { +// +// @Mock +// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; +// @Mock +// private InputStream inputStream; +// +// private final IncomingFile representation = IncomingFileTestFactory.create(); +// +// @BeforeEach +// void init() { +// when(fileService.getUploadedFileStream(any())).thenReturn(inputStream); +// doReturn(fileSender).when(service).createRepresentationFileSender(any(), any(), any()); +// when(fileSender.send()).thenReturn(fileSender); +// } +// +// @Test +// void shouldGetUploadedFileContent() { +// sendRepresentations(); +// +// verify(fileService).getUploadedFileStream(IncomingFileTestFactory.ID); +// } +// +// @Test +// void shouldCallCreateRepresentationFileSender() { +// sendRepresentations(); +// +// verify(service).createRepresentationFileSender(requestObserver, representation, inputStream); +// } +// +// @Test +// void shouldSend() { +// sendRepresentations(); +// +// verify(fileSender).send(); +// } +// +// private void sendRepresentations() { +// service.sendRepresentations(List.of(representation), requestObserver); +// } +// } +// +// @Nested +// class TestCreateRepresentationFileSender { +// +// @Mock +// private InputStream inputStream; +// @Mock +// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; +// @Mock +// private 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(service).createSenderWithoutMetadata(any(), any(), any()); +// doReturn(metadataRequest).when(service).buildGrpcRepresentationFile(any()); +// when(fileSender.withMetaData(any())).thenReturn(fileSenderWithMetadata); +// } +// +// @Test +// void shouldCallCreateSenderWithoutMetadata() { +// createRepresentationFileSender(); +// +// verify(service).createSenderWithoutMetadata(chunkBuilderCaptor.capture(), eq(requestObserver), eq(inputStream)); +// chunkBuilderCaptor.getValue().apply(chunk, chunk.length); +// verify(service).buildRepresentationChunk(chunk, chunk.length); +// } +// +// @Test +// void shouldCallBuildGrpcRepresentationFile() { +// createRepresentationFileSender(); +// +// verify(service).buildGrpcRepresentationFile(incomingFile); +// } +// +// @Test +// void shouldSetMetaData() { +// createRepresentationFileSender(); +// +// verify(fileSender).withMetaData(metadataRequest); +// } +// +// @Test +// void shouldReturnBuiltFileSender() { +// var returnedFileSender = createRepresentationFileSender(); +// +// assertThat(returnedFileSender).isSameAs(fileSenderWithMetadata); +// } +// +// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender() { +// return service.createRepresentationFileSender(requestObserver, incomingFile, inputStream); +// } +// } +// +// @Nested +// class TestCreateSenderWithoutMetadata { +// +// private MockedStatic<GrpcFileUploadUtils> grpcFileUploadUtilsMock; +// @Mock +// private BiFunction<byte[], Integer, GrpcRouteForwardingRequest> chunkBuilder; +// @Mock +// private CallStreamObserver<GrpcRouteForwardingRequest> requestCallStreamObserver; +// @Mock +// private InputStream inputStream; +// @Mock +// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; +// @Mock +// private StreamObserver<GrpcRouteForwardingResponse> responseObserver; +// @Captor +// private ArgumentCaptor<Function<StreamObserver<GrpcRouteForwardingResponse>, CallStreamObserver<GrpcRouteForwardingRequest>>> reqObserverBuilderCaptor; +// +// @BeforeEach +// void init() { +// grpcFileUploadUtilsMock = mockStatic(GrpcFileUploadUtils.class); +// grpcFileUploadUtilsMock.when(() -> GrpcFileUploadUtils.createSender(any(), any(), any(), anyBoolean())).thenReturn(fileSender); +// } +// +// @AfterEach +// void tearDown() { +// grpcFileUploadUtilsMock.close(); +// } +// +// @Test +// void shouldCreateFileSender() { +// createSenderWithoutMetadata(); +// +// grpcFileUploadUtilsMock +// .verify(() -> GrpcFileUploadUtils.createSender(eq(chunkBuilder), eq(inputStream), reqObserverBuilderCaptor.capture(), eq(false))); +// assertThat(reqObserverBuilderCaptor.getValue().apply(responseObserver)).isSameAs(requestCallStreamObserver); +// } +// +// @Test +// void shouldReturnCreatedFileSender() { +// var returnedFileSender = createSenderWithoutMetadata(); +// +// assertThat(returnedFileSender).isSameAs(fileSender); +// } +// +// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata() { +// return service.createSenderWithoutMetadata(chunkBuilder, requestCallStreamObserver, inputStream); +// } +// } +// +// @Nested +// class TestBuildRepresentationChunk { +// +// private final byte[] chunk = RandomUtils.insecure().randomBytes(5); +// +// @BeforeEach +// void mock() { +// doReturn(GrpcRepresentationTestFactory.CONTENT).when(service).buildGrpcFileContent(any(), anyInt()); +// } +// +// @Test +// void shouldCallBuildGrpcFileContent() { +// service.buildRepresentationChunk(chunk, chunk.length); +// +// verify(service).buildGrpcFileContent(chunk, chunk.length); +// } +// +// @Test +// void shouldReturnGrpcRouteForwardingRequest() { +// var result = service.buildRepresentationChunk(chunk, chunk.length); +// +// assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithRepresentationContent()); +// } +// } +// +// @Nested +// class TestBuildGrpcFileContent { +// +// @Nested +// class TestOnEndOfFile { +// +// @Test +// void shouldBuildEndOfFileChunk() { +// var fileContent = service.buildGrpcFileContent(new byte[0], -1); +// +// assertThat(fileContent).isEqualTo(GrpcFileContentTestFactory.createEndOfFile()); +// } +// } +// +// @Nested +// class TestOnContentProvided { +// +// @Test +// void shouldBuildEndOfFileChunk() { +// var fileContent = service.buildGrpcFileContent(GrpcFileContentTestFactory.CONTENT, GrpcFileContentTestFactory.CONTENT.length); +// +// assertThat(fileContent).isEqualTo(GrpcFileContentTestFactory.create()); +// } +// } +// } +// +// @Nested +// class TestBuildGrpcRepresentationFile { +// +// private final IncomingFile file = IncomingFileTestFactory.create(); +// +// @BeforeEach +// void mock() { +// when(incomingFileMapper.toRepresentationFile(any())).thenReturn(GrpcRepresentationFileTestFactory.create()); +// } +// +// @Test +// void shouldCallIncomingFileMapper() { +// service.buildGrpcRepresentationFile(file); +// +// verify(incomingFileMapper).toRepresentationFile(file); +// } +// +// @Test +// void shouldReturnRepresentationMetadataRequest() { +// var result = service.buildGrpcRepresentationFile(file); +// +// assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithRepresentationMetadata()); +// } +// } +// +// @Nested +// class TestWaitForCompletion { +// +// @Mock +// private CompletableFuture<Void> future; +// +// @SneakyThrows +// @Test +// void shouldGetFromFuture() { +// waitForCompletion(); +// +// verify(future).get(2, 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, 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, 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, TestWaitForCompletion.this::waitForCompletion); +// } +// } +// +// private void waitForCompletion() { +// service.waitForCompletion(future); +// } +// } +// +// @Nested +// class ForwardingResponseObserverTest { +// +// @Mock +// private CompletableFuture<Void> future; +// private ForwardingResponseObserver responseObserver; +// +// @BeforeEach +// void init() { +// responseObserver = new ForwardingResponseObserver(future); +// } +// +// @Test +// void shouldCompleteExceptionallyOnError() { +// var error = new Throwable(); +// +// responseObserver.onError(error); +// +// verify(future).completeExceptionally(error); +// } +// +// @Test +// void shouldCompleteOnCompleted() { +// responseObserver.onCompleted(); +// +// verify(future).complete(null); +// } +// } +//} -- GitLab From 9d6f4703635b482b6b708dad755e9767723acbe4 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Fri, 28 Mar 2025 10:53:19 +0100 Subject: [PATCH 02/18] OZG-7573 OZG-7991 Rewrite ForwardingRemoteService --- .../vorgang/redirect/EingangForwarder.java | 238 ++++++ .../redirect/ForwardingRemoteService.java | 163 +--- .../redirect/ForwardingRemoteServiceTest.java | 796 ------------------ 3 files changed, 242 insertions(+), 955 deletions(-) create mode 100644 vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarder.java delete mode 100644 vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteServiceTest.java 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 new file mode 100644 index 000000000..5136f4285 --- /dev/null +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarder.java @@ -0,0 +1,238 @@ +package de.ozgcloud.vorgang.vorgang.redirect; + +import java.io.InputStream; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; + +import com.google.protobuf.ByteString; + +import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; +import de.ozgcloud.eingang.forwarder.RouteForwardingServiceGrpc; +import de.ozgcloud.eingang.forwarding.GrpcAttachment; +import de.ozgcloud.eingang.forwarding.GrpcFileContent; +import de.ozgcloud.eingang.forwarding.GrpcRepresentation; +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.FileService; +import de.ozgcloud.vorgang.vorgang.IncomingFile; +import de.ozgcloud.vorgang.vorgang.IncomingFileGroup; +import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; +import io.grpc.stub.ClientCallStreamObserver; +import io.grpc.stub.ClientResponseObserver; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +class EingangForwarder { + + private final RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; + private final FileService fileService; + + private final IncomingFileMapper incomingFileMapper; + + private ForwardingResponseObserver responseObserver; + private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver; + + public CompletableFuture<Void> forward(GrpcRouteForwarding grpcRouteForwarding, List<IncomingFileGroup> attachments, + List<IncomingFile> representations) { + return CompletableFuture.allOf( + callService(), + sendRouteForwarding(grpcRouteForwarding) + .thenCompose(ignored -> sendAttachments(attachments)) + .thenCompose(ignored -> sendRepresentations(representations)) + .whenComplete((result, ex) -> { + if (ex != null) { + responseObserver.onError(ex); + } else { + responseObserver.onCompleted(); + } + }) + ); + } + + CompletableFuture<GrpcRouteForwardingResponse> callService() { + CompletableFuture<GrpcRouteForwardingResponse> responseFuture = new CompletableFuture<>(); + responseObserver = new ForwardingResponseObserver(responseFuture); + requestObserver = (ClientCallStreamObserver<GrpcRouteForwardingRequest>) serviceStub.withInterceptors( + new VorgangManagerClientCallContextAttachingInterceptor()) + .routeForwarding(responseObserver); + return responseFuture; + } + + CompletableFuture<GrpcRouteForwardingResponse> sendRouteForwarding(GrpcRouteForwarding grpcRouteForwarding) { + CompletableFuture<GrpcRouteForwardingResponse> future = new CompletableFuture<>(); + responseObserver.registerOnReadyHandler(() -> { + requestObserver.onNext(GrpcRouteForwardingRequest.newBuilder().setRouteForwarding(grpcRouteForwarding).build()); + future.complete(GrpcRouteForwardingResponse.newBuilder().build()); + }); + return future; + } + + CompletableFuture<GrpcRouteForwardingResponse> sendAttachments(List<IncomingFileGroup> attachments) { + return attachments.stream() + .flatMap(attachment -> { + var groupName = attachment.getName(); + return attachment.getFiles().stream().map(file -> getSendAttachmentFileFunction(groupName, file)); + }) + .reduce( + CompletableFuture.completedFuture(GrpcRouteForwardingResponse.newBuilder().build()), + CompletableFuture::thenCompose, + (f1, f2) -> f1.thenCompose(ignored -> f2) + ); + } + + private Function<GrpcRouteForwardingResponse, CompletableFuture<GrpcRouteForwardingResponse>> getSendAttachmentFileFunction(String groupName, + IncomingFile file) { + return ignored -> sendAttachmentFile(groupName, file); + } + + private CompletableFuture<GrpcRouteForwardingResponse> sendAttachmentFile(String groupName, IncomingFile file) { + var fileContentStream = fileService.getUploadedFileStream(file.getId()); + var sender = createAttachmentFileSender(groupName, file, fileContentStream).send(responseObserver::registerOnReadyHandler); + return sender.getResultFuture(); + } + + GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender(String groupName, + IncomingFile file, + InputStream fileContentStream) { + return createSenderWithoutMetadata(this::buildAttachmentChunk, fileContentStream) + .withMetaData(buildGrpcAttachmentFile(groupName, file)); + } + + GrpcRouteForwardingRequest buildAttachmentChunk(byte[] chunk, int length) { + return GrpcRouteForwardingRequest.newBuilder() + .setAttachment(GrpcAttachment.newBuilder() + .setContent(buildGrpcFileContent(chunk, length)) + .build()) + .build(); + } + + GrpcRouteForwardingRequest buildGrpcAttachmentFile(String name, IncomingFile file) { + return GrpcRouteForwardingRequest.newBuilder() + .setAttachment(GrpcAttachment.newBuilder() + .setFile(incomingFileMapper.toAttachmentFile(name, file)) + .build()) + .build(); + } + + CompletableFuture<GrpcRouteForwardingResponse> sendRepresentations(List<IncomingFile> representations) { + return representations.stream() + .map(this::getSendRepresentationFileFunction) + .reduce( + CompletableFuture.completedFuture(GrpcRouteForwardingResponse.newBuilder().build()), + CompletableFuture::thenCompose, + (f1, f2) -> f1.thenCompose(ignored -> f2) + ); + } + + private Function<GrpcRouteForwardingResponse, CompletableFuture<GrpcRouteForwardingResponse>> getSendRepresentationFileFunction(IncomingFile file) { + return ignored -> sendRepresentationFile(file); + } + + private CompletableFuture<GrpcRouteForwardingResponse> sendRepresentationFile(IncomingFile file) { + var fileContentStream = fileService.getUploadedFileStream(file.getId()); + var sender = createRepresentationFileSender(file, fileContentStream).send(responseObserver::registerOnReadyHandler); + return sender.getResultFuture(); + } + + GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender(IncomingFile file, + InputStream fileContentStream) { + return createSenderWithoutMetadata(this::buildRepresentationChunk, fileContentStream).withMetaData(buildGrpcRepresentationFile(file)); + } + + GrpcRouteForwardingRequest buildRepresentationChunk(byte[] chunk, int length) { + return GrpcRouteForwardingRequest.newBuilder() + .setRepresentation(GrpcRepresentation.newBuilder() + .setContent(buildGrpcFileContent(chunk, length)) + .build()) + .build(); + } + + GrpcRouteForwardingRequest buildGrpcRepresentationFile(IncomingFile file) { + return GrpcRouteForwardingRequest.newBuilder() + .setRepresentation(GrpcRepresentation.newBuilder() + .setFile(incomingFileMapper.toRepresentationFile(file)) + .build()) + .build(); + } + + GrpcFileContent buildGrpcFileContent(byte[] chunk, int length) { + var fileContentBuilder = GrpcFileContent.newBuilder(); + if (length <= 0) { + fileContentBuilder.setIsEndOfFile(true); + } else { + fileContentBuilder.setContent(ByteString.copyFrom(chunk)); + } + return fileContentBuilder.build(); + } + + GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata( + BiFunction<byte[], Integer, GrpcRouteForwardingRequest> chunkBuilder, InputStream fileContentStream) { + return GrpcFileUploadUtils.createSender(chunkBuilder, fileContentStream, response -> requestObserver, false); + } + + @RequiredArgsConstructor + static class ForwardingResponseObserver implements ClientResponseObserver<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> { + private final CompletableFuture<GrpcRouteForwardingResponse> future; + private DelegatingOnReadyHandler onReadyHandler; + private GrpcRouteForwardingResponse response; + + @Override + public void beforeStart(ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStream) { + onReadyHandler = new DelegatingOnReadyHandler(requestStream); + requestStream.setOnReadyHandler(onReadyHandler); + } + + @Override + public void onNext(GrpcRouteForwardingResponse response) { + this.response = response; + } + + @Override + public void onError(Throwable t) { + onReadyHandler.stop(); + future.completeExceptionally(t); + } + + @Override + public void onCompleted() { + onReadyHandler.stop(); + future.complete(response); + } + + public void registerOnReadyHandler(Runnable onReadyHandler) { + this.onReadyHandler.setDelegate(onReadyHandler); + } + } + + @RequiredArgsConstructor + static class DelegatingOnReadyHandler implements Runnable { + + private final ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStream; + private final AtomicReference<Runnable> onReadyHandler = new AtomicReference<>(); + private final AtomicBoolean done = new AtomicBoolean(false); + + public void setDelegate(Runnable onReadyHandler) { + this.onReadyHandler.set(onReadyHandler); + } + + public void stop() { + done.set(true); + } + + @Override + public void run() { + while (!done.get() && requestStream.isReady()) { + var runnable = onReadyHandler.get(); + if (runnable != null) { + runnable.run(); + } + } + } + } +} 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 5edee75f4..371374eca 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 @@ -23,39 +23,18 @@ */ package de.ozgcloud.vorgang.vorgang.redirect; -import java.io.InputStream; -import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.function.BiFunction; -import java.util.function.Function; import org.springframework.stereotype.Service; -import com.google.protobuf.ByteString; - -import de.ozgcloud.common.binaryfile.BinaryFileUploadStreamObserver; -import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; -import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils.FileSender; import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.eingang.forwarder.RouteForwardingServiceGrpc; -import de.ozgcloud.eingang.forwarding.GrpcAttachment; -import de.ozgcloud.eingang.forwarding.GrpcFileContent; -import de.ozgcloud.eingang.forwarding.GrpcRepresentation; -import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest; -import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingResponse; -import de.ozgcloud.vorgang.callcontext.VorgangManagerClientCallContextAttachingInterceptor; import de.ozgcloud.vorgang.files.FileService; -import de.ozgcloud.vorgang.vorgang.Eingang; -import de.ozgcloud.vorgang.vorgang.IncomingFile; -import de.ozgcloud.vorgang.vorgang.IncomingFileGroup; import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; import de.ozgcloud.vorgang.vorgang.VorgangService; -import io.grpc.stub.CallStreamObserver; -import io.grpc.stub.ClientCallStreamObserver; -import io.grpc.stub.StreamObserver; import lombok.RequiredArgsConstructor; import net.devh.boot.grpc.client.inject.GrpcClient; @@ -63,7 +42,7 @@ import net.devh.boot.grpc.client.inject.GrpcClient; @RequiredArgsConstructor class ForwardingRemoteService { - private static final int TIMEOUT_MINUTES = 2; + private static final int TIMEOUT_MINUTES = 10; private final VorgangService vorgangService; private final ForwardingRequestMapper forwardingRequestMapper; @GrpcClient("forwarder") @@ -72,124 +51,10 @@ class ForwardingRemoteService { private final IncomingFileMapper incomingFileMapper; public void forward(ForwardingRequest request) { - CompletableFuture<Void> responseFuture = new CompletableFuture<>(); - routeForwarding(request, new ForwardingResponseObserver(responseFuture)); - waitForCompletion(responseFuture); - } - - void routeForwarding(ForwardingRequest request, ForwardingResponseObserver responseObserver) { - var requestStreamObserver = (ClientCallStreamObserver<GrpcRouteForwardingRequest>) serviceStub.withInterceptors(new VorgangManagerClientCallContextAttachingInterceptor()) - .routeForwarding(responseObserver); - try { - sendEingang(request, requestStreamObserver); - requestStreamObserver.onCompleted(); - } catch (Exception e) { - requestStreamObserver.onError(e); - throw e; - } - } - - void sendEingang(ForwardingRequest request, ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver) { var eingang = vorgangService.getById(request.getVorgangId()).getEingangs().getFirst(); - requestStreamObserver.onNext(buildRouteForwardingRequest(request, eingang)); - sendAttachments(eingang.getAttachments(), requestStreamObserver); - sendRepresentations(eingang.getRepresentations(), requestStreamObserver); - } - - GrpcRouteForwardingRequest buildRouteForwardingRequest(ForwardingRequest request, Eingang eingang) { - var routeForwarding = forwardingRequestMapper.toGrpcRouteForwarding(request, eingang); - return GrpcRouteForwardingRequest.newBuilder().setRouteForwarding(routeForwarding).build(); - } - - void sendAttachments(List<IncomingFileGroup> attachments, ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver) { - for (var attachment : attachments) { - var groupName = attachment.getName(); - attachment.getFiles().forEach(file -> sendAttachmentFile(requestStreamObserver, groupName, file)); - } - } - - private void sendAttachmentFile(ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, String groupName, IncomingFile file) { - var fileContentStream = fileService.getUploadedFileStream(file.getId()); - var sender = createAttachmentFileSender(requestStreamObserver, groupName, file, fileContentStream).send(); - waitForCompletion(sender.getResultFuture()); - } - - FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender( - ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, String groupName, IncomingFile file, InputStream fileContentStream) { - return createSenderWithoutMetadata(this::buildAttachmentChunk, requestStreamObserver, fileContentStream) - .withMetaData(buildGrpcAttachmentFile(groupName, file)); - } - - GrpcRouteForwardingRequest buildAttachmentChunk(byte[] chunk, int length) { - return GrpcRouteForwardingRequest.newBuilder() - .setAttachment(GrpcAttachment.newBuilder() - .setContent(buildGrpcFileContent(chunk, length)) - .build()) - .build(); - } - - GrpcRouteForwardingRequest buildGrpcAttachmentFile(String name, IncomingFile file) { - return GrpcRouteForwardingRequest.newBuilder() - .setAttachment(GrpcAttachment.newBuilder() - .setFile(incomingFileMapper.toAttachmentFile(name, file)) - .build()) - .build(); - } - - void sendRepresentations(List<IncomingFile> representations, ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver) { - representations.forEach(representation -> { - var fileContentStream = fileService.getUploadedFileStream(representation.getId()); - var sender = createRepresentationFileSender(requestObserver, representation, fileContentStream).send(); - waitForCompletion(sender.getResultFuture()); - }); - } - - FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender( - ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, IncomingFile file, InputStream fileContentStream) { - return createSenderWithoutMetadata(this::buildRepresentationChunk, requestStreamObserver, fileContentStream) - .withMetaData(buildGrpcRepresentationFile(file)); - } - - FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata( - BiFunction<byte[], Integer, GrpcRouteForwardingRequest> chunkBuilder, - ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver, InputStream fileContentStream) { - return GrpcFileUploadUtils - .createSender(chunkBuilder, fileContentStream, requestCallStreamObserverProvider(requestStreamObserver), false); - } - - private Function<StreamObserver<GrpcRouteForwardingResponse>, CallStreamObserver<GrpcRouteForwardingRequest>> requestCallStreamObserverProvider( - ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStreamObserver) { - // responseObserver should be passed to GrpcService used to transfer files, otherwise onNext()-method won't be called - return response -> { - ((BinaryFileUploadStreamObserver<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse>) response).beforeStart(requestStreamObserver); - return (CallStreamObserver<GrpcRouteForwardingRequest>) requestStreamObserver; - }; - } - - GrpcRouteForwardingRequest buildRepresentationChunk(byte[] chunk, int length) { - return GrpcRouteForwardingRequest.newBuilder() - .setRepresentation(GrpcRepresentation.newBuilder() - .setContent(buildGrpcFileContent(chunk, length)) - .build()) - .build(); - } - - GrpcFileContent buildGrpcFileContent(byte[] chunk, int length) { - var fileContentBuilder = GrpcFileContent.newBuilder(); - if (length <= 0) { - fileContentBuilder.setIsEndOfFile(true); - } else { - fileContentBuilder.setContent(ByteString.copyFrom(chunk)); - } - return fileContentBuilder.build(); - } - - GrpcRouteForwardingRequest buildGrpcRepresentationFile(IncomingFile file) { - return GrpcRouteForwardingRequest.newBuilder() - .setRepresentation(GrpcRepresentation.newBuilder() - .setFile(incomingFileMapper.toRepresentationFile(file)) - .build()) - .build(); + var grpcRouteForwarding = forwardingRequestMapper.toGrpcRouteForwarding(request, eingang); + var responseFuture = new EingangForwarder(serviceStub, fileService, incomingFileMapper).forward(grpcRouteForwarding, eingang.getAttachments(), eingang.getRepresentations()); + waitForCompletion(responseFuture); } <T> void waitForCompletion(CompletableFuture<T> responseFuture) { @@ -204,24 +69,4 @@ class ForwardingRemoteService { throw new TechnicalException("Timeout on uploading file content.", e); } } - - @RequiredArgsConstructor - static class ForwardingResponseObserver implements StreamObserver<GrpcRouteForwardingResponse> { - private final CompletableFuture<Void> future; - - @Override - public void onNext(GrpcRouteForwardingResponse value) { - // noop - } - - @Override - public void onError(Throwable t) { - future.completeExceptionally(t); - } - - @Override - public void onCompleted() { - future.complete(null); - } - } } 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 deleted file mode 100644 index d39685c14..000000000 --- a/vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteServiceTest.java +++ /dev/null @@ -1,796 +0,0 @@ -///* -// * Copyright (C) 2025 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.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.io.InputStream; -//import java.util.List; -//import java.util.concurrent.CompletableFuture; -//import java.util.concurrent.ExecutionException; -//import java.util.concurrent.TimeUnit; -//import java.util.concurrent.TimeoutException; -//import java.util.function.BiFunction; -//import java.util.function.Function; -// -//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.GrpcFileUploadUtils.FileSender; -//import de.ozgcloud.common.errorhandling.TechnicalException; -//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.FileService; -//import de.ozgcloud.vorgang.vorgang.Eingang; -//import de.ozgcloud.vorgang.vorgang.EingangTestFactory; -//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.Vorgang; -//import de.ozgcloud.vorgang.vorgang.VorgangService; -//import de.ozgcloud.vorgang.vorgang.VorgangTestFactory; -//import de.ozgcloud.vorgang.vorgang.redirect.ForwardingRemoteService.ForwardingResponseObserver; -//import io.grpc.stub.CallStreamObserver; -//import io.grpc.stub.StreamObserver; -//import lombok.SneakyThrows; -// -//class ForwardingRemoteServiceTest { -// -// @Spy -// @InjectMocks -// private ForwardingRemoteService service; -// @Mock -// private VorgangService vorgangService; -// @Mock -// private ForwardingRequestMapper forwardingRequestMapper; -// @Mock -// private RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; -// @Mock -// private FileService fileService; -// @Mock -// private IncomingFileMapper incomingFileMapper; -// -// @Mock -// private StreamObserver<GrpcRouteForwardingRequest> requestObserver; -// private final ForwardingRequest request = ForwardingRequestTestFactory.create(); -// private final Eingang eingang = EingangTestFactory.create(); -// private final Vorgang vorgang = VorgangTestFactory.createBuilder().clearEingangs().eingang(eingang).build(); -// -// @Nested -// class TestForward { -// -// @Captor -// private ArgumentCaptor<ForwardingResponseObserver> responseObserverCaptor; -// @Captor -// private ArgumentCaptor<CompletableFuture<Void>> futureCaptor; -// -// @BeforeEach -// void init() { -// doNothing().when(service).routeForwarding(any(), any()); -// doNothing().when(service).waitForCompletion(any()); -// } -// -// @Test -// void shouldRouteForwarding() { -// forward(); -// -// verify(service).routeForwarding(eq(request), any(ForwardingResponseObserver.class)); -// } -// -// @Test -// void shouldWaitForCompletion() { -// forward(); -// -// verify(service).waitForCompletion(futureCaptor.capture()); -// verify(service).routeForwarding(any(), responseObserverCaptor.capture()); -// assertThat(futureCaptor.getValue()) -// .isSameAs(ReflectionTestUtils.getField(responseObserverCaptor.getValue(), "future", CompletableFuture.class)); -// } -// -// private void forward() { -// service.forward(request); -// } -// } -// -// @Nested -// class TestRouteForwarding { -// -// @Mock -// private ForwardingResponseObserver responseObserver; -// -// @BeforeEach -// void init() { -// when(serviceStub.withInterceptors(any())).thenReturn(serviceStub); -// } -// -// @Test -// void shouldAttachClientCallContextToServiceStub() { -// givenGrpcCallCompletedSuccessfully(); -// doNothing().when(service).sendEingang(any(), any()); -// -// routeForwarding(); -// -// verify(serviceStub).withInterceptors(any(VorgangManagerClientCallContextAttachingInterceptor.class)); -// } -// -// @Test -// void shouldMakeGrpcCallToRouteForwarding() { -// givenGrpcCallCompletedSuccessfully(); -// doNothing().when(service).sendEingang(any(), any()); -// -// routeForwarding(); -// -// verify(serviceStub).routeForwarding(responseObserver); -// } -// -// @Nested -// class OnSuccess { -// -// @BeforeEach -// void init() { -// givenGrpcCallCompletedSuccessfully(); -// doNothing().when(service).sendEingang(any(), any()); -// } -// -// @Test -// void shouldSendEingang() { -// routeForwarding(); -// -// verify(service).sendEingang(request, requestObserver); -// } -// -// @Test -// void shouldCallOnCompleted() { -// routeForwarding(); -// -// verify(requestObserver).onCompleted(); -// } -// } -// -// @Nested -// class OnFailure { -// -// private final RuntimeException error = new RuntimeException(); -// -// @BeforeEach -// void init() { -// givenGrpcCallCompletedSuccessfully(); -// doThrow(error).when(service).sendEingang(any(), any()); -// } -// -// @SuppressWarnings("ResultOfMethodCallIgnored") -// @Test -// void shouldCallOnError() { -// catchThrowableOfType(RuntimeException.class, TestRouteForwarding.this::routeForwarding); -// -// verify(requestObserver).onError(error); -// } -// -// @Test -// void shouldThrowError() { -// assertThatThrownBy(TestRouteForwarding.this::routeForwarding).isSameAs(error); -// } -// } -// -// private void givenGrpcCallCompletedSuccessfully() { -// when(serviceStub.routeForwarding(any())).thenAnswer(invocation -> { -// ((ForwardingResponseObserver) invocation.getArgument(0)).onCompleted(); -// return requestObserver; -// }); -// } -// -// private void routeForwarding() { -// service.routeForwarding(request, responseObserver); -// } -// } -// -// @Nested -// class TestSendEingang { -// -// private final GrpcRouteForwardingRequest routeForwardingRequest = GrpcRouteForwardingRequestTestFactory.create(); -// -// @BeforeEach -// void init() { -// when(vorgangService.getById(any())).thenReturn(vorgang); -// doReturn(routeForwardingRequest).when(service).buildRouteForwardingRequest(any(), any()); -// doNothing().when(service).sendAttachments(any(), any()); -// doNothing().when(service).sendRepresentations(any(), any()); -// } -// -// @Test -// void shouldGetVorgangById() { -// sendEingang(); -// -// verify(vorgangService).getById(VorgangTestFactory.ID); -// } -// -// @Test -// void shouldBuildRouteForwardingRequest() { -// sendEingang(); -// -// verify(service).buildRouteForwardingRequest(request, eingang); -// } -// -// @Test -// void shouldSendForwardingRequest() { -// sendEingang(); -// -// verify(requestObserver).onNext(routeForwardingRequest); -// } -// -// @Test -// void shouldCallSendAttachments() { -// sendEingang(); -// -// verify(service).sendAttachments(List.of(EingangTestFactory.ATTACHMENT), requestObserver); -// } -// -// @Test -// void shouldCallSendRepresentations() { -// sendEingang(); -// -// verify(service).sendRepresentations(List.of(EingangTestFactory.REPRESENTATION), requestObserver); -// } -// -// private void sendEingang() { -// service.sendEingang(request, requestObserver); -// } -// } -// -// @Nested -// class TestBuildRouteForwardingRequest { -// -// private final GrpcRouteForwarding routeForwarding = GrpcRouteForwardingTestFactory.create(); -// -// @BeforeEach -// void init() { -// when(forwardingRequestMapper.toGrpcRouteForwarding(any(), any())).thenReturn(routeForwarding); -// } -// -// @Test -// void shouldMapToRouteForwarding() { -// buildRouteForwardingRequest(); -// -// verify(forwardingRequestMapper).toGrpcRouteForwarding(request, eingang); -// } -// -// @Test -// void shouldReturnRouteForwardingRequest() { -// var builtRequest = buildRouteForwardingRequest(); -// -// assertThat(builtRequest).isEqualTo(GrpcRouteForwardingRequestTestFactory.create()); -// } -// -// private GrpcRouteForwardingRequest buildRouteForwardingRequest() { -// return service.buildRouteForwardingRequest(request, eingang); -// } -// } -// -// @Nested -// class TestSendAttachments { -// -// @Mock -// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; -// @Mock -// private InputStream inputStream; -// -// private final IncomingFileGroup attachment = IncomingFileGroupTestFactory.create(); -// -// @BeforeEach -// void init() { -// when(fileService.getUploadedFileStream(any())).thenReturn(inputStream); -// doReturn(fileSender).when(service).createAttachmentFileSender(any(), any(), any(), any()); -// when(fileSender.send()).thenReturn(fileSender); -// } -// -// @Test -// void shouldGetUploadedFileContent() { -// sendAttachments(); -// -// verify(fileService).getUploadedFileStream(IncomingFileTestFactory.ID); -// } -// -// @Test -// void shouldCallCreateAttachmentFileSender() { -// sendAttachments(); -// -// verify(service).createAttachmentFileSender(requestObserver, IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE, -// inputStream); -// } -// -// @Test -// void shouldSend() { -// sendAttachments(); -// -// verify(fileSender).send(); -// } -// -// private void sendAttachments() { -// service.sendAttachments(List.of(attachment), requestObserver); -// } -// } -// -// @Nested -// class TestCreateAttachmentFileSender { -// -// @Mock -// private InputStream inputStream; -// @Mock -// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; -// @Mock -// private 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(service).createSenderWithoutMetadata(any(), any(), any()); -// doReturn(metadataRequest).when(service).buildGrpcAttachmentFile(any(), any()); -// when(fileSender.withMetaData(any())).thenReturn(fileSenderWithMetadata); -// } -// -// @Test -// void shouldCallCreateSenderWithoutMetadata() { -// createAttachmentFileSender(); -// -// verify(service).createSenderWithoutMetadata(chunkBuilderCaptor.capture(), eq(requestObserver), eq(inputStream)); -// chunkBuilderCaptor.getValue().apply(chunk, chunk.length); -// verify(service).buildAttachmentChunk(chunk, chunk.length); -// } -// -// @Test -// void shouldCallBuildGrpcAttachmentFile() { -// createAttachmentFileSender(); -// -// verify(service).buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE); -// } -// -// @Test -// void shouldSetMetaData() { -// createAttachmentFileSender(); -// -// verify(fileSender).withMetaData(metadataRequest); -// } -// -// @Test -// void shouldReturnBuiltFileSender() { -// var returnedFileSender = createAttachmentFileSender(); -// -// assertThat(returnedFileSender).isSameAs(fileSenderWithMetadata); -// } -// -// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender() { -// return service.createAttachmentFileSender(requestObserver, IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE, -// inputStream); -// } -// } -// -// @Nested -// class TestBuildAttachmentChunk { -// -// private final byte[] chunk = RandomUtils.insecure().randomBytes(5); -// -// @BeforeEach -// void mock() { -// doReturn(GrpcAttachmentTestFactory.CONTENT).when(service).buildGrpcFileContent(any(), anyInt()); -// } -// -// @Test -// void shouldCallBuildGrpcFileContent() { -// service.buildAttachmentChunk(chunk, chunk.length); -// -// verify(service).buildGrpcFileContent(chunk, chunk.length); -// } -// -// @Test -// void shouldReturnGrpcRouteForwardingRequest() { -// var result = service.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() { -// service.buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, file); -// -// verify(incomingFileMapper).toAttachmentFile(IncomingFileGroupTestFactory.NAME, file); -// } -// -// @Test -// void shouldReturnAttachmentMetadataRequest() { -// var result = service.buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, file); -// -// assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithAttachmentMetadata()); -// } -// } -// -// @Nested -// class TestSendRepresentations { -// -// @Mock -// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; -// @Mock -// private InputStream inputStream; -// -// private final IncomingFile representation = IncomingFileTestFactory.create(); -// -// @BeforeEach -// void init() { -// when(fileService.getUploadedFileStream(any())).thenReturn(inputStream); -// doReturn(fileSender).when(service).createRepresentationFileSender(any(), any(), any()); -// when(fileSender.send()).thenReturn(fileSender); -// } -// -// @Test -// void shouldGetUploadedFileContent() { -// sendRepresentations(); -// -// verify(fileService).getUploadedFileStream(IncomingFileTestFactory.ID); -// } -// -// @Test -// void shouldCallCreateRepresentationFileSender() { -// sendRepresentations(); -// -// verify(service).createRepresentationFileSender(requestObserver, representation, inputStream); -// } -// -// @Test -// void shouldSend() { -// sendRepresentations(); -// -// verify(fileSender).send(); -// } -// -// private void sendRepresentations() { -// service.sendRepresentations(List.of(representation), requestObserver); -// } -// } -// -// @Nested -// class TestCreateRepresentationFileSender { -// -// @Mock -// private InputStream inputStream; -// @Mock -// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; -// @Mock -// private 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(service).createSenderWithoutMetadata(any(), any(), any()); -// doReturn(metadataRequest).when(service).buildGrpcRepresentationFile(any()); -// when(fileSender.withMetaData(any())).thenReturn(fileSenderWithMetadata); -// } -// -// @Test -// void shouldCallCreateSenderWithoutMetadata() { -// createRepresentationFileSender(); -// -// verify(service).createSenderWithoutMetadata(chunkBuilderCaptor.capture(), eq(requestObserver), eq(inputStream)); -// chunkBuilderCaptor.getValue().apply(chunk, chunk.length); -// verify(service).buildRepresentationChunk(chunk, chunk.length); -// } -// -// @Test -// void shouldCallBuildGrpcRepresentationFile() { -// createRepresentationFileSender(); -// -// verify(service).buildGrpcRepresentationFile(incomingFile); -// } -// -// @Test -// void shouldSetMetaData() { -// createRepresentationFileSender(); -// -// verify(fileSender).withMetaData(metadataRequest); -// } -// -// @Test -// void shouldReturnBuiltFileSender() { -// var returnedFileSender = createRepresentationFileSender(); -// -// assertThat(returnedFileSender).isSameAs(fileSenderWithMetadata); -// } -// -// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender() { -// return service.createRepresentationFileSender(requestObserver, incomingFile, inputStream); -// } -// } -// -// @Nested -// class TestCreateSenderWithoutMetadata { -// -// private MockedStatic<GrpcFileUploadUtils> grpcFileUploadUtilsMock; -// @Mock -// private BiFunction<byte[], Integer, GrpcRouteForwardingRequest> chunkBuilder; -// @Mock -// private CallStreamObserver<GrpcRouteForwardingRequest> requestCallStreamObserver; -// @Mock -// private InputStream inputStream; -// @Mock -// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; -// @Mock -// private StreamObserver<GrpcRouteForwardingResponse> responseObserver; -// @Captor -// private ArgumentCaptor<Function<StreamObserver<GrpcRouteForwardingResponse>, CallStreamObserver<GrpcRouteForwardingRequest>>> reqObserverBuilderCaptor; -// -// @BeforeEach -// void init() { -// grpcFileUploadUtilsMock = mockStatic(GrpcFileUploadUtils.class); -// grpcFileUploadUtilsMock.when(() -> GrpcFileUploadUtils.createSender(any(), any(), any(), anyBoolean())).thenReturn(fileSender); -// } -// -// @AfterEach -// void tearDown() { -// grpcFileUploadUtilsMock.close(); -// } -// -// @Test -// void shouldCreateFileSender() { -// createSenderWithoutMetadata(); -// -// grpcFileUploadUtilsMock -// .verify(() -> GrpcFileUploadUtils.createSender(eq(chunkBuilder), eq(inputStream), reqObserverBuilderCaptor.capture(), eq(false))); -// assertThat(reqObserverBuilderCaptor.getValue().apply(responseObserver)).isSameAs(requestCallStreamObserver); -// } -// -// @Test -// void shouldReturnCreatedFileSender() { -// var returnedFileSender = createSenderWithoutMetadata(); -// -// assertThat(returnedFileSender).isSameAs(fileSender); -// } -// -// private FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata() { -// return service.createSenderWithoutMetadata(chunkBuilder, requestCallStreamObserver, inputStream); -// } -// } -// -// @Nested -// class TestBuildRepresentationChunk { -// -// private final byte[] chunk = RandomUtils.insecure().randomBytes(5); -// -// @BeforeEach -// void mock() { -// doReturn(GrpcRepresentationTestFactory.CONTENT).when(service).buildGrpcFileContent(any(), anyInt()); -// } -// -// @Test -// void shouldCallBuildGrpcFileContent() { -// service.buildRepresentationChunk(chunk, chunk.length); -// -// verify(service).buildGrpcFileContent(chunk, chunk.length); -// } -// -// @Test -// void shouldReturnGrpcRouteForwardingRequest() { -// var result = service.buildRepresentationChunk(chunk, chunk.length); -// -// assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithRepresentationContent()); -// } -// } -// -// @Nested -// class TestBuildGrpcFileContent { -// -// @Nested -// class TestOnEndOfFile { -// -// @Test -// void shouldBuildEndOfFileChunk() { -// var fileContent = service.buildGrpcFileContent(new byte[0], -1); -// -// assertThat(fileContent).isEqualTo(GrpcFileContentTestFactory.createEndOfFile()); -// } -// } -// -// @Nested -// class TestOnContentProvided { -// -// @Test -// void shouldBuildEndOfFileChunk() { -// var fileContent = service.buildGrpcFileContent(GrpcFileContentTestFactory.CONTENT, GrpcFileContentTestFactory.CONTENT.length); -// -// assertThat(fileContent).isEqualTo(GrpcFileContentTestFactory.create()); -// } -// } -// } -// -// @Nested -// class TestBuildGrpcRepresentationFile { -// -// private final IncomingFile file = IncomingFileTestFactory.create(); -// -// @BeforeEach -// void mock() { -// when(incomingFileMapper.toRepresentationFile(any())).thenReturn(GrpcRepresentationFileTestFactory.create()); -// } -// -// @Test -// void shouldCallIncomingFileMapper() { -// service.buildGrpcRepresentationFile(file); -// -// verify(incomingFileMapper).toRepresentationFile(file); -// } -// -// @Test -// void shouldReturnRepresentationMetadataRequest() { -// var result = service.buildGrpcRepresentationFile(file); -// -// assertThat(result).isEqualTo(GrpcRouteForwardingRequestTestFactory.createWithRepresentationMetadata()); -// } -// } -// -// @Nested -// class TestWaitForCompletion { -// -// @Mock -// private CompletableFuture<Void> future; -// -// @SneakyThrows -// @Test -// void shouldGetFromFuture() { -// waitForCompletion(); -// -// verify(future).get(2, 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, 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, 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, TestWaitForCompletion.this::waitForCompletion); -// } -// } -// -// private void waitForCompletion() { -// service.waitForCompletion(future); -// } -// } -// -// @Nested -// class ForwardingResponseObserverTest { -// -// @Mock -// private CompletableFuture<Void> future; -// private ForwardingResponseObserver responseObserver; -// -// @BeforeEach -// void init() { -// responseObserver = new ForwardingResponseObserver(future); -// } -// -// @Test -// void shouldCompleteExceptionallyOnError() { -// var error = new Throwable(); -// -// responseObserver.onError(error); -// -// verify(future).completeExceptionally(error); -// } -// -// @Test -// void shouldCompleteOnCompleted() { -// responseObserver.onCompleted(); -// -// verify(future).complete(null); -// } -// } -//} -- GitLab From adfe531b216b2fb9edf5a4be1bd6ae80838130f6 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Fri, 28 Mar 2025 11:09:47 +0100 Subject: [PATCH 03/18] OZG-7573 OZG-7991 Copy GrpcFileUploadUtils from common-lib (only temporarily) --- .../BinaryFileUploadStreamObserver.java | 81 ++++++ .../binaryfile/GrpcFileUploadUtils.java | 257 ++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileUploadStreamObserver.java create mode 100644 vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/GrpcFileUploadUtils.java diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileUploadStreamObserver.java b/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileUploadStreamObserver.java new file mode 100644 index 000000000..44635a997 --- /dev/null +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileUploadStreamObserver.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 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.common.binaryfile; + +import java.util.concurrent.CompletableFuture; + +import io.grpc.stub.ClientCallStreamObserver; +import io.grpc.stub.ClientResponseObserver; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class BinaryFileUploadStreamObserver<ReqT, R> implements ClientResponseObserver<ReqT, R> { + + private final CompletableFuture<R> future; + private Runnable onReadyHandler; + + public static <ReqT, R> BinaryFileUploadStreamObserver<ReqT, R> create(CompletableFuture<R> future, Runnable onReadyHandler) { + BinaryFileUploadStreamObserver<ReqT, R> instance = create(future); + instance.onReadyHandler = onReadyHandler; + return instance; + } + + public static <ReqT, R> BinaryFileUploadStreamObserver<ReqT, R> create(CompletableFuture<R> future) { + return new BinaryFileUploadStreamObserver<>(future); + } + + @Getter + private R response; + + /* + requestStream is CallStreamObserver - received from Grpc-framework. onReadyHandler calls onNext on this observer + */ + @Override + public void beforeStart(ClientCallStreamObserver<ReqT> requestStreamObserver) { + requestStreamObserver.setOnReadyHandler(onReadyHandler); + } + + @Override + public void onNext(R response) { + this.response = response; + } + + @Override + public void onError(Throwable t) { + LOG.error("Error on uploading file. Completing Future.", t); + future.completeExceptionally(t); + } + + // will it even get called? requestStreamObserver.onCompleted() would need to be called first + @Override + public void onCompleted() { + LOG.debug("Complete future..."); + future.complete(response); + } + +} \ No newline at end of file diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/GrpcFileUploadUtils.java b/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/GrpcFileUploadUtils.java new file mode 100644 index 000000000..56b96476d --- /dev/null +++ b/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/GrpcFileUploadUtils.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2023 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.common.binaryfile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.apache.commons.io.IOUtils; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import io.grpc.stub.CallStreamObserver; +import io.grpc.stub.StreamObserver; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class GrpcFileUploadUtils { + + static final int CHUNK_SIZE = 4 * 1024; + + /* + * Q = Request Type; S = Response Type + */ + public static <Q, S> FileSender<Q, S> createSender(BiFunction<byte[], Integer, Q> chunkBuilder, InputStream inputStream, + Function<StreamObserver<S>, CallStreamObserver<Q>> reqObserverBuilder) { + return createSender(chunkBuilder, inputStream, reqObserverBuilder, true); + } + + public static <Q, S> FileSender<Q, S> createSender(BiFunction<byte[], Integer, Q> chunkBuilder, InputStream inputStream, + Function<StreamObserver<S>, CallStreamObserver<Q>> reqObserverBuilder, boolean completeOnFileSent) { + return new FileSender<>(chunkBuilder, reqObserverBuilder, inputStream, completeOnFileSent); + } + + public static class FileSender<Q, S> { + private final BiFunction<byte[], Integer, Q> chunkBuilder; + private final InputStream inputStream; + + @Getter + private final CompletableFuture<S> resultFuture = new CompletableFuture<>(); + private final Function<StreamObserver<S>, CallStreamObserver<Q>> reqObserverBuilder; + private CallStreamObserver<Q> requestObserver; + + private Optional<Q> metaData = Optional.empty(); + private final AtomicBoolean metaDataSent = new AtomicBoolean(false); + private final AtomicBoolean done = new AtomicBoolean(false); + + private final StreamReader streamReader; + private final boolean completeOnFileSent; + + FileSender(BiFunction<byte[], Integer, Q> chunkBuilder, Function<StreamObserver<S>, CallStreamObserver<Q>> reqObserverBuilder, + InputStream inputStream, boolean completeOnFileSent) { + this.chunkBuilder = chunkBuilder; + this.inputStream = inputStream; + this.reqObserverBuilder = reqObserverBuilder; + this.completeOnFileSent = completeOnFileSent; + + this.streamReader = new StreamReader(this.inputStream); + } + + public FileSender<Q, S> withMetaData(@NonNull Q metaData) { + this.metaData = Optional.of(metaData); + return this; + } + + public FileSender<Q, S> send(Consumer<Runnable> registerOnReadyHandler) { + LOG.debug("Start sending File."); + + registerOnReadyHandler.accept(this::sendNext); + requestObserver = reqObserverBuilder.apply(null); + + return this; + } + + public FileSender<Q, S> send() { + LOG.debug("Start sending File."); + + // this responseObserver registers also onReadyHandler + var responseObserver = BinaryFileUploadStreamObserver.create(resultFuture, this::sendNext); + requestObserver = reqObserverBuilder.apply(responseObserver); + + return this; + } + + public void cancelOnTimeout() { + LOG.warn("File transfer canceled on timeout"); + resultFuture.cancel(true); + requestObserver.onError(new TechnicalException("Timeout on waiting for upload.")); + closeStreams(); + } + + public void cancelOnError(Throwable t) { + LOG.error("File tranfer canceled on error.", t); + resultFuture.cancel(true); + requestObserver.onError(t); + closeStreams(); + } + + void sendNext() { + if (!done.get()) { + waitForOberver(); + sendMetaData(); + do { + LOG.debug("Sending next chunk."); + sendNextChunk(); + } while (!done.get() && isReady()); + LOG.debug("Finished or waiting to become ready."); + } + } + + private boolean isReady() { + return requestObserver.isReady(); + } + + private void waitForOberver() { + synchronized (this) { + while (Objects.isNull(requestObserver)) { + try { + LOG.debug("wait for observer"); + wait(300); + } catch (InterruptedException e) { + LOG.error("Error on waiting for request Observer.", e); + Thread.currentThread().interrupt(); + } + } + } + + } + + void sendNextChunk() { + byte[] contentToSend = streamReader.getNextData(); + + if (streamReader.getLastReadSize() > 0) { + sendChunk(contentToSend, streamReader.getLastReadSize()); + } else { + endTransfer(); + } + } + + private void endTransfer() { + if (completeOnFileSent) { + requestObserver.onCompleted(); + } else { + sendEndOfFile(); + resultFuture.complete(null); + } + done.set(true); + LOG.debug("File Transfer done."); + closeStreams(); + + } + + private void sendEndOfFile() { + sendChunk(new byte[0], streamReader.getLastReadSize()); + } + + void closeStreams() { + LOG.debug("Closing streams"); + streamReader.close(); + } + + void sendChunk(byte[] content, int length) { + LOG.debug("Sending {} byte Data.", length); + var chunk = chunkBuilder.apply(content, length); + requestObserver.onNext(chunk); + } + + byte[] readFromStream() { + try { + return inputStream.readNBytes(CHUNK_SIZE); + } catch (IOException e) { + throw new TechnicalException("Error on sending a single chunk", e); + } + } + + void sendMetaData() { + metaData.filter(md -> !metaDataSent.get()).ifPresent(this::doSendMetaData); + } + + private void doSendMetaData(Q metadata) { + LOG.debug("Sending Metadata."); + requestObserver.onNext(metadata); + metaDataSent.set(true); + } + + void checkForEndOfStream(long sentSize) { + if (sentSize < CHUNK_SIZE) { + LOG.debug("File Transfer done. Closing stream."); + IOUtils.closeQuietly(inputStream); + requestObserver.onCompleted(); + done.set(true); + } else { + LOG.debug("File Transfer not jet done - need to tranfer another chunk."); + } + } + + @RequiredArgsConstructor + private class StreamReader { + private final InputStream inStream; + private final byte[] buffer = new byte[CHUNK_SIZE]; + @Getter + private int lastReadSize = 0; + @Getter + private final AtomicBoolean done = new AtomicBoolean(false); + + byte[] getNextData() { + readNext(); + return buffer; + } + + void close() { + IOUtils.closeQuietly(inStream); + } + + void readNext() { + try { + lastReadSize = inStream.read(buffer, 0, CHUNK_SIZE); + } catch (IOException e) { + throw new TechnicalException("Error on reading a single chunk", e); + } + } + } + } + +} \ No newline at end of file -- GitLab From b85abcac73d1ee68c0916f7eff51f674ac554318 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Mon, 31 Mar 2025 09:14:43 +0200 Subject: [PATCH 04/18] OZG-7573 OZG-7991 Call on*()-methods on requestObserver --- .../ozgcloud/vorgang/vorgang/redirect/EingangForwarder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5136f4285..e3dbaee2e 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 @@ -47,9 +47,9 @@ class EingangForwarder { .thenCompose(ignored -> sendRepresentations(representations)) .whenComplete((result, ex) -> { if (ex != null) { - responseObserver.onError(ex); + requestObserver.onError(ex); } else { - responseObserver.onCompleted(); + requestObserver.onCompleted(); } }) ); -- GitLab From ac2444a7644d3bc159373e98b413f88eca20b490 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Tue, 1 Apr 2025 13:11:05 +0200 Subject: [PATCH 05/18] Revert "OZG-7573 OZG-7991 Copy GrpcFileUploadUtils from common-lib (only temporarily)" This reverts commit adfe531b216b2fb9edf5a4be1bd6ae80838130f6. --- .../BinaryFileUploadStreamObserver.java | 81 ------ .../binaryfile/GrpcFileUploadUtils.java | 257 ------------------ 2 files changed, 338 deletions(-) delete mode 100644 vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileUploadStreamObserver.java delete mode 100644 vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/GrpcFileUploadUtils.java diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileUploadStreamObserver.java b/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileUploadStreamObserver.java deleted file mode 100644 index 44635a997..000000000 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/BinaryFileUploadStreamObserver.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2023 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.common.binaryfile; - -import java.util.concurrent.CompletableFuture; - -import io.grpc.stub.ClientCallStreamObserver; -import io.grpc.stub.ClientResponseObserver; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; - -@Log4j2 -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class BinaryFileUploadStreamObserver<ReqT, R> implements ClientResponseObserver<ReqT, R> { - - private final CompletableFuture<R> future; - private Runnable onReadyHandler; - - public static <ReqT, R> BinaryFileUploadStreamObserver<ReqT, R> create(CompletableFuture<R> future, Runnable onReadyHandler) { - BinaryFileUploadStreamObserver<ReqT, R> instance = create(future); - instance.onReadyHandler = onReadyHandler; - return instance; - } - - public static <ReqT, R> BinaryFileUploadStreamObserver<ReqT, R> create(CompletableFuture<R> future) { - return new BinaryFileUploadStreamObserver<>(future); - } - - @Getter - private R response; - - /* - requestStream is CallStreamObserver - received from Grpc-framework. onReadyHandler calls onNext on this observer - */ - @Override - public void beforeStart(ClientCallStreamObserver<ReqT> requestStreamObserver) { - requestStreamObserver.setOnReadyHandler(onReadyHandler); - } - - @Override - public void onNext(R response) { - this.response = response; - } - - @Override - public void onError(Throwable t) { - LOG.error("Error on uploading file. Completing Future.", t); - future.completeExceptionally(t); - } - - // will it even get called? requestStreamObserver.onCompleted() would need to be called first - @Override - public void onCompleted() { - LOG.debug("Complete future..."); - future.complete(response); - } - -} \ No newline at end of file diff --git a/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/GrpcFileUploadUtils.java b/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/GrpcFileUploadUtils.java deleted file mode 100644 index 56b96476d..000000000 --- a/vorgang-manager-server/src/main/java/de/ozgcloud/common/binaryfile/GrpcFileUploadUtils.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (C) 2023 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.common.binaryfile; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; - -import org.apache.commons.io.IOUtils; - -import de.ozgcloud.common.errorhandling.TechnicalException; -import io.grpc.stub.CallStreamObserver; -import io.grpc.stub.StreamObserver; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; - -@Log4j2 -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class GrpcFileUploadUtils { - - static final int CHUNK_SIZE = 4 * 1024; - - /* - * Q = Request Type; S = Response Type - */ - public static <Q, S> FileSender<Q, S> createSender(BiFunction<byte[], Integer, Q> chunkBuilder, InputStream inputStream, - Function<StreamObserver<S>, CallStreamObserver<Q>> reqObserverBuilder) { - return createSender(chunkBuilder, inputStream, reqObserverBuilder, true); - } - - public static <Q, S> FileSender<Q, S> createSender(BiFunction<byte[], Integer, Q> chunkBuilder, InputStream inputStream, - Function<StreamObserver<S>, CallStreamObserver<Q>> reqObserverBuilder, boolean completeOnFileSent) { - return new FileSender<>(chunkBuilder, reqObserverBuilder, inputStream, completeOnFileSent); - } - - public static class FileSender<Q, S> { - private final BiFunction<byte[], Integer, Q> chunkBuilder; - private final InputStream inputStream; - - @Getter - private final CompletableFuture<S> resultFuture = new CompletableFuture<>(); - private final Function<StreamObserver<S>, CallStreamObserver<Q>> reqObserverBuilder; - private CallStreamObserver<Q> requestObserver; - - private Optional<Q> metaData = Optional.empty(); - private final AtomicBoolean metaDataSent = new AtomicBoolean(false); - private final AtomicBoolean done = new AtomicBoolean(false); - - private final StreamReader streamReader; - private final boolean completeOnFileSent; - - FileSender(BiFunction<byte[], Integer, Q> chunkBuilder, Function<StreamObserver<S>, CallStreamObserver<Q>> reqObserverBuilder, - InputStream inputStream, boolean completeOnFileSent) { - this.chunkBuilder = chunkBuilder; - this.inputStream = inputStream; - this.reqObserverBuilder = reqObserverBuilder; - this.completeOnFileSent = completeOnFileSent; - - this.streamReader = new StreamReader(this.inputStream); - } - - public FileSender<Q, S> withMetaData(@NonNull Q metaData) { - this.metaData = Optional.of(metaData); - return this; - } - - public FileSender<Q, S> send(Consumer<Runnable> registerOnReadyHandler) { - LOG.debug("Start sending File."); - - registerOnReadyHandler.accept(this::sendNext); - requestObserver = reqObserverBuilder.apply(null); - - return this; - } - - public FileSender<Q, S> send() { - LOG.debug("Start sending File."); - - // this responseObserver registers also onReadyHandler - var responseObserver = BinaryFileUploadStreamObserver.create(resultFuture, this::sendNext); - requestObserver = reqObserverBuilder.apply(responseObserver); - - return this; - } - - public void cancelOnTimeout() { - LOG.warn("File transfer canceled on timeout"); - resultFuture.cancel(true); - requestObserver.onError(new TechnicalException("Timeout on waiting for upload.")); - closeStreams(); - } - - public void cancelOnError(Throwable t) { - LOG.error("File tranfer canceled on error.", t); - resultFuture.cancel(true); - requestObserver.onError(t); - closeStreams(); - } - - void sendNext() { - if (!done.get()) { - waitForOberver(); - sendMetaData(); - do { - LOG.debug("Sending next chunk."); - sendNextChunk(); - } while (!done.get() && isReady()); - LOG.debug("Finished or waiting to become ready."); - } - } - - private boolean isReady() { - return requestObserver.isReady(); - } - - private void waitForOberver() { - synchronized (this) { - while (Objects.isNull(requestObserver)) { - try { - LOG.debug("wait for observer"); - wait(300); - } catch (InterruptedException e) { - LOG.error("Error on waiting for request Observer.", e); - Thread.currentThread().interrupt(); - } - } - } - - } - - void sendNextChunk() { - byte[] contentToSend = streamReader.getNextData(); - - if (streamReader.getLastReadSize() > 0) { - sendChunk(contentToSend, streamReader.getLastReadSize()); - } else { - endTransfer(); - } - } - - private void endTransfer() { - if (completeOnFileSent) { - requestObserver.onCompleted(); - } else { - sendEndOfFile(); - resultFuture.complete(null); - } - done.set(true); - LOG.debug("File Transfer done."); - closeStreams(); - - } - - private void sendEndOfFile() { - sendChunk(new byte[0], streamReader.getLastReadSize()); - } - - void closeStreams() { - LOG.debug("Closing streams"); - streamReader.close(); - } - - void sendChunk(byte[] content, int length) { - LOG.debug("Sending {} byte Data.", length); - var chunk = chunkBuilder.apply(content, length); - requestObserver.onNext(chunk); - } - - byte[] readFromStream() { - try { - return inputStream.readNBytes(CHUNK_SIZE); - } catch (IOException e) { - throw new TechnicalException("Error on sending a single chunk", e); - } - } - - void sendMetaData() { - metaData.filter(md -> !metaDataSent.get()).ifPresent(this::doSendMetaData); - } - - private void doSendMetaData(Q metadata) { - LOG.debug("Sending Metadata."); - requestObserver.onNext(metadata); - metaDataSent.set(true); - } - - void checkForEndOfStream(long sentSize) { - if (sentSize < CHUNK_SIZE) { - LOG.debug("File Transfer done. Closing stream."); - IOUtils.closeQuietly(inputStream); - requestObserver.onCompleted(); - done.set(true); - } else { - LOG.debug("File Transfer not jet done - need to tranfer another chunk."); - } - } - - @RequiredArgsConstructor - private class StreamReader { - private final InputStream inStream; - private final byte[] buffer = new byte[CHUNK_SIZE]; - @Getter - private int lastReadSize = 0; - @Getter - private final AtomicBoolean done = new AtomicBoolean(false); - - byte[] getNextData() { - readNext(); - return buffer; - } - - void close() { - IOUtils.closeQuietly(inStream); - } - - void readNext() { - try { - lastReadSize = inStream.read(buffer, 0, CHUNK_SIZE); - } catch (IOException e) { - throw new TechnicalException("Error on reading a single chunk", e); - } - } - } - } - -} \ No newline at end of file -- GitLab From 692dc220d2e46bffdd18504064e28378a88bdec6 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Tue, 1 Apr 2025 13:16:27 +0200 Subject: [PATCH 06/18] OZG-7573 OZG-7991 Use StreamingFileSender from common-lib --- vorgang-manager-server/pom.xml | 2 +- .../vorgang/redirect/EingangForwarder.java | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/vorgang-manager-server/pom.xml b/vorgang-manager-server/pom.xml index 8399fdf8c..b95fe9a9e 100644 --- a/vorgang-manager-server/pom.xml +++ b/vorgang-manager-server/pom.xml @@ -51,7 +51,7 @@ <spring-boot.build-image.imageName>docker.ozg-sh.de/vorgang-manager:build-latest</spring-boot.build-image.imageName> <zufi-manager-interface.version>1.6.0</zufi-manager-interface.version> - <common-lib.version>4.12.0</common-lib.version> + <common-lib.version>4.13.0-OZG-7573-files-weiterleitung-bug-SNAPSHOT</common-lib.version> <user-manager-interface.version>2.12.0</user-manager-interface.version> <processor-manager.version>0.5.0</processor-manager.version> <nachrichten-manager.version>2.19.0</nachrichten-manager.version> 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 e3dbaee2e..2909ed3b5 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 @@ -11,6 +11,7 @@ import java.util.function.Function; import com.google.protobuf.ByteString; import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; +import de.ozgcloud.common.binaryfile.StreamingFileSender; import de.ozgcloud.eingang.forwarder.RouteForwardingServiceGrpc; import de.ozgcloud.eingang.forwarding.GrpcAttachment; import de.ozgcloud.eingang.forwarding.GrpcFileContent; @@ -93,15 +94,14 @@ class EingangForwarder { private CompletableFuture<GrpcRouteForwardingResponse> sendAttachmentFile(String groupName, IncomingFile file) { var fileContentStream = fileService.getUploadedFileStream(file.getId()); - var sender = createAttachmentFileSender(groupName, file, fileContentStream).send(responseObserver::registerOnReadyHandler); + var sender = createAttachmentFileSender(groupName, file, fileContentStream).send(); return sender.getResultFuture(); } - GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender(String groupName, + StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender(String groupName, IncomingFile file, InputStream fileContentStream) { - return createSenderWithoutMetadata(this::buildAttachmentChunk, fileContentStream) - .withMetaData(buildGrpcAttachmentFile(groupName, file)); + return createSenderWithoutMetadata(this::buildAttachmentChunk, fileContentStream).withMetaData(buildGrpcAttachmentFile(groupName, file)); } GrpcRouteForwardingRequest buildAttachmentChunk(byte[] chunk, int length) { @@ -136,11 +136,11 @@ class EingangForwarder { private CompletableFuture<GrpcRouteForwardingResponse> sendRepresentationFile(IncomingFile file) { var fileContentStream = fileService.getUploadedFileStream(file.getId()); - var sender = createRepresentationFileSender(file, fileContentStream).send(responseObserver::registerOnReadyHandler); + var sender = createRepresentationFileSender(file, fileContentStream).send(); return sender.getResultFuture(); } - GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender(IncomingFile file, + StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender(IncomingFile file, InputStream fileContentStream) { return createSenderWithoutMetadata(this::buildRepresentationChunk, fileContentStream).withMetaData(buildGrpcRepresentationFile(file)); } @@ -171,9 +171,9 @@ class EingangForwarder { return fileContentBuilder.build(); } - GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata( + StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createSenderWithoutMetadata( BiFunction<byte[], Integer, GrpcRouteForwardingRequest> chunkBuilder, InputStream fileContentStream) { - return GrpcFileUploadUtils.createSender(chunkBuilder, fileContentStream, response -> requestObserver, false); + return GrpcFileUploadUtils.createStreamSharingSender(chunkBuilder, fileContentStream, requestObserver, responseObserver::registerOnReadyHandler); } @RequiredArgsConstructor -- GitLab From d6a5e58a53e309f33aceb3f8a806750306ae6a7a Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Wed, 2 Apr 2025 17:57:20 +0200 Subject: [PATCH 07/18] OZG-7573 OZG-7991 Stop transfers in case of error --- .../vorgang/redirect/EingangForwarder.java | 46 +- .../redirect/ForwardingRemoteService.java | 5 +- .../vorgang/IncomingFileGroupTestFactory.java | 8 + .../redirect/EingangForwarderTest.java | 689 ++++++++++++++++++ .../redirect/ForwardingRemoteServiceTest.java | 123 ++++ 5 files changed, 858 insertions(+), 13 deletions(-) create mode 100644 vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/EingangForwarderTest.java create mode 100644 vorgang-manager-server/src/test/java/de/ozgcloud/vorgang/vorgang/redirect/ForwardingRemoteServiceTest.java 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 2909ed3b5..5fa1cc80c 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 371374eca..b76dde08c 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 f95a61236..4075d2c3b 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 000000000..78f1837c3 --- /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 000000000..3df3152b6 --- /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); + } + } +} -- GitLab From 30b208922834e23acd1d09d0a57d28b73ac493c6 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Thu, 3 Apr 2025 13:59:22 +0200 Subject: [PATCH 08/18] OZG-7573 OZG-7991 Complete future exceptionally on exception --- .../vorgang/redirect/EingangForwarder.java | 31 +- .../redirect/ForwardingRemoteService.java | 5 +- .../redirect/EingangForwarderTest.java | 354 +++++++++++++++++- .../redirect/ForwardingRemoteServiceTest.java | 100 ++++- 4 files changed, 470 insertions(+), 20 deletions(-) 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 5fa1cc80c..f34f7e0ba 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,10 +26,11 @@ import de.ozgcloud.vorgang.vorgang.IncomingFileGroup; import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; import io.grpc.stub.ClientCallStreamObserver; import io.grpc.stub.ClientResponseObserver; +import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) class EingangForwarder { private final RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; @@ -43,6 +44,11 @@ class EingangForwarder { @Getter private CompletableFuture<Void> forwardFuture; + public static EingangForwarder create(RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub, FileService fileService, + IncomingFileMapper incomingFileMapper) { + return new EingangForwarder(serviceStub, fileService, incomingFileMapper); + } + public EingangForwarder forward(GrpcRouteForwarding grpcRouteForwarding, List<IncomingFileGroup> attachments, List<IncomingFile> representations) { @@ -104,10 +110,8 @@ class EingangForwarder { CompletableFuture<GrpcRouteForwardingResponse> sendAttachmentFile(String groupName, IncomingFile file) { var fileContentStream = fileService.getUploadedFileStream(file.getId()); - var sender = createAttachmentFileSender(groupName, file, fileContentStream).send(); - var future = sender.getResultFuture(); - configureToCancelIfForwardFutureCompleted(future); - return future; + var future = createAttachmentFileSender(groupName, file, fileContentStream).send().getResultFuture(); + return configureToCancelIfForwardFutureCompleted(future); } StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender(String groupName, @@ -149,10 +153,8 @@ class EingangForwarder { CompletableFuture<GrpcRouteForwardingResponse> sendRepresentationFile(IncomingFile file) { var fileContentStream = fileService.getUploadedFileStream(file.getId()); - var sender = createRepresentationFileSender(file, fileContentStream).send(); - var future = sender.getResultFuture(); - configureToCancelIfForwardFutureCompleted(future); - return future; + var future = createRepresentationFileSender(file, fileContentStream).send().getResultFuture(); + return configureToCancelIfForwardFutureCompleted(future); } StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender(IncomingFile file, @@ -176,12 +178,13 @@ class EingangForwarder { .build(); } - void configureToCancelIfForwardFutureCompleted(CompletableFuture<GrpcRouteForwardingResponse> future) { + CompletableFuture<GrpcRouteForwardingResponse> configureToCancelIfForwardFutureCompleted(CompletableFuture<GrpcRouteForwardingResponse> future) { forwardFuture.whenComplete((result, ex) -> { if (forwardFuture.isDone() && !future.isDone()) { future.cancel(true); } }); + return future; } GrpcFileContent buildGrpcFileContent(byte[] chunk, int length) { @@ -251,11 +254,9 @@ class EingangForwarder { @Override public void run() { - while (!done.get() && requestStream.isReady()) { - var runnable = onReadyHandler.get(); - if (runnable != null) { - runnable.run(); - } + var delegate = onReadyHandler.get(); + if (delegate != null && !done.get() && requestStream.isReady()) { + delegate.run(); } } } 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 b76dde08c..eef4e2ca0 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 @@ -53,7 +53,7 @@ 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(), + var responseFuture = EingangForwarder.create(serviceStub, fileService, incomingFileMapper).forward(grpcRouteForwarding, eingang.getAttachments(), eingang.getRepresentations()).getForwardFuture(); waitForCompletion(responseFuture); } @@ -63,10 +63,13 @@ class ForwardingRemoteService { responseFuture.get(TIMEOUT_MINUTES, TimeUnit.MINUTES); } catch (InterruptedException e) { Thread.currentThread().interrupt(); + responseFuture.completeExceptionally(e); throw new TechnicalException("Waiting for finishing file upload was interrupted.", e); } catch (ExecutionException e) { + responseFuture.completeExceptionally(e); throw new TechnicalException("Error on uploading file content.", e); } catch (TimeoutException e) { + responseFuture.completeExceptionally(e); throw new TechnicalException("Timeout on uploading file content.", e); } } 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 index 78f1837c3..0361376b0 100644 --- 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 @@ -10,6 +10,7 @@ import java.io.InputStream; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -40,6 +41,7 @@ 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.DelegatingOnReadyHandler; import de.ozgcloud.vorgang.vorgang.redirect.EingangForwarder.ForwardingResponseObserver; import io.grpc.stub.ClientCallStreamObserver; @@ -309,7 +311,7 @@ class EingangForwarderTest { doReturn(fileSender).when(forwarder).createAttachmentFileSender(any(), any(), any()); doReturn(fileSender).when(fileSender).send(); when(fileSender.getResultFuture()).thenReturn(resultFuture); - doNothing().when(forwarder).configureToCancelIfForwardFutureCompleted(any()); + doReturn(resultFuture).when(forwarder).configureToCancelIfForwardFutureCompleted(any()); } @Test @@ -479,6 +481,110 @@ class EingangForwarderTest { assertThat(returned).isNotNull(); } + + @Test + void shouldInitiallySendOnlyFirstFile() { + forwarder.sendRepresentations(representations); + + verify(forwarder).sendRepresentationFile(FILE); + verify(forwarder, times(1)).sendRepresentationFile(any()); + } + + @Test + void shouldSendSecondFileAfterFirstFutureCompleted() { + forwarder.sendRepresentations(representations); + + future.complete(GrpcRouteForwardingResponse.newBuilder().build()); + + verify(forwarder).sendRepresentationFile(FILE2); + verify(forwarder, times(2)).sendRepresentationFile(any()); + } + + @Test + void shouldReturnedFutureBeInitiallyIncomplete() { + var returned = forwarder.sendRepresentations(representations); + + assertThat(returned.isDone()).isFalse(); + } + + @Test + void shouldReturnedFutureBeIncompleteAfterSendingFirstFile() { + var returned = forwarder.sendRepresentations(representations); + + future.complete(GrpcRouteForwardingResponse.newBuilder().build()); + + assertThat(returned.isDone()).isFalse(); + } + + @Test + void shouldReturnedFutureBeDoneAfterSendingAllFiles() { + var returned = forwarder.sendRepresentations(representations); + + future.complete(GrpcRouteForwardingResponse.newBuilder().build()); + future2.complete(GrpcRouteForwardingResponse.newBuilder().build()); + + assertThat(returned.isDone()).isTrue(); + } + } + + @Nested + class TestSendRepresentationFile { + + @Mock + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; + private final CompletableFuture<GrpcRouteForwardingResponse> resultFuture = new CompletableFuture<>(); + @Mock + private InputStream fileContentStream; + + private final IncomingFile file = IncomingFileTestFactory.create(); + + @BeforeEach + void init() { + when(fileService.getUploadedFileStream(any())).thenReturn(fileContentStream); + doReturn(fileSender).when(forwarder).createRepresentationFileSender(any(), any()); + doReturn(fileSender).when(fileSender).send(); + when(fileSender.getResultFuture()).thenReturn(resultFuture); + doReturn(resultFuture).when(forwarder).configureToCancelIfForwardFutureCompleted(any()); + } + + @Test + void shouldGetUploadFileStream() { + sendRepresentationFile(); + + verify(fileService).getUploadedFileStream(IncomingFileTestFactory.ID); + } + + @Test + void shouldCreateRepresentationFileSender() { + sendRepresentationFile(); + + verify(forwarder).createRepresentationFileSender(file, fileContentStream); + } + + @Test + void shouldSend() { + sendRepresentationFile(); + + verify(fileSender).send(); + } + + @Test + void shouldConfigureFutureToCancelIfForwardFutureCompleted() { + sendRepresentationFile(); + + verify(forwarder).configureToCancelIfForwardFutureCompleted(resultFuture); + } + + @Test + void shouldReturnResultFuture() { + var returned = sendRepresentationFile(); + + assertThat(returned).isSameAs(resultFuture); + } + + private CompletableFuture<GrpcRouteForwardingResponse> sendRepresentationFile() { + return forwarder.sendRepresentationFile(file); + } } @Nested @@ -589,6 +695,55 @@ class EingangForwarderTest { } } + @Nested + class TestConfigureToCancelIfForwardFutureCompleted { + + private final CompletableFuture<Void> forwardFuture = new CompletableFuture<>(); + private final CompletableFuture<GrpcRouteForwardingResponse> future = new CompletableFuture<>(); + + @BeforeEach + void init() { + setForwardFutureInForwarder(forwardFuture); + } + + @Test + void shouldCancelFutureWhenForwardFutureCompleted() { + forwarder.configureToCancelIfForwardFutureCompleted(future); + + forwardFuture.complete(null); + + assertThat(future.isCancelled()).isTrue(); + } + + @Test + void shouldCancelFutureWhenForwardFutureWasCancelled() { + forwarder.configureToCancelIfForwardFutureCompleted(future); + + forwardFuture.cancel(true); + + assertThat(future.isCancelled()).isTrue(); + } + + @Test + void shouldCancelFutureWhenForwardFutureCompletedExceptionally() { + forwarder.configureToCancelIfForwardFutureCompleted(future); + + forwardFuture.completeExceptionally(new RuntimeException("Forced failure")); + + assertThat(future.isCancelled()).isTrue(); + } + + @Test + void shouldNotCancelFutureIfItIsDone() { + forwarder.configureToCancelIfForwardFutureCompleted(future); + future.complete(GrpcRouteForwardingResponse.getDefaultInstance()); + + forwardFuture.complete(null); + + assertThat(future.isCancelled()).isFalse(); + } + } + @Nested class TestBuildGrpcFileContent { @@ -675,6 +830,199 @@ class EingangForwarderTest { } } + @Nested + class TestForwardingResponseObserver { + + @Mock + private CompletableFuture<GrpcRouteForwardingResponse> future; + @Mock + private DelegatingOnReadyHandler onReadyHandler; + @Mock + private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStream; + private final GrpcRouteForwardingResponse response = GrpcRouteForwardingResponse.getDefaultInstance(); + @InjectMocks + private ForwardingResponseObserver observer; + + @Nested + class TestBeforeStart { + + @Test + void shouldCreateOnReadyHandler() { + observer.beforeStart(requestStream); + + assertThat(getOnReadyHandlerFromObserver()).isNotNull(); + } + + @Test + void shouldSetOnReadyHandler() { + observer.beforeStart(requestStream); + + verify(requestStream).setOnReadyHandler(getOnReadyHandlerFromObserver()); + } + } + + @Nested + class TestOnNext { + + @Test + void shouldSetResponse() { + observer.onNext(response); + + assertThat(getResponseFromObserver()).isSameAs(response); + } + } + + @Nested + class TestOnError { + + private final Throwable error = new RuntimeException("Error when forwarding"); + + @BeforeEach + void init() { + setOnReadyHandlerInObserver(); + } + + @Test + void shouldStopOnReadyHandler() { + observer.onError(error); + + verify(onReadyHandler).stop(); + } + + @Test + void shouldCompleteFutureExceptionally() { + observer.onError(error); + + verify(future).completeExceptionally(error); + } + } + + @Nested + class TestOnCompleted { + + @BeforeEach + void init() { + setOnReadyHandlerInObserver(); + } + + @Test + void shouldStopOnReadyHandler() { + observer.onCompleted(); + + verify(onReadyHandler).stop(); + } + + @Test + void shouldCompleteFutureWithResponse() { + observer.onNext(response); + + observer.onCompleted(); + + verify(future).complete(response); + } + } + + @Nested + class TestRegisterOnReadyHandler { + + @Mock + private Runnable delegate; + + @BeforeEach + void init() { + setOnReadyHandlerInObserver(); + } + + @Test + void shouldSetDelegateInOnReadyHandler() { + observer.registerOnReadyHandler(delegate); + + verify(onReadyHandler).setDelegate(delegate); + } + } + + private DelegatingOnReadyHandler getOnReadyHandlerFromObserver() { + return ReflectionTestUtils.getField(observer, "onReadyHandler", DelegatingOnReadyHandler.class); + } + + private void setOnReadyHandlerInObserver() { + ReflectionTestUtils.setField(observer, "onReadyHandler", onReadyHandler); + } + + private GrpcRouteForwardingResponse getResponseFromObserver() { + return ReflectionTestUtils.getField(observer, "response", GrpcRouteForwardingResponse.class); + } + } + + @Nested + class TestDelegatingOnReadyHandler { + + @Mock + private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStream; + @InjectMocks + private DelegatingOnReadyHandler onReadyHandler; + + @Test + void shouldDoneBeInitiallyFalse() { + assertThat(getDoneFromOnReadyHandler()).isFalse(); + } + + @Nested + class TestStop { + + @Test + void shouldSetDoneToTrue() { + onReadyHandler.stop(); + + assertThat(getDoneFromOnReadyHandler()).isTrue(); + } + } + + @Nested + class TestRun { + + @Mock + private Runnable delegate; + + @BeforeEach + void init() { + onReadyHandler.setDelegate(delegate); + } + + @Test + void shouldNotRunDelegateIfDone() { + onReadyHandler.stop(); + lenient().when(requestStream.isReady()).thenReturn(true); + + onReadyHandler.run(); + + verify(delegate, never()).run(); + } + + @Test + void shouldNotRunDelegateIfNotReady() { + when(requestStream.isReady()).thenReturn(false); + + onReadyHandler.run(); + + verify(delegate, never()).run(); + } + + @Test + void shouldRunDelegateIfNotDoneAndReady() { + when(requestStream.isReady()).thenReturn(true); + + onReadyHandler.run(); + + verify(delegate).run(); + } + } + + private boolean getDoneFromOnReadyHandler() { + return ReflectionTestUtils.getField(onReadyHandler, "done", AtomicBoolean.class).get(); + } + } + private void setResponseObserverInForwarder(ForwardingResponseObserver responseObserver) { ReflectionTestUtils.setField(forwarder, "responseObserver", responseObserver); } @@ -686,4 +1034,8 @@ class EingangForwarderTest { private void setRequestObserverInForwarder(ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver) { ReflectionTestUtils.setField(forwarder, "requestObserver", requestObserver); } + + private void setForwardFutureInForwarder(CompletableFuture<Void> future) { + ReflectionTestUtils.setField(forwarder, "forwardFuture", future); + } } 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 index 3df3152b6..e883ab782 100644 --- 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 @@ -5,23 +5,29 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.util.List; 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.AfterEach; 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.MockedStatic; import org.mockito.Spy; import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.eingang.forwarder.RouteForwardingServiceGrpc; +import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding; import de.ozgcloud.vorgang.files.FileService; +import de.ozgcloud.vorgang.vorgang.EingangTestFactory; import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; import de.ozgcloud.vorgang.vorgang.VorgangService; +import de.ozgcloud.vorgang.vorgang.VorgangTestFactory; import lombok.SneakyThrows; class ForwardingRemoteServiceTest { @@ -40,6 +46,61 @@ class ForwardingRemoteServiceTest { @Spy private ForwardingRemoteService service; + @Nested + class TestForward { + + private final ForwardingRequest request = ForwardingRequestTestFactory.create(); + private final GrpcRouteForwarding grpcRouteForwarding = GrpcRouteForwardingTestFactory.create(); + private final CompletableFuture<Void> responseFuture = new CompletableFuture<>(); + private MockedStatic<EingangForwarder> eingangForwarderMockedStatic; + @Mock + private EingangForwarder eingangForwarder; + + @BeforeEach + void init() { + when(vorgangService.getById(any())).thenReturn(VorgangTestFactory.create()); + eingangForwarderMockedStatic = mockStatic(EingangForwarder.class); + eingangForwarderMockedStatic.when(() -> EingangForwarder.create(any(), any(), any())).thenReturn(eingangForwarder); + when(eingangForwarder.forward(any(), any(), any())).thenReturn(eingangForwarder); + when(eingangForwarder.getForwardFuture()).thenReturn(responseFuture); + when(forwardingRequestMapper.toGrpcRouteForwarding(any(), any())).thenReturn(grpcRouteForwarding); + doNothing().when(service).waitForCompletion(any()); + } + + @AfterEach + void teardown() { + eingangForwarderMockedStatic.close(); + } + + @Test + void shouldGetVorgang() { + service.forward(request); + + verify(vorgangService).getById(VorgangTestFactory.ID); + } + + @Test + void shouldMapToGrpcRouteForwarding() { + service.forward(request); + + verify(forwardingRequestMapper).toGrpcRouteForwarding(request, VorgangTestFactory.EINGANG); + } + + @Test + void shouldForward() { + service.forward(request); + + verify(eingangForwarder).forward(grpcRouteForwarding, List.of(EingangTestFactory.ATTACHMENT), List.of(EingangTestFactory.REPRESENTATION)); + } + + @Test + void shouldWaitForCompletion() { + service.forward(request); + + verify(service).waitForCompletion(responseFuture); + } + } + @Nested class TestWaitForCompletion { @@ -67,7 +128,7 @@ class ForwardingRemoteServiceTest { @Test void shouldThrowTechnicalException() { - assertThrows(TechnicalException.class, de.ozgcloud.vorgang.vorgang.redirect.ForwardingRemoteServiceTest.TestWaitForCompletion.this::waitForCompletion); + assertThrows(TechnicalException.class, TestWaitForCompletion.this::waitForCompletion); } @Test @@ -80,6 +141,17 @@ class ForwardingRemoteServiceTest { assertThat(Thread.currentThread().isInterrupted()).isTrue(); } + + @Test + void shouldCompleteFutureExceptionally() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + verify(future).completeExceptionally(exception); + } } @Nested @@ -95,7 +167,18 @@ class ForwardingRemoteServiceTest { @Test void shouldThrowTechnicalException() { - assertThrows(TechnicalException.class, de.ozgcloud.vorgang.vorgang.redirect.ForwardingRemoteServiceTest.TestWaitForCompletion.this::waitForCompletion); + assertThrows(TechnicalException.class, TestWaitForCompletion.this::waitForCompletion); + } + + @Test + void shouldCompleteFutureExceptionally() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + verify(future).completeExceptionally(exception); } } @@ -112,7 +195,18 @@ class ForwardingRemoteServiceTest { @Test void shouldThrowTechnicalException() { - assertThrows(TechnicalException.class, de.ozgcloud.vorgang.vorgang.redirect.ForwardingRemoteServiceTest.TestWaitForCompletion.this::waitForCompletion); + assertThrows(TechnicalException.class, TestWaitForCompletion.this::waitForCompletion); + } + + @Test + void shouldCompleteFutureExceptionally() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + verify(future).completeExceptionally(exception); } } -- GitLab From b6f50afd1d5edbe2beb1295fce866c0e540eac8f Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Thu, 3 Apr 2025 17:12:18 +0200 Subject: [PATCH 09/18] OZG-7573 OZG-7991 Do not exit run() when delegate is replaced --- .../vorgang/redirect/EingangForwarder.java | 24 ++++++++-- .../redirect/EingangForwarderTest.java | 48 +++++++++++++++++-- 2 files changed, 64 insertions(+), 8 deletions(-) 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 f34f7e0ba..7222987de 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 @@ -29,8 +29,10 @@ import io.grpc.stub.ClientResponseObserver; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; @RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Log4j2 class EingangForwarder { private final RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; @@ -241,11 +243,11 @@ class EingangForwarder { static class DelegatingOnReadyHandler implements Runnable { private final ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStream; - private final AtomicReference<Runnable> onReadyHandler = new AtomicReference<>(); + private final AtomicReference<Runnable> delegate = new AtomicReference<>(); private final AtomicBoolean done = new AtomicBoolean(false); public void setDelegate(Runnable onReadyHandler) { - this.onReadyHandler.set(onReadyHandler); + this.delegate.set(onReadyHandler); } public void stop() { @@ -254,9 +256,21 @@ class EingangForwarder { @Override public void run() { - var delegate = onReadyHandler.get(); - if (delegate != null && !done.get() && requestStream.isReady()) { - delegate.run(); + while (!done.get() && requestStream.isReady()) { + if (Thread.currentThread().isInterrupted()) { + break; + } + var delegate = this.delegate.get(); + if (delegate != null) { + delegate.run(); + } else { + try { + wait(100); + } catch (InterruptedException e) { + LOG.debug("Interrupted while waiting for delegate to be set"); + Thread.currentThread().interrupt(); + } + } } } } 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 index 0361376b0..25f551826 100644 --- 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 @@ -1,16 +1,21 @@ package de.ozgcloud.vorgang.vorgang.redirect; import static org.assertj.core.api.Assertions.*; +import static org.awaitility.Awaitility.*; 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.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -967,6 +972,20 @@ class EingangForwarderTest { assertThat(getDoneFromOnReadyHandler()).isFalse(); } + @Nested + class TestSetDelegate { + + @Mock + private Runnable delegate; + + @Test + void shouldSetDelegate() { + onReadyHandler.setDelegate(delegate); + + assertThat(getDelegateFromOnReadyHandler()).isSameAs(delegate); + } + } + @Nested class TestStop { @@ -1010,17 +1029,40 @@ class EingangForwarderTest { @Test void shouldRunDelegateIfNotDoneAndReady() { - when(requestStream.isReady()).thenReturn(true); + when(requestStream.isReady()).thenReturn(true).thenReturn(false); + runWithOnReadyHandlerInAnotherThread(() -> { + await().atMost(Duration.ofMillis(500)).untilAsserted(() -> verify(delegate, atLeastOnce()).run()); + }); + } - onReadyHandler.run(); + @Test + void shouldContinueAfterDelegateWasReplaced() { + when(requestStream.isReady()).thenReturn(true); + runWithOnReadyHandlerInAnotherThread(() -> { + await().atMost(Duration.ofMillis(500)).untilAsserted(() -> verify(delegate, atLeastOnce()).run()); + var delegate2 = mock(Runnable.class); + onReadyHandler.setDelegate(delegate2); + await().atMost(Duration.ofMillis(500)).untilAsserted(() -> verify(delegate2, atLeastOnce()).run()); + }); + } - verify(delegate).run(); + private void runWithOnReadyHandlerInAnotherThread(Runnable runnable) { + try (ExecutorService executor = Executors.newSingleThreadExecutor()) { + var future = executor.submit(onReadyHandler); + runnable.run(); + future.cancel(true); + executor.shutdown(); + } } } private boolean getDoneFromOnReadyHandler() { return ReflectionTestUtils.getField(onReadyHandler, "done", AtomicBoolean.class).get(); } + + private Runnable getDelegateFromOnReadyHandler() { + return (Runnable) ReflectionTestUtils.getField(onReadyHandler, "delegate", AtomicReference.class).get(); + } } private void setResponseObserverInForwarder(ForwardingResponseObserver responseObserver) { -- GitLab From f22371e17ab4844d9cee09ca5515d4c43884973f Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Thu, 3 Apr 2025 17:40:32 +0200 Subject: [PATCH 10/18] OZG-7573 OZG-7991 Update version of common-lib --- vorgang-manager-server/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vorgang-manager-server/pom.xml b/vorgang-manager-server/pom.xml index b95fe9a9e..0946d4873 100644 --- a/vorgang-manager-server/pom.xml +++ b/vorgang-manager-server/pom.xml @@ -51,7 +51,7 @@ <spring-boot.build-image.imageName>docker.ozg-sh.de/vorgang-manager:build-latest</spring-boot.build-image.imageName> <zufi-manager-interface.version>1.6.0</zufi-manager-interface.version> - <common-lib.version>4.13.0-OZG-7573-files-weiterleitung-bug-SNAPSHOT</common-lib.version> + <common-lib.version>4.13.0-SNAPSHOT</common-lib.version> <user-manager-interface.version>2.12.0</user-manager-interface.version> <processor-manager.version>0.5.0</processor-manager.version> <nachrichten-manager.version>2.19.0</nachrichten-manager.version> -- GitLab From cb433faf462da6c7d638a2c6669193307bd4bcdf Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Thu, 3 Apr 2025 17:44:41 +0200 Subject: [PATCH 11/18] OZG-7573 OZG-7991 Add license --- .../vorgang/redirect/EingangForwarder.java | 23 +++++++++++++++++++ .../redirect/EingangForwarderTest.java | 23 +++++++++++++++++++ .../redirect/ForwardingRemoteServiceTest.java | 23 +++++++++++++++++++ 3 files changed, 69 insertions(+) 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 7222987de..338af0917 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 @@ -1,3 +1,26 @@ +/* + * Copyright (C) 2025 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.vorgang.vorgang.redirect; import java.io.InputStream; 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 index 25f551826..bddca9e4e 100644 --- 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 @@ -1,3 +1,26 @@ +/* + * Copyright (C) 2025 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.vorgang.vorgang.redirect; import static org.assertj.core.api.Assertions.*; 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 index e883ab782..dce2bd91b 100644 --- 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 @@ -1,3 +1,26 @@ +/* + * Copyright (C) 2025 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.vorgang.vorgang.redirect; import static org.assertj.core.api.Assertions.*; -- GitLab From 462e5e40d21702c568c84c23b4a5058c80f68291 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Fri, 4 Apr 2025 11:47:49 +0200 Subject: [PATCH 12/18] OZG-7573 OZG-7991 Make EingangForwarder a managed bean --- .../vorgang/redirect/EingangForwarder.java | 17 ++++++++-------- .../redirect/ForwardingRemoteService.java | 16 +++++++-------- .../redirect/ForwardingRemoteServiceTest.java | 20 +------------------ 3 files changed, 17 insertions(+), 36 deletions(-) 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 338af0917..6c71321b1 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 @@ -31,6 +31,10 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Function; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + import com.google.protobuf.ByteString; import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; @@ -49,18 +53,20 @@ import de.ozgcloud.vorgang.vorgang.IncomingFileGroup; import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; import io.grpc.stub.ClientCallStreamObserver; import io.grpc.stub.ClientResponseObserver; -import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; +import net.devh.boot.grpc.client.inject.GrpcClient; -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@RequiredArgsConstructor @Log4j2 class EingangForwarder { + @GrpcClient("forwarder") private final RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; private final FileService fileService; - private final IncomingFileMapper incomingFileMapper; private ForwardingResponseObserver responseObserver; @@ -69,11 +75,6 @@ class EingangForwarder { @Getter private CompletableFuture<Void> forwardFuture; - public static EingangForwarder create(RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub, FileService fileService, - IncomingFileMapper incomingFileMapper) { - return new EingangForwarder(serviceStub, fileService, incomingFileMapper); - } - public EingangForwarder forward(GrpcRouteForwarding grpcRouteForwarding, List<IncomingFileGroup> attachments, List<IncomingFile> representations) { 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 eef4e2ca0..387491c0a 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 @@ -28,15 +28,12 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.springframework.beans.factory.annotation.Lookup; import org.springframework.stereotype.Service; 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.RequiredArgsConstructor; -import net.devh.boot.grpc.client.inject.GrpcClient; @Service @RequiredArgsConstructor @@ -45,19 +42,20 @@ class ForwardingRemoteService { static final int TIMEOUT_MINUTES = 10; private final VorgangService vorgangService; private final ForwardingRequestMapper forwardingRequestMapper; - @GrpcClient("forwarder") - private final RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; - private final FileService fileService; - private final IncomingFileMapper incomingFileMapper; public void forward(ForwardingRequest request) { var eingang = vorgangService.getById(request.getVorgangId()).getEingangs().getFirst(); var grpcRouteForwarding = forwardingRequestMapper.toGrpcRouteForwarding(request, eingang); - var responseFuture = EingangForwarder.create(serviceStub, fileService, incomingFileMapper).forward(grpcRouteForwarding, eingang.getAttachments(), + var responseFuture = getEingangForwarder().forward(grpcRouteForwarding, eingang.getAttachments(), eingang.getRepresentations()).getForwardFuture(); waitForCompletion(responseFuture); } + @Lookup + EingangForwarder getEingangForwarder() { + return null; // provided by Spring + } + <T> void waitForCompletion(CompletableFuture<T> responseFuture) { try { responseFuture.get(TIMEOUT_MINUTES, TimeUnit.MINUTES); 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 index dce2bd91b..5ddb68d30 100644 --- 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 @@ -34,21 +34,16 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -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.InjectMocks; import org.mockito.Mock; -import org.mockito.MockedStatic; import org.mockito.Spy; import de.ozgcloud.common.errorhandling.TechnicalException; -import de.ozgcloud.eingang.forwarder.RouteForwardingServiceGrpc; import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding; -import de.ozgcloud.vorgang.files.FileService; import de.ozgcloud.vorgang.vorgang.EingangTestFactory; -import de.ozgcloud.vorgang.vorgang.IncomingFileMapper; import de.ozgcloud.vorgang.vorgang.VorgangService; import de.ozgcloud.vorgang.vorgang.VorgangTestFactory; import lombok.SneakyThrows; @@ -59,12 +54,6 @@ class ForwardingRemoteServiceTest { 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; @@ -75,26 +64,19 @@ class ForwardingRemoteServiceTest { private final ForwardingRequest request = ForwardingRequestTestFactory.create(); private final GrpcRouteForwarding grpcRouteForwarding = GrpcRouteForwardingTestFactory.create(); private final CompletableFuture<Void> responseFuture = new CompletableFuture<>(); - private MockedStatic<EingangForwarder> eingangForwarderMockedStatic; @Mock private EingangForwarder eingangForwarder; @BeforeEach void init() { + doReturn(eingangForwarder).when(service).getEingangForwarder(); when(vorgangService.getById(any())).thenReturn(VorgangTestFactory.create()); - eingangForwarderMockedStatic = mockStatic(EingangForwarder.class); - eingangForwarderMockedStatic.when(() -> EingangForwarder.create(any(), any(), any())).thenReturn(eingangForwarder); when(eingangForwarder.forward(any(), any(), any())).thenReturn(eingangForwarder); when(eingangForwarder.getForwardFuture()).thenReturn(responseFuture); when(forwardingRequestMapper.toGrpcRouteForwarding(any(), any())).thenReturn(grpcRouteForwarding); doNothing().when(service).waitForCompletion(any()); } - @AfterEach - void teardown() { - eingangForwarderMockedStatic.close(); - } - @Test void shouldGetVorgang() { service.forward(request); -- GitLab From ab9da5d1395187f6533f99e539891ad11f8e262c Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Mon, 7 Apr 2025 12:45:30 +0200 Subject: [PATCH 13/18] OZG-7573 OZG-7991 Replace future composition with waiting --- .../vorgang/redirect/EingangForwarder.java | 135 ++--- .../redirect/ForwardingRemoteService.java | 27 +- .../redirect/EingangForwarderTest.java | 566 ++++++++++-------- .../redirect/ForwardingRemoteServiceTest.java | 133 ---- 4 files changed, 395 insertions(+), 466 deletions(-) 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 6c71321b1..144022330 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,11 +26,15 @@ package de.ozgcloud.vorgang.vorgang.redirect; import java.io.InputStream; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; -import java.util.function.Function; +import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @@ -39,6 +43,7 @@ import com.google.protobuf.ByteString; import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; import de.ozgcloud.common.binaryfile.StreamingFileSender; +import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.eingang.forwarder.RouteForwardingServiceGrpc; import de.ozgcloud.eingang.forwarding.GrpcAttachment; import de.ozgcloud.eingang.forwarding.GrpcFileContent; @@ -53,7 +58,6 @@ 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; import lombok.extern.log4j.Log4j2; import net.devh.boot.grpc.client.inject.GrpcClient; @@ -64,6 +68,8 @@ import net.devh.boot.grpc.client.inject.GrpcClient; @Log4j2 class EingangForwarder { + static final int TIMEOUT_MINUTES = 10; + @GrpcClient("forwarder") private final RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; private final FileService fileService; @@ -72,30 +78,16 @@ class EingangForwarder { private ForwardingResponseObserver responseObserver; private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver; - @Getter - private CompletableFuture<Void> forwardFuture; - - public EingangForwarder forward(GrpcRouteForwarding grpcRouteForwarding, List<IncomingFileGroup> attachments, - List<IncomingFile> representations) { - - forwardFuture = CompletableFuture.allOf( - callService(), - sendRouteForwarding(grpcRouteForwarding) - .thenCompose(ignored -> sendAttachments(attachments)) - .thenCompose(ignored -> sendRepresentations(representations)) - .whenComplete((result, ex) -> { - if (ex != null) { - requestObserver.onError(ex); - } else { - requestObserver.onCompleted(); - } - }) - ); - return this; + public void forward(GrpcRouteForwarding grpcRouteForwarding, List<IncomingFileGroup> attachments, List<IncomingFile> representations) { + var future = performGrpcCall(); + sendRouteForwarding(grpcRouteForwarding); + sendAttachments(attachments); + sendRepresentations(representations); + waitForCompletion(future); } - CompletableFuture<GrpcRouteForwardingResponse> callService() { - CompletableFuture<GrpcRouteForwardingResponse> responseFuture = new CompletableFuture<>(); + Future<GrpcRouteForwardingResponse> performGrpcCall() { + var responseFuture = new CompletableFuture<GrpcRouteForwardingResponse>(); responseObserver = new ForwardingResponseObserver(responseFuture); requestObserver = (ClientCallStreamObserver<GrpcRouteForwardingRequest>) serviceStub.withInterceptors( new VorgangManagerClientCallContextAttachingInterceptor()) @@ -103,41 +95,35 @@ class EingangForwarder { return responseFuture; } - CompletableFuture<GrpcRouteForwardingResponse> sendRouteForwarding(GrpcRouteForwarding grpcRouteForwarding) { - CompletableFuture<GrpcRouteForwardingResponse> future = new CompletableFuture<>(); + void sendRouteForwarding(GrpcRouteForwarding grpcRouteForwarding) { + var future = new CompletableFuture<Void>(); responseObserver.registerOnReadyHandler(getSendRouteForwardingRunnable(grpcRouteForwarding, future)); - return future; + waitForCompletion(future); } - Runnable getSendRouteForwardingRunnable(GrpcRouteForwarding grpcRouteForwarding, CompletableFuture<GrpcRouteForwardingResponse> future) { + Runnable getSendRouteForwardingRunnable(GrpcRouteForwarding grpcRouteForwarding, CompletableFuture<Void> future) { return () -> { requestObserver.onNext(GrpcRouteForwardingRequest.newBuilder().setRouteForwarding(grpcRouteForwarding).build()); - future.complete(GrpcRouteForwardingResponse.newBuilder().build()); + future.complete(null); }; } - CompletableFuture<GrpcRouteForwardingResponse> sendAttachments(List<IncomingFileGroup> attachments) { - return attachments.stream() + void sendAttachments(List<IncomingFileGroup> attachments) { + attachments.stream() .flatMap(attachment -> { var groupName = attachment.getName(); - return attachment.getFiles().stream().map(file -> getSendAttachmentFileFunction(groupName, file)); + return attachment.getFiles().stream().map(file -> new FileInGroup(groupName, file)); }) - .reduce( - CompletableFuture.completedFuture(GrpcRouteForwardingResponse.newBuilder().build()), - CompletableFuture::thenCompose, - (f1, f2) -> f1.thenCompose(ignored -> f2) - ); + .forEach(this::sendAttachmentFile); } - private Function<GrpcRouteForwardingResponse, CompletableFuture<GrpcRouteForwardingResponse>> getSendAttachmentFileFunction(String groupName, - IncomingFile file) { - return ignored -> sendAttachmentFile(groupName, file); + void sendAttachmentFile(FileInGroup fileInGroup) { + var fileContentStream = fileService.getUploadedFileStream(fileInGroup.file.getId()); + var fileSender = createAttachmentFileSender(fileInGroup.groupName, fileInGroup.file, fileContentStream).send(); + waitForCompletion(fileSender, fileContentStream); } - CompletableFuture<GrpcRouteForwardingResponse> sendAttachmentFile(String groupName, IncomingFile file) { - var fileContentStream = fileService.getUploadedFileStream(file.getId()); - var future = createAttachmentFileSender(groupName, file, fileContentStream).send().getResultFuture(); - return configureToCancelIfForwardFutureCompleted(future); + record FileInGroup(String groupName, IncomingFile file) { } StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createAttachmentFileSender(String groupName, @@ -162,25 +148,14 @@ class EingangForwarder { .build(); } - CompletableFuture<GrpcRouteForwardingResponse> sendRepresentations(List<IncomingFile> representations) { - return representations.stream() - .map(this::getSendRepresentationFileFunction) - .reduce( - CompletableFuture.completedFuture(GrpcRouteForwardingResponse.newBuilder().build()), - CompletableFuture::thenCompose, - (f1, f2) -> f1.thenCompose(ignored -> f2) - ); - } - - private Function<GrpcRouteForwardingResponse, CompletableFuture<GrpcRouteForwardingResponse>> getSendRepresentationFileFunction( - IncomingFile file) { - return ignored -> sendRepresentationFile(file); + void sendRepresentations(List<IncomingFile> representations) { + representations.forEach(this::sendRepresentationFile); } - CompletableFuture<GrpcRouteForwardingResponse> sendRepresentationFile(IncomingFile file) { + void sendRepresentationFile(IncomingFile file) { var fileContentStream = fileService.getUploadedFileStream(file.getId()); - var future = createRepresentationFileSender(file, fileContentStream).send().getResultFuture(); - return configureToCancelIfForwardFutureCompleted(future); + var fileSender = createRepresentationFileSender(file, fileContentStream).send(); + waitForCompletion(fileSender, fileContentStream); } StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> createRepresentationFileSender(IncomingFile file, @@ -204,15 +179,6 @@ class EingangForwarder { .build(); } - CompletableFuture<GrpcRouteForwardingResponse> configureToCancelIfForwardFutureCompleted(CompletableFuture<GrpcRouteForwardingResponse> future) { - forwardFuture.whenComplete((result, ex) -> { - if (forwardFuture.isDone() && !future.isDone()) { - future.cancel(true); - } - }); - return future; - } - GrpcFileContent buildGrpcFileContent(byte[] chunk, int length) { var fileContentBuilder = GrpcFileContent.newBuilder(); if (length <= 0) { @@ -229,6 +195,37 @@ class EingangForwarder { responseObserver::registerOnReadyHandler); } + <T> void waitForCompletion(Future<T> responseFuture) { + try { + responseFuture.get(TIMEOUT_MINUTES, TimeUnit.MINUTES); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new TechnicalException("Waiting for finishing file upload was interrupted.", e); + } catch (ExecutionException e) { + throw new TechnicalException("Error on uploading file content.", e); + } catch (TimeoutException e) { + throw new TechnicalException("Timeout on uploading file content.", e); + } + } + + void waitForCompletion(StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender, InputStream fileContentStream) { + try { + fileSender.getResultFuture().get(TIMEOUT_MINUTES, TimeUnit.MINUTES); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + fileSender.cancelOnError(e); + throw new TechnicalException("Waiting for finishing upload was interrupted.", e); + } catch (ExecutionException e) { + fileSender.cancelOnError(e); + throw new TechnicalException("Error on uploading file content.", e); + } catch (TimeoutException e) { + fileSender.cancelOnTimeout(); + throw new TechnicalException("Timeout on uploading data.", e); + } finally { + IOUtils.closeQuietly(fileContentStream); + } + } + @RequiredArgsConstructor static class ForwardingResponseObserver implements ClientResponseObserver<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> { private final CompletableFuture<GrpcRouteForwardingResponse> future; 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 387491c0a..aac13d6d1 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 @@ -23,15 +23,9 @@ */ package de.ozgcloud.vorgang.vorgang.redirect; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - import org.springframework.beans.factory.annotation.Lookup; import org.springframework.stereotype.Service; -import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.vorgang.vorgang.VorgangService; import lombok.RequiredArgsConstructor; @@ -39,36 +33,17 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor class ForwardingRemoteService { - static final int TIMEOUT_MINUTES = 10; private final VorgangService vorgangService; private final ForwardingRequestMapper forwardingRequestMapper; public void forward(ForwardingRequest request) { var eingang = vorgangService.getById(request.getVorgangId()).getEingangs().getFirst(); var grpcRouteForwarding = forwardingRequestMapper.toGrpcRouteForwarding(request, eingang); - var responseFuture = getEingangForwarder().forward(grpcRouteForwarding, eingang.getAttachments(), - eingang.getRepresentations()).getForwardFuture(); - waitForCompletion(responseFuture); + getEingangForwarder().forward(grpcRouteForwarding, eingang.getAttachments(), eingang.getRepresentations()); } @Lookup EingangForwarder getEingangForwarder() { return null; // provided by Spring } - - <T> void waitForCompletion(CompletableFuture<T> responseFuture) { - try { - responseFuture.get(TIMEOUT_MINUTES, TimeUnit.MINUTES); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - responseFuture.completeExceptionally(e); - throw new TechnicalException("Waiting for finishing file upload was interrupted.", e); - } catch (ExecutionException e) { - responseFuture.completeExceptionally(e); - throw new TechnicalException("Error on uploading file content.", e); - } catch (TimeoutException e) { - responseFuture.completeExceptionally(e); - throw new TechnicalException("Timeout on uploading file content.", e); - } - } } 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 index bddca9e4e..9681f8c6f 100644 --- 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 @@ -25,18 +25,21 @@ package de.ozgcloud.vorgang.vorgang.redirect; import static org.assertj.core.api.Assertions.*; import static org.awaitility.Awaitility.*; +import static org.junit.jupiter.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.time.Duration; import java.util.List; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; @@ -56,13 +59,13 @@ import org.mockito.Spy; import de.ozgcloud.common.binaryfile.GrpcFileUploadUtils; import de.ozgcloud.common.binaryfile.StreamingFileSender; +import de.ozgcloud.common.errorhandling.TechnicalException; 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; @@ -72,6 +75,7 @@ import de.ozgcloud.vorgang.vorgang.IncomingFileTestFactory; import de.ozgcloud.vorgang.vorgang.redirect.EingangForwarder.DelegatingOnReadyHandler; import de.ozgcloud.vorgang.vorgang.redirect.EingangForwarder.ForwardingResponseObserver; import io.grpc.stub.ClientCallStreamObserver; +import lombok.SneakyThrows; class EingangForwarderTest { @@ -88,83 +92,60 @@ class EingangForwarderTest { @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()); + @Mock + private Future<GrpcRouteForwardingResponse> future; @BeforeEach void init() { - setRequestObserverInForwarder(requestObserver); + doReturn(future).when(forwarder).performGrpcCall(); + doNothing().when(forwarder).sendRouteForwarding(any()); + doNothing().when(forwarder).sendAttachments(any()); + doNothing().when(forwarder).sendRepresentations(any()); + doNothing().when(forwarder).waitForCompletion(any()); } @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); + void shouldPerformGrpcCall() { + forwarder.forward(grpcRouteForwarding, attachments, representations); - CompletableFuture<Void> future = forwarder.forward(grpcRouteForwarding, attachments, representations).getForwardFuture(); - - assertOnCompletedCalled(future); + verify(forwarder).performGrpcCall(); } @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(); + void shouldSendRouteForwarding() { + forwarder.forward(grpcRouteForwarding, attachments, representations); - assertOnErrorCalled(future, error); + verify(forwarder).sendRouteForwarding(grpcRouteForwarding); } @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); + void shouldSendAttachments() { + forwarder.forward(grpcRouteForwarding, attachments, representations); - var future = forwarder.forward(grpcRouteForwarding, attachments, representations).getForwardFuture(); - - assertOnErrorCalled(future, error); + verify(forwarder).sendAttachments(attachments); } @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(); + void shouldSendRepresentations() { + forwarder.forward(grpcRouteForwarding, attachments, representations); - assertOnErrorCalled(future, error); + verify(forwarder).sendRepresentations(representations); } - private void assertOnCompletedCalled(CompletableFuture<Void> future) { - future.join(); - verify(requestObserver).onCompleted(); - verify(requestObserver, never()).onError(any()); - } + @Test + void shouldWaitForCompletion() { + forwarder.forward(grpcRouteForwarding, attachments, representations); - 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(); + verify(forwarder).waitForCompletion(future); } } @Nested - class TestCallService { + class TestPerformGrpcCall { @BeforeEach void init() { @@ -173,21 +154,21 @@ class EingangForwarderTest { @Test void shouldAttachClientCallContextToServiceStub() { - forwarder.callService(); + forwarder.performGrpcCall(); verify(serviceStub).withInterceptors(any(VorgangManagerClientCallContextAttachingInterceptor.class)); } @Test void shouldCreateResponseObserver() { - forwarder.callService(); + forwarder.performGrpcCall(); assertThat(getResponseObserverFromForwarder()).isNotNull(); } @Test void shouldMakeGrpcCallToRouteForwarding() { - forwarder.callService(); + forwarder.performGrpcCall(); verify(serviceStub).routeForwarding(getResponseObserverFromForwarder()); } @@ -203,18 +184,22 @@ class EingangForwarderTest { private Runnable onReadyHandler; @Captor private ArgumentCaptor<Runnable> onReadyHandlerCaptor; + @Captor + private ArgumentCaptor<CompletableFuture<Void>> futureCaptor; @BeforeEach void init() { setResponseObserverInForwarder(responseObserver); doReturn(onReadyHandler).when(forwarder).getSendRouteForwardingRunnable(any(), any()); + doNothing().when(forwarder).waitForCompletion(any()); } + @SuppressWarnings("unchecked") @Test void shouldGetSendRouteForwardingRunnable() { - var future = forwarder.sendRouteForwarding(grpcRouteForwarding); + forwarder.sendRouteForwarding(grpcRouteForwarding); - verify(forwarder).getSendRouteForwardingRunnable(grpcRouteForwarding, future); + verify(forwarder).getSendRouteForwardingRunnable(eq(grpcRouteForwarding), any(CompletableFuture.class)); } @Test @@ -222,10 +207,27 @@ class EingangForwarderTest { forwarder.sendRouteForwarding(grpcRouteForwarding); verify(responseObserver).registerOnReadyHandler(onReadyHandlerCaptor.capture()); - assertThatIsResultOfGetSendRouteForwardingRunnable(onReadyHandlerCaptor.getValue()); + assertIsSendRouteForwardingRunnable(onReadyHandlerCaptor.getValue()); + } + + @SuppressWarnings("unchecked") + @Test + void shouldWaitForCompletion() { + forwarder.sendRouteForwarding(grpcRouteForwarding); + + verify(forwarder).waitForCompletion(any(CompletableFuture.class)); + } + + @Test + void shouldBeTheSameFuture() { + forwarder.sendRouteForwarding(grpcRouteForwarding); + + verify(forwarder).getSendRouteForwardingRunnable(any(), futureCaptor.capture()); + verify(forwarder).waitForCompletion(futureCaptor.getValue()); + assertThat(futureCaptor.getAllValues().getFirst()).isSameAs(futureCaptor.getValue()); } - private void assertThatIsResultOfGetSendRouteForwardingRunnable(Runnable runnable) { + private void assertIsSendRouteForwardingRunnable(Runnable runnable) { runnable.run(); verify(onReadyHandler).run(); } @@ -238,7 +240,7 @@ class EingangForwarderTest { @Mock private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver; @Mock - private CompletableFuture<GrpcRouteForwardingResponse> future; + private CompletableFuture<Void> future; @BeforeEach void init() { @@ -253,10 +255,10 @@ class EingangForwarderTest { } @Test - void shouldCallOnComplete() { + void shouldCompleteFuture() { forwarder.getSendRouteForwardingRunnable(grpcRouteForwarding, future).run(); - verify(future).complete(GrpcRouteForwardingResponse.newBuilder().build()); + verify(future).complete(null); } } @@ -264,63 +266,26 @@ class EingangForwarderTest { 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()); + doNothing().when(forwarder).sendAttachmentFile(any()); } @Test - void shouldReturnFuture() { - var returned = forwarder.sendAttachments(attachments); - - assertThat(returned).isNotNull(); - } - - @Test - void shouldInitiallySendOnlyFirstFile() { + void shouldSendFirstAttachmentFile() { forwarder.sendAttachments(attachments); - verify(forwarder).sendAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE); - verify(forwarder, times(1)).sendAttachmentFile(anyString(), any()); + verify(forwarder).sendAttachmentFile( + new EingangForwarder.FileInGroup(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE)); } @Test - void shouldSendSecondFileAfterFirstFutureCompleted() { + void shouldSendSecondAttachmentFile() { 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(); + verify(forwarder).sendAttachmentFile( + new EingangForwarder.FileInGroup(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE2)); } } @@ -329,7 +294,6 @@ class EingangForwarderTest { @Mock private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; - private final CompletableFuture<GrpcRouteForwardingResponse> resultFuture = new CompletableFuture<>(); @Mock private InputStream fileContentStream; @@ -338,8 +302,7 @@ class EingangForwarderTest { when(fileService.getUploadedFileStream(any())).thenReturn(fileContentStream); doReturn(fileSender).when(forwarder).createAttachmentFileSender(any(), any(), any()); doReturn(fileSender).when(fileSender).send(); - when(fileSender.getResultFuture()).thenReturn(resultFuture); - doReturn(resultFuture).when(forwarder).configureToCancelIfForwardFutureCompleted(any()); + doNothing().when(forwarder).waitForCompletion(any(), any()); } @Test @@ -364,21 +327,14 @@ class EingangForwarderTest { } @Test - void shouldConfigureFutureToCancelIfForwardFutureCompleted() { + void shouldWaitForCompletion() { sendAttachmentFile(); - verify(forwarder).configureToCancelIfForwardFutureCompleted(resultFuture); + verify(forwarder).waitForCompletion(fileSender, fileContentStream); } - @Test - void shouldReturnResultFuture() { - var returned = sendAttachmentFile(); - - assertThat(returned).isSameAs(resultFuture); - } - - private CompletableFuture<GrpcRouteForwardingResponse> sendAttachmentFile() { - return forwarder.sendAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE); + private void sendAttachmentFile() { + forwarder.sendAttachmentFile(new EingangForwarder.FileInGroup(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE)); } } @@ -388,9 +344,9 @@ class EingangForwarderTest { @Mock private InputStream inputStream; @Mock - private GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; @Mock - private GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSenderWithMetadata; + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSenderWithMetadata; @Captor private ArgumentCaptor<BiFunction<byte[], Integer, GrpcRouteForwardingRequest>> chunkBuilderCaptor; @@ -405,7 +361,7 @@ class EingangForwarderTest { } @Test - void shouldCallCreateSenderWithoutMetadata() { + void shouldCreateSenderWithoutMetadata() { createAttachmentFileSender(); verify(forwarder).createSenderWithoutMetadata(chunkBuilderCaptor.capture(), eq(inputStream)); @@ -414,7 +370,7 @@ class EingangForwarderTest { } @Test - void shouldCallBuildGrpcAttachmentFile() { + void shouldBuildGrpcAttachmentFile() { createAttachmentFileSender(); verify(forwarder).buildGrpcAttachmentFile(IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.FILE); @@ -493,65 +449,17 @@ class EingangForwarderTest { 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(); + doNothing().when(forwarder).sendRepresentationFile(any()); } @Test - void shouldInitiallySendOnlyFirstFile() { - forwarder.sendRepresentations(representations); + void shouldSendRepresentationFile() { + forwarder.sendRepresentations(List.of(FILE)); verify(forwarder).sendRepresentationFile(FILE); - verify(forwarder, times(1)).sendRepresentationFile(any()); - } - - @Test - void shouldSendSecondFileAfterFirstFutureCompleted() { - forwarder.sendRepresentations(representations); - - future.complete(GrpcRouteForwardingResponse.newBuilder().build()); - - verify(forwarder).sendRepresentationFile(FILE2); - verify(forwarder, times(2)).sendRepresentationFile(any()); - } - - @Test - void shouldReturnedFutureBeInitiallyIncomplete() { - var returned = forwarder.sendRepresentations(representations); - - assertThat(returned.isDone()).isFalse(); - } - - @Test - void shouldReturnedFutureBeIncompleteAfterSendingFirstFile() { - var returned = forwarder.sendRepresentations(representations); - - future.complete(GrpcRouteForwardingResponse.newBuilder().build()); - - assertThat(returned.isDone()).isFalse(); - } - - @Test - void shouldReturnedFutureBeDoneAfterSendingAllFiles() { - var returned = forwarder.sendRepresentations(representations); - - future.complete(GrpcRouteForwardingResponse.newBuilder().build()); - future2.complete(GrpcRouteForwardingResponse.newBuilder().build()); - - assertThat(returned.isDone()).isTrue(); } } @@ -560,7 +468,6 @@ class EingangForwarderTest { @Mock private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; - private final CompletableFuture<GrpcRouteForwardingResponse> resultFuture = new CompletableFuture<>(); @Mock private InputStream fileContentStream; @@ -571,8 +478,7 @@ class EingangForwarderTest { when(fileService.getUploadedFileStream(any())).thenReturn(fileContentStream); doReturn(fileSender).when(forwarder).createRepresentationFileSender(any(), any()); doReturn(fileSender).when(fileSender).send(); - when(fileSender.getResultFuture()).thenReturn(resultFuture); - doReturn(resultFuture).when(forwarder).configureToCancelIfForwardFutureCompleted(any()); + doNothing().when(forwarder).waitForCompletion(any(), any()); } @Test @@ -597,21 +503,14 @@ class EingangForwarderTest { } @Test - void shouldConfigureFutureToCancelIfForwardFutureCompleted() { + void shouldWaitForCompletion() { sendRepresentationFile(); - verify(forwarder).configureToCancelIfForwardFutureCompleted(resultFuture); - } - - @Test - void shouldReturnResultFuture() { - var returned = sendRepresentationFile(); - - assertThat(returned).isSameAs(resultFuture); + verify(forwarder).waitForCompletion(fileSender, fileContentStream); } - private CompletableFuture<GrpcRouteForwardingResponse> sendRepresentationFile() { - return forwarder.sendRepresentationFile(file); + private void sendRepresentationFile() { + forwarder.sendRepresentationFile(file); } } @@ -621,9 +520,9 @@ class EingangForwarderTest { @Mock private InputStream inputStream; @Mock - private GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; @Mock - private GrpcFileUploadUtils.FileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSenderWithMetadata; + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSenderWithMetadata; @Captor private ArgumentCaptor<BiFunction<byte[], Integer, GrpcRouteForwardingRequest>> chunkBuilderCaptor; @@ -723,55 +622,6 @@ class EingangForwarderTest { } } - @Nested - class TestConfigureToCancelIfForwardFutureCompleted { - - private final CompletableFuture<Void> forwardFuture = new CompletableFuture<>(); - private final CompletableFuture<GrpcRouteForwardingResponse> future = new CompletableFuture<>(); - - @BeforeEach - void init() { - setForwardFutureInForwarder(forwardFuture); - } - - @Test - void shouldCancelFutureWhenForwardFutureCompleted() { - forwarder.configureToCancelIfForwardFutureCompleted(future); - - forwardFuture.complete(null); - - assertThat(future.isCancelled()).isTrue(); - } - - @Test - void shouldCancelFutureWhenForwardFutureWasCancelled() { - forwarder.configureToCancelIfForwardFutureCompleted(future); - - forwardFuture.cancel(true); - - assertThat(future.isCancelled()).isTrue(); - } - - @Test - void shouldCancelFutureWhenForwardFutureCompletedExceptionally() { - forwarder.configureToCancelIfForwardFutureCompleted(future); - - forwardFuture.completeExceptionally(new RuntimeException("Forced failure")); - - assertThat(future.isCancelled()).isTrue(); - } - - @Test - void shouldNotCancelFutureIfItIsDone() { - forwarder.configureToCancelIfForwardFutureCompleted(future); - future.complete(GrpcRouteForwardingResponse.getDefaultInstance()); - - forwardFuture.complete(null); - - assertThat(future.isCancelled()).isFalse(); - } - } - @Nested class TestBuildGrpcFileContent { @@ -858,6 +708,246 @@ class EingangForwarderTest { } } + @Nested + class TestWaitForCompletionOfFuture { + + @Mock + private CompletableFuture<Void> future; + + @SneakyThrows + @Test + void shouldGetFromFuture() { + waitForCompletion(); + + verify(future).get(EingangForwarder.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, TestWaitForCompletionOfFuture.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, TestWaitForCompletionOfFuture.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, TestWaitForCompletionOfFuture.this::waitForCompletion); + } + } + + private void waitForCompletion() { + forwarder.waitForCompletion(future); + } + } + + @Nested + class TestWaitForCompletionOfFileSender { + + @Mock + private StreamingFileSender<GrpcRouteForwardingRequest, GrpcRouteForwardingResponse> fileSender; + @Mock + private InputStream fileContentStream; + @Mock + private CompletableFuture<GrpcRouteForwardingResponse> future; + + @BeforeEach + void init() { + when(fileSender.getResultFuture()).thenReturn(future); + } + + @SneakyThrows + @Test + void shouldGetFromFuture() { + waitForCompletion(); + + verify(future).get(EingangForwarder.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 shouldInterruptThread() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + assertThat(Thread.currentThread().isInterrupted()).isTrue(); + } + + @Test + void shouldCancelOnError() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + verify(fileSender).cancelOnError(exception); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, TestWaitForCompletionOfFileSender.this::waitForCompletion); + } + + @SneakyThrows + @Test + void shouldCloseStream() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + verify(fileContentStream).close(); + } + } + + @Nested + class TestOnExecutionException { + + private final ExecutionException exception = new ExecutionException(new Exception()); + + @BeforeEach + @SneakyThrows + void mock() { + when(future.get(anyLong(), any())).thenThrow(exception); + } + + @Test + void shouldCancelOnError() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + verify(fileSender).cancelOnError(exception); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, TestWaitForCompletionOfFileSender.this::waitForCompletion); + } + + @SneakyThrows + @Test + void shouldCloseStream() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + verify(fileContentStream).close(); + } + } + + @Nested + class TestOnTimeoutException { + + private final TimeoutException exception = new TimeoutException(); + + @BeforeEach + @SneakyThrows + void mock() { + when(future.get(anyLong(), any())).thenThrow(exception); + } + + @Test + void shouldCancelOnTimeout() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + verify(fileSender).cancelOnTimeout(); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, TestWaitForCompletionOfFileSender.this::waitForCompletion); + } + + @SneakyThrows + @Test + void shouldCloseStream() { + try { + waitForCompletion(); + } catch (TechnicalException e) { + // expected + } + + verify(fileContentStream).close(); + } + } + + private void waitForCompletion() { + forwarder.waitForCompletion(fileSender, fileContentStream); + } + } + @Nested class TestForwardingResponseObserver { 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 index 5ddb68d30..2a1c5ea86 100644 --- 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 @@ -23,16 +23,10 @@ */ 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.List; -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; @@ -41,12 +35,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; -import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding; import de.ozgcloud.vorgang.vorgang.EingangTestFactory; import de.ozgcloud.vorgang.vorgang.VorgangService; import de.ozgcloud.vorgang.vorgang.VorgangTestFactory; -import lombok.SneakyThrows; class ForwardingRemoteServiceTest { @@ -63,7 +55,6 @@ class ForwardingRemoteServiceTest { private final ForwardingRequest request = ForwardingRequestTestFactory.create(); private final GrpcRouteForwarding grpcRouteForwarding = GrpcRouteForwardingTestFactory.create(); - private final CompletableFuture<Void> responseFuture = new CompletableFuture<>(); @Mock private EingangForwarder eingangForwarder; @@ -71,10 +62,7 @@ class ForwardingRemoteServiceTest { void init() { doReturn(eingangForwarder).when(service).getEingangForwarder(); when(vorgangService.getById(any())).thenReturn(VorgangTestFactory.create()); - when(eingangForwarder.forward(any(), any(), any())).thenReturn(eingangForwarder); - when(eingangForwarder.getForwardFuture()).thenReturn(responseFuture); when(forwardingRequestMapper.toGrpcRouteForwarding(any(), any())).thenReturn(grpcRouteForwarding); - doNothing().when(service).waitForCompletion(any()); } @Test @@ -97,126 +85,5 @@ class ForwardingRemoteServiceTest { verify(eingangForwarder).forward(grpcRouteForwarding, List.of(EingangTestFactory.ATTACHMENT), List.of(EingangTestFactory.REPRESENTATION)); } - - @Test - void shouldWaitForCompletion() { - service.forward(request); - - verify(service).waitForCompletion(responseFuture); - } - } - - @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, TestWaitForCompletion.this::waitForCompletion); - } - - @Test - void shouldInterruptThread() { - try { - waitForCompletion(); - } catch (TechnicalException e) { - // expected - } - - assertThat(Thread.currentThread().isInterrupted()).isTrue(); - } - - @Test - void shouldCompleteFutureExceptionally() { - try { - waitForCompletion(); - } catch (TechnicalException e) { - // expected - } - - verify(future).completeExceptionally(exception); - } - } - - @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, TestWaitForCompletion.this::waitForCompletion); - } - - @Test - void shouldCompleteFutureExceptionally() { - try { - waitForCompletion(); - } catch (TechnicalException e) { - // expected - } - - verify(future).completeExceptionally(exception); - } - } - - @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, TestWaitForCompletion.this::waitForCompletion); - } - - @Test - void shouldCompleteFutureExceptionally() { - try { - waitForCompletion(); - } catch (TechnicalException e) { - // expected - } - - verify(future).completeExceptionally(exception); - } - } - - private void waitForCompletion() { - service.waitForCompletion(future); - } } } -- GitLab From 6a0dbd515494b590229f77c122b93fd5da3679a9 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Mon, 7 Apr 2025 12:45:56 +0200 Subject: [PATCH 14/18] OZG-7573 OZG-7991 Set version of common-lib to snapshot --- vorgang-manager-server/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vorgang-manager-server/pom.xml b/vorgang-manager-server/pom.xml index 0946d4873..b95fe9a9e 100644 --- a/vorgang-manager-server/pom.xml +++ b/vorgang-manager-server/pom.xml @@ -51,7 +51,7 @@ <spring-boot.build-image.imageName>docker.ozg-sh.de/vorgang-manager:build-latest</spring-boot.build-image.imageName> <zufi-manager-interface.version>1.6.0</zufi-manager-interface.version> - <common-lib.version>4.13.0-SNAPSHOT</common-lib.version> + <common-lib.version>4.13.0-OZG-7573-files-weiterleitung-bug-SNAPSHOT</common-lib.version> <user-manager-interface.version>2.12.0</user-manager-interface.version> <processor-manager.version>0.5.0</processor-manager.version> <nachrichten-manager.version>2.19.0</nachrichten-manager.version> -- GitLab From 7c77fcf7efb19b890634a976456b5b83eadf772c Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Mon, 7 Apr 2025 17:09:35 +0200 Subject: [PATCH 15/18] OZG-7573 OZG-7991 Send GrpcRouteForwardingRequest only once --- .../vorgang/redirect/EingangForwarder.java | 16 ++++++---------- .../vorgang/redirect/EingangForwarderTest.java | 10 ++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) 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 144022330..655df82a5 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 @@ -68,7 +68,7 @@ import net.devh.boot.grpc.client.inject.GrpcClient; @Log4j2 class EingangForwarder { - static final int TIMEOUT_MINUTES = 10; + static final int TIMEOUT_MINUTES = 2; @GrpcClient("forwarder") private final RouteForwardingServiceGrpc.RouteForwardingServiceStub serviceStub; @@ -102,9 +102,12 @@ class EingangForwarder { } Runnable getSendRouteForwardingRunnable(GrpcRouteForwarding grpcRouteForwarding, CompletableFuture<Void> future) { + var executed = new AtomicBoolean(); return () -> { - requestObserver.onNext(GrpcRouteForwardingRequest.newBuilder().setRouteForwarding(grpcRouteForwarding).build()); - future.complete(null); + if (!executed.compareAndExchange(false, true)) { + requestObserver.onNext(GrpcRouteForwardingRequest.newBuilder().setRouteForwarding(grpcRouteForwarding).build()); + future.complete(null); + } }; } @@ -284,13 +287,6 @@ class EingangForwarder { var delegate = this.delegate.get(); if (delegate != null) { delegate.run(); - } else { - try { - wait(100); - } catch (InterruptedException e) { - LOG.debug("Interrupted while waiting for delegate to be set"); - Thread.currentThread().interrupt(); - } } } } 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 index 9681f8c6f..3c58b837e 100644 --- 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 @@ -44,6 +44,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.stream.IntStream; import org.apache.commons.lang3.RandomUtils; import org.junit.jupiter.api.AfterEach; @@ -260,6 +261,15 @@ class EingangForwarderTest { verify(future).complete(null); } + + @Test + void shouldRunOnlyOnce() { + var runnable = forwarder.getSendRouteForwardingRunnable(grpcRouteForwarding, future); + + IntStream.range(0, 3).forEach(i -> runnable.run()); + + verify(requestObserver, times(1)).onNext(any()); + } } @Nested -- GitLab From 6a11e2d35a300278b41e38ad1f315760cd3db79e Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Mon, 7 Apr 2025 18:04:36 +0200 Subject: [PATCH 16/18] OZG-7573 OZG-7991 Complete request --- .../vorgang/vorgang/redirect/EingangForwarder.java | 1 + .../vorgang/vorgang/redirect/EingangForwarderTest.java | 10 ++++++++++ 2 files changed, 11 insertions(+) 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 655df82a5..1e12a3b6c 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 @@ -83,6 +83,7 @@ class EingangForwarder { sendRouteForwarding(grpcRouteForwarding); sendAttachments(attachments); sendRepresentations(representations); + requestObserver.onCompleted(); waitForCompletion(future); } 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 index 3c58b837e..49798fe14 100644 --- 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 @@ -95,6 +95,8 @@ class EingangForwarderTest { @Mock private GrpcRouteForwarding grpcRouteForwarding; + @Mock + private ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver; private final List<IncomingFileGroup> attachments = List.of(IncomingFileGroupTestFactory.create()); private final List<IncomingFile> representations = List.of(IncomingFileTestFactory.create()); @Mock @@ -107,6 +109,7 @@ class EingangForwarderTest { doNothing().when(forwarder).sendAttachments(any()); doNothing().when(forwarder).sendRepresentations(any()); doNothing().when(forwarder).waitForCompletion(any()); + setRequestObserverInForwarder(requestObserver); } @Test @@ -137,6 +140,13 @@ class EingangForwarderTest { verify(forwarder).sendRepresentations(representations); } + @Test + void shouldCompleteRequest() { + forwarder.forward(grpcRouteForwarding, attachments, representations); + + verify(requestObserver).onCompleted(); + } + @Test void shouldWaitForCompletion() { forwarder.forward(grpcRouteForwarding, attachments, representations); -- GitLab From 3269b04ab32af36b9748ac855218aef64147b53b Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Tue, 8 Apr 2025 11:58:53 +0200 Subject: [PATCH 17/18] Revert "OZG-7573 OZG-7991 Set version of common-lib to snapshot" This reverts commit 6a0dbd515494b590229f77c122b93fd5da3679a9. --- vorgang-manager-server/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vorgang-manager-server/pom.xml b/vorgang-manager-server/pom.xml index b95fe9a9e..0946d4873 100644 --- a/vorgang-manager-server/pom.xml +++ b/vorgang-manager-server/pom.xml @@ -51,7 +51,7 @@ <spring-boot.build-image.imageName>docker.ozg-sh.de/vorgang-manager:build-latest</spring-boot.build-image.imageName> <zufi-manager-interface.version>1.6.0</zufi-manager-interface.version> - <common-lib.version>4.13.0-OZG-7573-files-weiterleitung-bug-SNAPSHOT</common-lib.version> + <common-lib.version>4.13.0-SNAPSHOT</common-lib.version> <user-manager-interface.version>2.12.0</user-manager-interface.version> <processor-manager.version>0.5.0</processor-manager.version> <nachrichten-manager.version>2.19.0</nachrichten-manager.version> -- GitLab From 4c263c5790cadc5653235e7c78c91f004b00fba5 Mon Sep 17 00:00:00 2001 From: Krzysztof <krzysztof.witukiewicz@mgm-tp.com> Date: Tue, 8 Apr 2025 16:08:24 +0200 Subject: [PATCH 18/18] OZG-7573 OZG-7991 CR adjustments --- .../vorgang/vorgang/redirect/EingangForwarder.java | 14 +++++++++----- .../vorgang/redirect/EingangForwarderTest.java | 14 +++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) 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 1e12a3b6c..20cbfb80b 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 @@ -80,11 +80,15 @@ class EingangForwarder { public void forward(GrpcRouteForwarding grpcRouteForwarding, List<IncomingFileGroup> attachments, List<IncomingFile> representations) { var future = performGrpcCall(); + sendEingang(grpcRouteForwarding, attachments, representations); + requestObserver.onCompleted(); + waitForCompletion(future); + } + + private void sendEingang(GrpcRouteForwarding grpcRouteForwarding, List<IncomingFileGroup> attachments, List<IncomingFile> representations) { sendRouteForwarding(grpcRouteForwarding); sendAttachments(attachments); sendRepresentations(representations); - requestObserver.onCompleted(); - waitForCompletion(future); } Future<GrpcRouteForwardingResponse> performGrpcCall() { @@ -268,11 +272,11 @@ class EingangForwarder { static class DelegatingOnReadyHandler implements Runnable { private final ClientCallStreamObserver<GrpcRouteForwardingRequest> requestStream; - private final AtomicReference<Runnable> delegate = new AtomicReference<>(); + private final AtomicReference<Runnable> delegateRef = new AtomicReference<>(); private final AtomicBoolean done = new AtomicBoolean(false); public void setDelegate(Runnable onReadyHandler) { - this.delegate.set(onReadyHandler); + this.delegateRef.set(onReadyHandler); } public void stop() { @@ -285,7 +289,7 @@ class EingangForwarder { if (Thread.currentThread().isInterrupted()) { break; } - var delegate = this.delegate.get(); + var delegate = delegateRef.get(); if (delegate != null) { delegate.run(); } 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 index 49798fe14..aacedd385 100644 --- 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 @@ -183,6 +183,14 @@ class EingangForwarderTest { verify(serviceStub).routeForwarding(getResponseObserverFromForwarder()); } + + @Test + void shouldReturnFutureOfResponseObserver() { + var result = forwarder.performGrpcCall(); + + var expectedFuture = ReflectionTestUtils.getField(getResponseObserverFromForwarder(), "future", CompletableFuture.class); + assertThat(result).isSameAs(expectedFuture); + } } @Nested @@ -1194,7 +1202,7 @@ class EingangForwarderTest { } private Runnable getDelegateFromOnReadyHandler() { - return (Runnable) ReflectionTestUtils.getField(onReadyHandler, "delegate", AtomicReference.class).get(); + return (Runnable) ReflectionTestUtils.getField(onReadyHandler, "delegateRef", AtomicReference.class).get(); } } @@ -1209,8 +1217,4 @@ class EingangForwarderTest { private void setRequestObserverInForwarder(ClientCallStreamObserver<GrpcRouteForwardingRequest> requestObserver) { ReflectionTestUtils.setField(forwarder, "requestObserver", requestObserver); } - - private void setForwardFutureInForwarder(CompletableFuture<Void> future) { - ReflectionTestUtils.setField(forwarder, "forwardFuture", future); - } } -- GitLab