From 8fe93cf567a0d1d21144b2a53bc9396be97546e0 Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Mon, 21 Oct 2024 17:38:19 +0200
Subject: [PATCH] OZG-6944 refactor AntragraumGrpcService to use GrpcDownloader

---
 .../antragraum/AntragraumGrpcService.java     |  81 +---
 .../antragraum/AntragraumService.java         |   8 +-
 .../common/grpc/GrpcDownloader.java           |   3 +-
 .../antragraum/AntragraumGrpcServiceTest.java | 438 +++---------------
 .../common/grpc/GrpcDownloaderTest.java       |   5 -
 5 files changed, 92 insertions(+), 443 deletions(-)

diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcService.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcService.java
index 0f96740..ab77067 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcService.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcService.java
@@ -23,30 +23,23 @@
 
 package de.ozgcloud.nachrichten.antragraum;
 
-import java.io.BufferedInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
 import java.time.ZonedDateTime;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executors;
-import java.util.concurrent.FutureTask;
 import java.util.stream.Stream;
 
-import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.core.task.TaskExecutor;
 
 import com.google.protobuf.ByteString;
 
-import de.ozgcloud.apilib.common.errorhandling.NotFoundException;
 import de.ozgcloud.common.errorhandling.TechnicalException;
 import de.ozgcloud.nachrichten.NachrichtenManagerConfiguration;
+import de.ozgcloud.nachrichten.common.grpc.GrpcDownloader;
 import de.ozgcloud.nachrichten.common.vorgang.VorgangService;
 import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
+import io.grpc.stub.CallStreamObserver;
 import io.grpc.stub.StreamObserver;
 import lombok.RequiredArgsConstructor;
 import net.devh.boot.grpc.server.service.GrpcService;
@@ -64,6 +57,7 @@ class AntragraumGrpcService extends AntragraumServiceGrpc.AntragraumServiceImplB
 	private final AttachmentFileRequestMapper attachmentFileRequestMapper;
 	@Qualifier(NachrichtenManagerConfiguration.NACHRICHTEN_OZG_CLOUD_FILE_MAPPER)
 	private final OzgCloudFileMapper ozgCloudFileMapper;
+	private final TaskExecutor taskExecutor;
 
 	private final VorgangService vorgangService;
 
@@ -136,66 +130,21 @@ class AntragraumGrpcService extends AntragraumServiceGrpc.AntragraumServiceImplB
 
 	@Override
 	public void getAttachmentContent(GrpcGetAttachmentContentRequest request, StreamObserver<GrpcGetAttachmentContentResponse> responseObserver) {
-		try (var pipedInputStream = new PipedInputStream();
-				var pipedOutputStream = new PipedOutputStream(pipedInputStream);
-				var executor = Executors.newFixedThreadPool(1);) {
-			var task = new FutureTask<>(() -> getAttachmentFileContent(request, pipedOutputStream), null);
-
-			executor.execute(task);
-			sendFileContent(pipedInputStream, responseObserver);
-			handleExceptionOnRetrievingContent(task);
-
-		} catch (IOException e) {
-			throw new TechnicalException("Error on sending attachment content!", e);
-		}
+		buildAttachmentDownloader(attachmentFileRequestMapper.fromContentRequest(request), responseObserver).start();
 	}
 
