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