-	void getAttachmentFileContent(GrpcGetAttachmentContentRequest request, PipedOutputStream pipedOutputStream) {
-		service.getAttachmentContent(attachmentFileRequestMapper.fromContentRequest(request), pipedOutputStream);
-	}
-
-	void sendFileContent(InputStream fileContent, StreamObserver<GrpcGetAttachmentContentResponse> responseObserver) {
-		var fileChunk = new byte[CHUNK_SIZE];
-		int length;
-		try (var bufferedInputStream = createBufferedInputStream(fileContent)) {
-			while ((length = bufferedInputStream.read(fileChunk)) != -1) {
-				sendChunk(responseObserver, fileChunk, length);
-			}
-			responseObserver.onCompleted();
-		} catch (IOException e) {
-			handleException(fileContent, e, "Error on sending file!");
-		}
-	}
-
-	InputStream createBufferedInputStream(InputStream fileContent) {
-		return new BufferedInputStream(fileContent, CHUNK_SIZE);
-	}
-
-	private void sendChunk(StreamObserver<GrpcGetAttachmentContentResponse> responseObserver, byte[] fileChunk, int length) {
-		responseObserver.onNext(GrpcGetAttachmentContentResponse.newBuilder()
-				.setFileContent(ByteString.copyFrom(fileChunk, 0, length))
-				.build());
-	}
-
-	private void handleException(InputStream inputStream, IOException e, String message) {
-		IOUtils.closeQuietly(inputStream);
-		throw new TechnicalException(message, e);
+	GrpcDownloader<GrpcGetAttachmentContentResponse> buildAttachmentDownloader(AttachmentFileRequest request,
+			StreamObserver<GrpcGetAttachmentContentResponse> responseObserver) {
+		return GrpcDownloader.<GrpcGetAttachmentContentResponse>builder()
+				.callObserver((CallStreamObserver<GrpcGetAttachmentContentResponse>) responseObserver)
+				.taskExecutor(taskExecutor)
+				.downloadConsumer(outputStream -> service.getAttachmentContent(request, outputStream))
+				.chunkBuilder(this::buildAttachmentChunkResponse)
+				.build();
 	}
 
-	void handleExceptionOnRetrievingContent(FutureTask<Object> task) {
-		try {
-			task.get();
-		} catch (InterruptedException e) { // NOSONAR
-			throw new TechnicalException("Retrieving attachment content was interupted.", e);
-		} catch (ExecutionException e) {
-			if (e.getCause() instanceof SecurityException) {
-				throw (SecurityException) e.getCause();
-			}
-			if (e.getCause() instanceof NotFoundException) {
-				throw (NotFoundException) e.getCause();
-			}
-			throw new TechnicalException("Error on retrieving attachment content.", e);
-		}
+	GrpcGetAttachmentContentResponse buildAttachmentChunkResponse(ByteString chunk) {
+		return GrpcGetAttachmentContentResponse.newBuilder().setFileContent(chunk).build();
 	}
 
 	@Override
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumService.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumService.java
index 5da9037..d86093c 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumService.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/antragraum/AntragraumService.java
@@ -27,7 +27,7 @@ import static java.util.Objects.*;
 
 import java.io.DataOutputStream;
 import java.io.IOException;
-import java.io.PipedOutputStream;
+import java.io.OutputStream;
 import java.time.ZonedDateTime;
 import java.util.Comparator;
 import java.util.Optional;
@@ -199,15 +199,15 @@ public class AntragraumService {
 		return parser.parse(samlToken);
 	}
 
-	public void getAttachmentContent(AttachmentFileRequest request, PipedOutputStream pipedOutputStream) {
+	public void getAttachmentContent(AttachmentFileRequest request, OutputStream outputStream) {
 		try {
 			verifyAccessToFile(request);
 		} catch (RuntimeException e) {
-			IOUtils.closeQuietly(pipedOutputStream);
+			IOUtils.closeQuietly(outputStream);
 			throw e;
 		}
 
-		try (var dataOutput = new DataOutputStream(pipedOutputStream)) {
+		try (var dataOutput = new DataOutputStream(outputStream)) {
 			ozgCloudFileService.writeFileDataToStream(OzgCloudFileId.from(request.getFileId()), dataOutput);
 		} catch (IOException e) {
 			throw new TechnicalException("Error on getting attachment file content.", e);
diff --git a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/common/grpc/GrpcDownloader.java b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/common/grpc/GrpcDownloader.java
index 92d6c2b..7f34bd4 100644
--- a/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/common/grpc/GrpcDownloader.java
+++ b/nachrichten-manager-server/src/main/java/de/ozgcloud/nachrichten/common/grpc/GrpcDownloader.java
@@ -12,7 +12,6 @@ import org.springframework.core.task.TaskExecutor;
 
 import com.google.protobuf.ByteString;
 
-import de.ozgcloud.apilib.file.OzgCloudFileId;
 import de.ozgcloud.common.errorhandling.TechnicalException;
 import io.grpc.stub.CallStreamObserver;
 import lombok.Builder;
@@ -33,7 +32,7 @@ public class GrpcDownloader<T> {
 	private PipedOutputStream outputStream;
 
 	@Builder
-	public GrpcDownloader(CallStreamObserver<T> callObserver, OzgCloudFileId fileId, Function<ByteString, T> chunkBuilder,
+	public GrpcDownloader(CallStreamObserver<T> callObserver, Function<ByteString, T> chunkBuilder,
 			Consumer<OutputStream> downloadConsumer, TaskExecutor taskExecutor) {
 		this.callObserver = callObserver;
 		this.chunkBuilder = chunkBuilder;
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcServiceTest.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcServiceTest.java
index aeb61e1..8584a2d 100644
--- a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcServiceTest.java
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/antragraum/AntragraumGrpcServiceTest.java
@@ -21,28 +21,19 @@
 package de.ozgcloud.nachrichten.antragraum;
 
 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.BufferedInputStream;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
+import java.io.OutputStream;
 import java.time.ZonedDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.FutureTask;
+import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.stream.Stream;
 
-import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
@@ -53,17 +44,16 @@ import org.mockito.Captor;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.MockedConstruction;
-import org.mockito.MockedStatic;
 import org.mockito.Spy;
+import org.springframework.core.task.TaskExecutor;
 
 import com.google.protobuf.ByteString;
 import com.thedeanda.lorem.LoremIpsum;
 
-import de.ozgcloud.apilib.common.errorhandling.NotFoundException;
 import de.ozgcloud.apilib.file.OzgCloudFile;
 import de.ozgcloud.apilib.file.OzgCloudFileTestFactory;
-import de.ozgcloud.common.datatype.StringBasedValue;
 import de.ozgcloud.common.errorhandling.TechnicalException;
+import de.ozgcloud.nachrichten.common.grpc.GrpcDownloader;
 import de.ozgcloud.nachrichten.common.vorgang.GrpcServiceKontoTestFactory;
 import de.ozgcloud.nachrichten.common.vorgang.Vorgang;
 import de.ozgcloud.nachrichten.common.vorgang.VorgangService;
@@ -71,8 +61,8 @@ import de.ozgcloud.nachrichten.common.vorgang.VorgangTestFactory;
 import de.ozgcloud.nachrichten.postfach.PostfachNachricht;
 import de.ozgcloud.nachrichten.postfach.PostfachNachrichtTestFactory;
 import de.ozgcloud.nachrichten.postfach.osi.MessageTestFactory;
+import io.grpc.stub.CallStreamObserver;
 import io.grpc.stub.StreamObserver;
-import lombok.SneakyThrows;
 
 class AntragraumGrpcServiceTest {
 
@@ -91,6 +81,8 @@ class AntragraumGrpcServiceTest {
 	private VorgangService vorgangService;
 	@Mock
 	private OzgCloudFileMapper ozgCloudFileMapper;
+	@Mock
+	private TaskExecutor taskExecutor;
 
 	@DisplayName("Find rueckfragen")
 	@Nested
@@ -480,160 +472,8 @@ class AntragraumGrpcServiceTest {
 
 		@Mock
 		private StreamObserver<GrpcGetAttachmentContentResponse> responseObserver;
-
-		private final GrpcGetAttachmentContentRequest grpcRequest = GrpcGetAttachmentContentRequestTestFactory.create();
-
-		@Nested
-		class OnNoException {
-
-			private PipedInputStream pipedInputStream;
-			private MockedConstruction<PipedInputStream> mockedConstructionInput;
-
-			private PipedOutputStream pipedOutputStream;
-			private MockedConstruction<PipedOutputStream> mockedConstructionOutput;
-			private InputStream connectedInput;
-
-			private MockedConstruction<FutureTask> mockedConstructionFutureTask; // NOSONAR
-			private FutureTask<Object> futureTask;
-			private Runnable runnable;
-
-			private MockedStatic<Executors> mockedStaticExecutors;
-			@Mock
-			private ExecutorService executor;
-
-			@BeforeEach
-			void setUpMockedConstruction() {
-				mockedConstructionInput = mockConstruction(PipedInputStream.class, (pipedInputStream, context) -> {
-					this.pipedInputStream = pipedInputStream;
-					doNothing().when(grpcService).sendFileContent(pipedInputStream, responseObserver);
-				});
-				mockedConstructionOutput = mockConstruction(PipedOutputStream.class, (pipedOutputStream, context) -> {
-					this.pipedOutputStream = pipedOutputStream;
-					connectedInput = (InputStream) context.arguments().get(0);
-				});
-				mockedConstructionFutureTask = mockConstruction(FutureTask.class, (futureTask, context) -> {
-					this.futureTask = futureTask; // NOSONAR
-					runnable = (Runnable) context.arguments().get(0);
-				});
-				mockedStaticExecutors = mockStatic(Executors.class);
-				mockedStaticExecutors.when(() -> Executors.newFixedThreadPool(1)).thenReturn(executor);
-			}
-
-			@AfterEach
-			void closeMocks() {
-				mockedConstructionInput.close();
-				mockedConstructionOutput.close();
-				mockedConstructionFutureTask.close();
-				mockedStaticExecutors.close();
-			}
-
-			@Test
-			void shouldConstructInputStream() {
-				callGetAttachmentContent();
-
-				assertThat(mockedConstructionInput.constructed()).hasSize(1);
-			}
-
-			@Test
-			void shouldConstructOutputStream() {
-				callGetAttachmentContent();
-
-				assertThat(mockedConstructionOutput.constructed()).hasSize(1);
-			}
-
-			@Test
-			void shouldConnectPipes() {
-				callGetAttachmentContent();
-
-				assertThat(connectedInput).isEqualTo(pipedInputStream);
-			}
-
-			@Test
-			void shouldCreateNewFixedThreadPool() {
-				callGetAttachmentContent();
-
-				mockedStaticExecutors.verify(() -> Executors.newFixedThreadPool(1));
-			}
-
-			@Test
-			void shouldConstructFutureTask() {
-				callGetAttachmentContent();
-
-				assertThat(mockedConstructionFutureTask.constructed()).hasSize(1);
-			}
-
-			@Test
-			void shouldExecuteTask() {
-				callGetAttachmentContent();
-
-				verify(executor).execute(futureTask);
-			}
-
-			@Test
-			void shouldSendFileContent() {
-				callGetAttachmentContent();
-
-				verify(grpcService).sendFileContent(pipedInputStream, responseObserver);
-			}
-
-			@Test
-			void shouldCallHandleExceptionOnRetrievingContent() {
-				callGetAttachmentContent();
-
-				verify(grpcService).handleExceptionOnRetrievingContent(futureTask);
-			}
-
-			@Nested
-			class TestTask {
-				@Test
-				void shouldCallGetAttachmentFileContent() {
-					callGetAttachmentContent();
-
-					runnable.run();
-
-					verify(grpcService).getAttachmentFileContent(grpcRequest, pipedOutputStream);
-				}
-			}
-		}
-
-		@Nested
-		class OnIOException {
-
-			@BeforeEach
-			void setUpMock() {
-				doNothing().when(grpcService).sendFileContent(any(), eq(responseObserver));
-			}
-
-			@Test
-			@SneakyThrows
-			void shouldThrowTechnicalExceptionOnErrorOnInputStream() {
-				try (var mockedConstructionInput = mockConstruction(PipedInputStream.class,
-						(mock, context) -> doThrow(IOException.class).when(mock).close())) {
-
-					assertThrows(TechnicalException.class, () -> callGetAttachmentContent());
-				}
-			}
-
-			@Test
-			@SneakyThrows
-			void shouldThrowTechnicalExceptionOnErrorOnOutputStream() {
-				try (var mockedConstructionInput = mockConstruction(PipedOutputStream.class,
-						(mock, context) -> doThrow(IOException.class).when(mock).close())) {
-
-					assertThrows(TechnicalException.class, () -> callGetAttachmentContent());
-				}
-			}
-		}
-
-		private void callGetAttachmentContent() {
-			grpcService.getAttachmentContent(grpcRequest, responseObserver);
-		}
-	}
-
-	@Nested
-	class TestGetAttachmentFileContent {
 		@Mock
-		private PipedOutputStream pipedOutputStream;
+		private GrpcDownloader<GrpcGetAttachmentContentResponse> downloader;
 
 		private final GrpcGetAttachmentContentRequest grpcRequest = GrpcGetAttachmentContentRequestTestFactory.create();
 		private final AttachmentFileRequest request = AttachmentFileRequestTestFactory.create();
@@ -641,262 +481,128 @@ class AntragraumGrpcServiceTest {
 		@BeforeEach
 		void mock() {
 			when(attachmentFileRequestMapper.fromContentRequest(grpcRequest)).thenReturn(request);
+			doReturn(downloader).when(grpcService).buildAttachmentDownloader(request, responseObserver);
 		}
 
 		@Test
 		void shouldMapRequest() {
-			callGetRueckfrageAttachmentFile();
+			callGetAttachmentContent();
 
 			verify(attachmentFileRequestMapper).fromContentRequest(grpcRequest);
 		}
 
 		@Test
-		void shouldCallServiceToGetAttachmentContent() {
-			callGetRueckfrageAttachmentFile();
-
-			verify(service).getAttachmentContent(request, pipedOutputStream);
-		}
+		void shouldCallBuildAttachmentDownloader() {
+			callGetAttachmentContent();
 
-		private void callGetRueckfrageAttachmentFile() {
-			grpcService.getAttachmentFileContent(grpcRequest, pipedOutputStream);
+			verify(grpcService).buildAttachmentDownloader(request, responseObserver);
 		}
-	}
-
-	@Nested
-	class TestSendFileContent {
-
-		@Mock
-		private StreamObserver<GrpcGetAttachmentContentResponse> responseObserver;
-
-		@Mock
-		private InputStream fileContent;
-
-		@Nested
-		class TestWithByteArrayInputStream {
-			private final byte[] byteContent = FileContentTestFactory.createContentInByte((int) (AntragraumGrpcService.CHUNK_SIZE * 1.5));
-			private final InputStream inputStream = new ByteArrayInputStream(byteContent);
-
-			@BeforeEach
-			void setUpMock() {
-				doReturn(inputStream).when(grpcService).createBufferedInputStream(fileContent);
-			}
-
-			@Test
-			void shouldCreateBufferedInputStream() {
-				callSendFileContent();
 
-				verify(grpcService).createBufferedInputStream(fileContent);
-			}
-
-			@Test
-			void shouldSendFirstDataChunk() {
-				callSendFileContent();
-
-				verify(responseObserver)
-						.onNext(argThat((response) -> response.getFileContent()
-								.equals(ByteString.copyFrom(byteContent, 0, AntragraumGrpcService.CHUNK_SIZE))));
-			}
-
-			@Test
-			void shouldSendSecondDataChunk() {
-				callSendFileContent();
-
-				verify(responseObserver)
-						.onNext(argThat((response) -> response.getFileContent()
-								.equals(ByteString.copyFrom(byteContent, AntragraumGrpcService.CHUNK_SIZE,
-										byteContent.length - AntragraumGrpcService.CHUNK_SIZE))));
-			}
-
-			@Test
-			void shouldComplete() {
-				callSendFileContent();
-
-				verify(responseObserver).onCompleted();
-			}
-		}
-
-		@Nested
-		class TestWithMockedInputStream {
-
-			@Mock
-			private BufferedInputStream inputStream;
-
-			@BeforeEach
-			void setUpMock() {
-				doReturn(inputStream).when(grpcService).createBufferedInputStream(fileContent);
-			}
-
-			@Test
-			@SneakyThrows
-			void shouldCloseInputStream() {
-				when(inputStream.read(any())).thenReturn(-1);
-
-				callSendFileContent();
-
-				verify(inputStream).close();
-			}
-
-			@Nested
-			class OnIOException {
-
-				private MockedStatic<IOUtils> mockedIOUtils;
-
-				@BeforeEach
-				@SneakyThrows
-				void setUpMock() {
-					when(inputStream.read(any())).thenThrow(new IOException());
-					mockedIOUtils = mockStatic(IOUtils.class);
-				}
-
-				@AfterEach
-				void cleanUp() {
-					mockedIOUtils.close();
-				}
-
-				@Test
-				void shouldThrowTechnicalException() {
-					assertThrows(TechnicalException.class,
-							() -> callSendFileContent());
-				}
-
-				@Test
-				void shouldCloseFileContentStreamQuietly() {
-					try {
-						callSendFileContent();
-					} catch (TechnicalException e) {
-					}
+		@Test
+		void shouldStartDownloader() {
+			callGetAttachmentContent();
 
-					mockedIOUtils.verify(() -> IOUtils.closeQuietly(fileContent));
-				}
-			}
+			verify(downloader).start();
 		}
 
-		private void callSendFileContent() {
-			grpcService.sendFileContent(fileContent, responseObserver);
+		private void callGetAttachmentContent() {
+			grpcService.getAttachmentContent(grpcRequest, responseObserver);
 		}
 	}
 
 	@Nested
-	class TestCreateBufferedInputStream {
+	class TestBuildAttachmentDownloader {
+		@Mock
+		private CallStreamObserver<GrpcGetAttachmentContentResponse> responseObserver;
+		@Mock
+		private OutputStream outputStream;
 
-		private MockedConstruction<BufferedInputStream> mockConstructionBufferedInputStream;
-		private InputStream passedInputStream;
-		private BufferedInputStream constructedInputStream;
-		private int chunkSize;
+		private MockedConstruction<GrpcDownloader> downloaderMockedConstruction;
+		private GrpcDownloader<GrpcGetAttachmentContentResponse> downloader;
+		private StreamObserver<GrpcGetAttachmentContentResponse> setResponseObserver;
+		private TaskExecutor setTaskExecutor;
+		private Consumer<OutputStream> setDownloadConsumer;
+		private Function<ByteString, GrpcGetAttachmentContentResponse> setChunkBuilder;
 
-		@Mock
-		private InputStream fileContent;
+		private final AttachmentFileRequest request = AttachmentFileRequestTestFactory.create();
+		private final ByteString chunk = ByteString.copyFromUtf8(LoremIpsum.getInstance().getWords(5));
 
+		@SuppressWarnings("unchecked")
 		@BeforeEach
-		void setUpMock() {
-			mockConstructionBufferedInputStream = mockConstruction(BufferedInputStream.class, (mock, context) -> {
-				passedInputStream = (InputStream) context.arguments().get(0);
-				chunkSize = (int) context.arguments().get(1);
-				constructedInputStream = mock;
+		void mock() {
+			downloaderMockedConstruction = mockConstruction(GrpcDownloader.class, (downloader, context) -> {
+				setResponseObserver = (StreamObserver<GrpcGetAttachmentContentResponse>) context.arguments().get(0);
+				setChunkBuilder = (Function<ByteString, GrpcGetAttachmentContentResponse>) context.arguments().get(1);
+				setDownloadConsumer = (Consumer<OutputStream>) context.arguments().get(2);
+				setTaskExecutor = (TaskExecutor) context.arguments().get(3);
+				this.downloader = downloader;
 			});
 		}
 
 		@AfterEach
 		void closeMock() {
-			mockConstructionBufferedInputStream.close();
+			downloaderMockedConstruction.close();
 		}
 
 		@Test
-		void shouldConstructBufferedInputStream() {
-			callCreateBufferedInputStream();
+		void shouldSetResponseObserver() {
+			callBuildAttachmentDownloader();
 
-			assertThat(mockConstructionBufferedInputStream.constructed()).hasSize(1);
+			assertThat(setResponseObserver).isEqualTo(responseObserver);
 		}
 
 		@Test
-		void shouldConstructBufferedInputStreamWithFileContent() {
-			callCreateBufferedInputStream();
+		void shouldSetTaskExecutor() {
+			callBuildAttachmentDownloader();
 
-			assertThat(passedInputStream).isEqualTo(fileContent);
+			assertThat(setTaskExecutor).isEqualTo(taskExecutor);
 		}
 
 		@Test
-		void shouldConstructBufferedInputStreamWithChunkSize() {
-			callCreateBufferedInputStream();
+		void shouldSetDownloadConsumer() {
+			callBuildAttachmentDownloader();
+
+			setDownloadConsumer.accept(outputStream);
 
-			assertThat(chunkSize).isEqualTo(AntragraumGrpcService.CHUNK_SIZE);
+			verify(service).getAttachmentContent(request, outputStream);
 		}
 
 		@Test
-		void shouldReturnBufferedInputStream() {
-			var returnedInputStream = callCreateBufferedInputStream();
+		void shouldSetChunkBuilder() {
+			var response = GrpcGetAttachmentContentResponse.newBuilder().setFileContent(chunk).build();
+			doReturn(response).when(grpcService).buildAttachmentChunkResponse(chunk);
+			callBuildAttachmentDownloader();
 
-			assertThat(returnedInputStream).isEqualTo(constructedInputStream);
-		}
+			var result = setChunkBuilder.apply(chunk);
 
-		private InputStream callCreateBufferedInputStream() {
-			return grpcService.createBufferedInputStream(fileContent);
+			assertThat(result).isEqualTo(response);
 		}
 
-	}
-
-	@Nested
-	class TestHandleExceptionOnRetrievingContent {
-
-		@Mock
-		private FutureTask<Object> futureTask;
-
-		private final StringBasedValue id = new StringBasedValue() {
-		};
-		private final String entityName = LoremIpsum.getInstance().getWords(1);
-
 		@Test
-		@SneakyThrows
-		void shouldGettaskResult() {
-			callHandleExceptionOnRetrievingContent();
+		void shouldReturnDownloader() {
+			var returnedDownloader = callBuildAttachmentDownloader();
 
-			verify(futureTask).get();
+			assertThat(returnedDownloader).isEqualTo(downloader);
 		}
 
-		@Test
-		@SneakyThrows
-		void shouldThrowTechnicalExceptionOnInteruption() {
-			var interruptedException = new InterruptedException();
-			when(futureTask.get()).thenThrow(interruptedException);
-
-			assertThatThrownBy(() -> callHandleExceptionOnRetrievingContent())
-					.isInstanceOf(TechnicalException.class)
-					.hasCause(interruptedException);
+		private GrpcDownloader<GrpcGetAttachmentContentResponse> callBuildAttachmentDownloader() {
+			return grpcService.buildAttachmentDownloader(request, responseObserver);
 		}
+	}
 
-		@Test
-		@SneakyThrows
-		void shouldRethrowSecurityException() {
-			var securityException = new SecurityException();
-			when(futureTask.get()).thenThrow(new ExecutionException(securityException));
+	@Nested
+	class TestBuildAttachmentChunkResponse {
 
-			assertThatThrownBy(() -> callHandleExceptionOnRetrievingContent())
-					.isInstanceOf(SecurityException.class);
-		}
+		private final ByteString chunk = ByteString.copyFromUtf8(LoremIpsum.getInstance().getWords(5));
 
 		@Test
-		@SneakyThrows
-		void shouldRethrowNotFoundException() {
-			var notFoundException = new NotFoundException(id, entityName);
-			when(futureTask.get()).thenThrow(new ExecutionException(notFoundException));
+		void shouldReturnResponse() {
+			var expectedResponse = GrpcGetAttachmentContentResponse.newBuilder().setFileContent(chunk).build();
 
-			assertThatThrownBy(() -> callHandleExceptionOnRetrievingContent())
-					.isInstanceOf(NotFoundException.class);
-		}
+			var result = grpcService.buildAttachmentChunkResponse(chunk);
 
-		@Test
-		@SneakyThrows
-		void shouldThrowTechnicalExceptionOtherExecutionException() {
-			var executionException = new ExecutionException(new Exception());
-			when(futureTask.get()).thenThrow(executionException);
-
-			assertThatThrownBy(() -> callHandleExceptionOnRetrievingContent())
-					.isInstanceOf(TechnicalException.class)
-					.hasCause(executionException);
-		}
+			assertThat(result).isEqualTo(expectedResponse);
 
-		private void callHandleExceptionOnRetrievingContent() {
-			grpcService.handleExceptionOnRetrievingContent(futureTask);
 		}
 	}
 
diff --git a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/common/grpc/GrpcDownloaderTest.java b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/common/grpc/GrpcDownloaderTest.java
index 28880fd..4f7c0c5 100644
--- a/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/common/grpc/GrpcDownloaderTest.java
+++ b/nachrichten-manager-server/src/test/java/de/ozgcloud/nachrichten/common/grpc/GrpcDownloaderTest.java
@@ -26,8 +26,6 @@ import org.springframework.test.util.ReflectionTestUtils;
 
 import com.google.protobuf.ByteString;
 
-import de.ozgcloud.apilib.file.OzgCloudFileId;
-import de.ozgcloud.apilib.file.OzgCloudFileTestFactory;
 import de.ozgcloud.common.errorhandling.TechnicalException;
 import io.grpc.stub.ClientCallStreamObserver;
 import lombok.SneakyThrows;
@@ -42,12 +40,9 @@ class GrpcDownloaderTest {
 	private Consumer<OutputStream> downloadConsumer = Mockito.mock(Consumer.class);
 	private TaskExecutor taskExecutor = mock(TaskExecutor.class);
 
-	private OzgCloudFileId fileId = OzgCloudFileTestFactory.ID;
-
 	@Spy
 	private GrpcDownloader<GrpcResponseDummy> downloader = GrpcDownloader.<GrpcResponseDummy>builder()
 			.callObserver(callObserver)
-			.fileId(fileId)
 			.downloadConsumer(downloadConsumer)
 			.chunkBuilder(chunkBuilder)
 			.taskExecutor(taskExecutor)
-- 
GitLab