diff --git a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java index 5c9f3d4ad29bf0fd09c57784300b28afb1bea20f..d5f1aa22a54acee33d98a9212725616fbae03111 100644 --- a/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java +++ b/forwarder/src/main/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserver.java @@ -103,14 +103,14 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou } } - private void handleRouteForwarding(GrpcRouteForwarding routeForwarding) { + void handleRouteForwarding(GrpcRouteForwarding routeForwarding) { if (Objects.nonNull(formData)) { throw new IllegalStateException("Received second RouteForwarding. Send only one per request."); } formData = routeForwardingMapper.toFormData(routeForwarding); } - private void handleAttachment(GrpcAttachment attachment) { + void handleAttachment(GrpcAttachment attachment) { if (attachment.hasFile()) { setCurrentMetadata(incomingFileMapper.fromGrpcAttachmentFile(attachment.getFile())); groupName = Optional.of(attachment.getFile().getGroupName()); @@ -119,7 +119,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou } } - private void handleRepresentation(GrpcRepresentation representation) { + void handleRepresentation(GrpcRepresentation representation) { if (representation.hasFile()) { setCurrentMetadata(incomingFileMapper.fromGrpcRepresentationFile(representation.getFile())); } else { @@ -128,21 +128,21 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou } - private void setCurrentMetadata(IncomingFile metaData) { + void setCurrentMetadata(IncomingFile metaData) { if (Objects.nonNull(currentFile)) { - throw new TechnicalException("Received additional file before previos file reached the end."); + throw new IllegalStateException("Received additional file before previos file reached the end."); } currentFile = metaData; } - private void handleFileContent(GrpcFileContent fileContent) { + void handleFileContent(GrpcFileContent fileContent) { if (Objects.isNull(receivingFileContent)) { initContentReceiving(); } storeFileContent(fileContent); } - private void initContentReceiving() { + void initContentReceiving() { try { pipedInput = new PipedInputStream(CHUNK_SIZE); pipedOutput = new PipedOutputStream(pipedInput); @@ -152,9 +152,9 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou } } - private void storeFileContent(GrpcFileContent content) { + void storeFileContent(GrpcFileContent content) { if (Objects.isNull(currentFile)) { - throw new TechnicalException("File content received before metadata."); + throw new IllegalStateException("File content received before metadata."); } try { pipedOutput.write(content.getContent().toByteArray()); @@ -166,14 +166,16 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou } } - private void handleEndOfFile() { + void handleEndOfFile() { closeOutputPipe(); var completedIncomingFile = currentFile.toBuilder().file(getSavedFileContent()).build(); - groupName.map(group -> attachments.get(group)).orElse(representations).add(completedIncomingFile); + groupName.map(group -> attachments.computeIfAbsent(group, s -> new ArrayList<IncomingFile>())) + .orElse(representations) + .add(completedIncomingFile); resetFileReceiving(); } - private File getSavedFileContent() { + File getSavedFileContent() { try { return receivingFileContent.get(TIMEOUT_MINUTES, TimeUnit.MINUTES); } catch (ExecutionException | TimeoutException e) { @@ -186,7 +188,7 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou } } - private void resetFileReceiving() { + void resetFileReceiving() { currentFile = null; groupName = Optional.empty(); pipedOutput = null; @@ -201,11 +203,11 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou closeInputPipe(); } - private void closeOutputPipe() { + void closeOutputPipe() { IOUtils.closeQuietly(pipedOutput, e -> LOG.error("Cannot close output stream.", e)); } - private void closeInputPipe() { + void closeInputPipe() { IOUtils.closeQuietly(pipedInput, e -> LOG.error("Cannot close input stream.", e)); } @@ -215,13 +217,15 @@ public class EingangStubReceiverStreamObserver implements StreamObserver<GrpcRou responseConsumer.accept(GrpcRouteForwardingResponse.getDefaultInstance()); } - private FormData assembleFormData() { + FormData assembleFormData() { if (Objects.isNull(formData)) { throw new IllegalStateException("Never received RouteForwarding containing EingangStub and RouteCriteria."); } return formData.toBuilder() .representations(representations) .attachments(attachments.entrySet().stream().map(incomingFileGroupMapper::fromMapEntry).toList()) + .numberOfAttachments(attachments.size()) + .numberOfRepresentations(representations.size()) .build(); } diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f4bacd0b2c16dc6db7749bfe7a49e6b20473aebf --- /dev/null +++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/EingangStubReceiverStreamObserverTest.java @@ -0,0 +1,1024 @@ +/* + * 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.eingang.forwarder; + +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.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.test.util.ReflectionTestUtils; + +import de.ozgcloud.common.errorhandling.TechnicalException; +import de.ozgcloud.eingang.common.formdata.FormData; +import de.ozgcloud.eingang.common.formdata.FormDataTestFactory; +import de.ozgcloud.eingang.common.formdata.IncomingFile; +import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory; +import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory; +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.GrpcRouteForwardingResponse; +import lombok.SneakyThrows; + +class EingangStubReceiverStreamObserverTest { + + private EingangStubReceiverStreamObserver observer; + + @Mock + private RouteForwardingMapper routeForwardingMapper; + @Mock + private IncomingFileMapper incomingFileMapper; + @Mock + private IncomingFileGroupMapper incomingFileGroupMapper; + @Mock + private Function<InputStream, CompletableFuture<File>> fileSaver; + @Mock + private Consumer<FormData> formDataConsumer; + @Mock + private Consumer<GrpcRouteForwardingResponse> responseConsumer; + + @BeforeEach + void setUp() { + observer = spy(EingangStubReceiverStreamObserver.builder() + .fileSaver(fileSaver) + .routeForwardingMapper(routeForwardingMapper) + .incomingFileMapper(incomingFileMapper) + .incomingFileGroupMapper(incomingFileGroupMapper) + .formDataConsumer(formDataConsumer) + .responseConsumer(responseConsumer) + .build()); + } + + @Nested + class TestOnNext { + + @Nested + class TestOnRouteForwarding { + + @BeforeEach + void mock() { + doNothing().when(observer).handleRouteForwarding(any()); + } + + @Test + void shouldCallHandleRouteForwarding() { + observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRouteForwarding()); + + verify(observer).handleRouteForwarding(GrpcRouteForwardingRequestTestFactory.ROUTE_FORWARDING); + } + + @Test + void shouldNotCallHandleAttachment() { + observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRouteForwarding()); + + verify(observer, never()).handleAttachment(any()); + } + + @Test + void shouldNotCallHandleRepresentation() { + observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRouteForwarding()); + + verify(observer, never()).handleRepresentation(any()); + } + } + + @Nested + class TestOnAttachment { + @Test + void shouldCallHandleAttachment() { + observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithAttachment()); + + verify(observer).handleAttachment(GrpcRouteForwardingRequestTestFactory.ATTACHMENT); + } + + @Test + void shouldNotCallHandleRouteForwarding() { + observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithAttachment()); + + verify(observer, never()).handleRouteForwarding(any()); + } + + @Test + void shouldNotCallHandleRepresentation() { + observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithAttachment()); + + verify(observer, never()).handleRepresentation(any()); + } + } + + @Nested + class TestOnRepresentation { + @Test + void shouldCallHandleRepresentation() { + observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRepresentation()); + + verify(observer).handleRepresentation(GrpcRouteForwardingRequestTestFactory.REPRESENTATION); + } + + @Test + void shouldNotCallHandleRouteForwarding() { + observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRepresentation()); + + verify(observer, never()).handleRouteForwarding(any()); + } + + @Test + void shouldNotCallHandleAttachment() { + observer.onNext(GrpcRouteForwardingRequestTestFactory.createWithRepresentation()); + + verify(observer, never()).handleAttachment(any()); + } + } + } + + @Nested + class TestHandleRouteForwarding { + + private final GrpcRouteForwarding routeForwarding = GrpcRouteForwardingTestFactory.create(); + + @Test + void shouldThrowIllegalStateExceptionIfFormDataIsSet() { + setFormData(FormDataTestFactory.create()); + + assertThrows(IllegalStateException.class, () -> observer.handleRouteForwarding(routeForwarding)); + + } + + @Test + void shouldMapRouteForwarding() { + observer.handleRouteForwarding(routeForwarding); + + verify(routeForwardingMapper).toFormData(routeForwarding); + } + + @Test + void shouldSetFormData() { + var formData = FormDataTestFactory.create(); + when(routeForwardingMapper.toFormData(any())).thenReturn(formData); + + observer.handleRouteForwarding(routeForwarding); + + assertThat(getFormData()).isEqualTo(formData); + } + } + + @Nested + class TestHandleAttachment { + + @Nested + class TestWithFile { + + private final GrpcAttachment attachmentWithFile = GrpcAttachmentTestFactory.createWithFile(); + private final IncomingFile incomingFile = IncomingFileTestFactory.create(); + + @BeforeEach + void mock() { + doNothing().when(observer).setCurrentMetadata(any()); + when(incomingFileMapper.fromGrpcAttachmentFile(any())).thenReturn(incomingFile); + } + + @Test + void shouldCallIncomingFileMapper() { + observer.handleAttachment(attachmentWithFile); + + verify(incomingFileMapper).fromGrpcAttachmentFile(GrpcAttachmentTestFactory.FILE); + } + + @Test + void shouldCallSetCurrentMetadata() { + observer.handleAttachment(attachmentWithFile); + + verify(observer).setCurrentMetadata(incomingFile); + } + + @Test + void shouldSetGroupName() { + observer.handleAttachment(attachmentWithFile); + + assertThat(getGroupName()).contains(GrpcAttachmentFileTestFactory.GROUP_NAME); + } + + @Test + void shouldNotCallHandleFileContent() { + observer.handleAttachment(attachmentWithFile); + + verify(observer, never()).handleFileContent(any()); + } + } + + @Nested + class TestWithContent { + + private final GrpcAttachment attachmentWithContent = GrpcAttachmentTestFactory.createWithContent(); + + @BeforeEach + void mock() { + doNothing().when(observer).handleFileContent(any()); + } + + @Test + void shouldCallHandleFileContent() { + observer.handleAttachment(attachmentWithContent); + + verify(observer).handleFileContent(GrpcAttachmentTestFactory.CONTENT); + } + + @Test + void shouldNotCallIncomingFileMapper() { + observer.handleAttachment(attachmentWithContent); + + verify(incomingFileMapper, never()).fromGrpcAttachmentFile(any()); + } + + @Test + void shouldNotSetGroupName() { + observer.handleAttachment(attachmentWithContent); + + assertThat(getGroupName()).isEmpty(); + } + } + } + + @Nested + class TestHandleRepresentation { + + @Nested + class TestWithFile { + + private final GrpcRepresentation representationWithFile = GrpcRepresentationTestFactory.createWithFile(); + private final IncomingFile incomingFile = IncomingFileTestFactory.create(); + + @BeforeEach + void mock() { + doNothing().when(observer).setCurrentMetadata(any()); + when(incomingFileMapper.fromGrpcRepresentationFile(any())).thenReturn(incomingFile); + } + + @Test + void shouldCallIncomingFileMapper() { + observer.handleRepresentation(representationWithFile); + + verify(incomingFileMapper).fromGrpcRepresentationFile(GrpcRepresentationTestFactory.FILE); + } + + @Test + void shouldCallSetCurrentMetadata() { + observer.handleRepresentation(representationWithFile); + + verify(observer).setCurrentMetadata(incomingFile); + } + + @Test + void shouldNotCallHandleFileContent() { + observer.handleRepresentation(representationWithFile); + + verify(observer, never()).handleFileContent(any()); + } + } + + @Nested + class TestWithContent { + + private final GrpcRepresentation representationWithContent = GrpcRepresentationTestFactory.createWithContent(); + + @BeforeEach + void mock() { + doNothing().when(observer).handleFileContent(any()); + } + + @Test + void shouldCallHandleFileContent() { + observer.handleRepresentation(representationWithContent); + + verify(observer).handleFileContent(GrpcRepresentationTestFactory.CONTENT); + } + + @Test + void shouldNotCallIncomingFileMapper() { + observer.handleRepresentation(representationWithContent); + + verify(incomingFileMapper, never()).fromGrpcRepresentationFile(any()); + } + } + } + + @Nested + class TestSetCurrentMetadata { + + private final IncomingFile incomingFile = IncomingFileTestFactory.create(); + + @Test + void shouldThrowIllegalStateExceptionIfCurrentFileIsSet() { + setCurrentFile(incomingFile); + + assertThrows(IllegalStateException.class, () -> observer.setCurrentMetadata(incomingFile)); + } + + @Test + void shouldSetCurrentFile() { + observer.setCurrentMetadata(incomingFile); + + assertThat(getCurrentFile()).isSameAs(incomingFile); + } + } + + @Nested + class TestHandleFileContent { + + private final GrpcFileContent fileContent = GrpcFileContentTestFactory.create(); + + @Nested + class TestOnReceivingFileContentIsNull { + @BeforeEach + void mock() { + doNothing().when(observer).initContentReceiving(); + doNothing().when(observer).storeFileContent(any()); + } + + @Test + void shouldCallInitContentReceiving() { + observer.handleFileContent(fileContent); + + verify(observer).initContentReceiving(); + } + + @Test + void shouldCallStoreFileContent() { + observer.handleFileContent(fileContent); + + verify(observer).storeFileContent(fileContent); + } + } + + @Nested + class TestOnReceivingFileContentIsNotNull { + @Mock + private CompletableFuture<File> receivingFileContent; + + @BeforeEach + void mock() { + doNothing().when(observer).storeFileContent(any()); + setFileContent(receivingFileContent); + } + + @Test + void shouldNotCallInitContentReceiving() { + observer.handleFileContent(fileContent); + + verify(observer, never()).initContentReceiving(); + } + + @Test + void shouldCallStoreFileContent() { + observer.handleFileContent(fileContent); + + verify(observer).storeFileContent(fileContent); + } + } + } + + @Nested + class TestInitContentReceiving { + + private final byte[] content = new byte[] { 1, 2, 3 }; + + @Test + void shouldCreateInputStream() { + observer.initContentReceiving(); + + assertThat(getPipedInput()).isNotNull(); + } + + @Test + void shouldCreateOutputStream() { + observer.initContentReceiving(); + + assertThat(getPipedOutput()).isNotNull(); + } + + @Test + void shouldCreateConnectedStreams() { + observer.initContentReceiving(); + + verifyStreamSetUp(); + } + + @SneakyThrows + private void verifyStreamSetUp() { + var pipedInput = getPipedInput(); + var pipedOutput = getPipedOutput(); + pipedOutput.write(content); + pipedOutput.close(); + var readBytes = pipedInput.readAllBytes(); + assertThat(readBytes).isEqualTo(content); + } + + @Test + void shouldCallFileSaver() { + observer.initContentReceiving(); + + verify(fileSaver).apply(getPipedInput()); + } + + @Test + void shouldSetReceivingFileContent() { + var fileFuture = CompletableFuture.completedFuture(mock(File.class)); + when(fileSaver.apply(any())).thenReturn(fileFuture); + + observer.initContentReceiving(); + + assertThat(getFileContent()).isSameAs(fileFuture); + } + } + + @Nested + class TestStoreFileContent { + @Mock + private PipedOutputStream pipedOutput; + + @BeforeEach + void setUp() { + setPipedOutput(pipedOutput); + } + + @Nested + class TestOnCurrentFileIsNull { + + @Test + void shouldThrowTechnicalException() { + var fileContent = GrpcFileContentTestFactory.create(); + + assertThrows(IllegalStateException.class, () -> observer.storeFileContent(fileContent)); + } + } + + @Nested + class TestOnCurrentFileIsNotNull { + + private final IncomingFile incomingFile = IncomingFileTestFactory.create(); + + @BeforeEach + void mock() { + setCurrentFile(incomingFile); + } + + @Test + @SneakyThrows + void shouldWriteContentToOutputStream() { + observer.storeFileContent(GrpcFileContentTestFactory.create()); + + verify(pipedOutput).write(GrpcFileContentTestFactory.CONTENT); + } + + @Test + void shouldCallHandleEndOfFile() { + doNothing().when(observer).handleEndOfFile(); + var fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(true).build(); + + observer.storeFileContent(fileContent); + + verify(observer).handleEndOfFile(); + } + + @Test + void shouldNotCallHandleEndOfFile() { + var fileContent = GrpcFileContentTestFactory.createBuilder().setIsEndOfFile(false).build(); + + observer.storeFileContent(fileContent); + + verify(observer, never()).handleEndOfFile(); + } + + @Test + @SneakyThrows + void shouldThrowTechnicalExceptionOnIOException() { + doThrow(new IOException()).when(pipedOutput).write(any()); + var fileContent = GrpcFileContentTestFactory.create(); + + assertThrows(TechnicalException.class, () -> { + observer.storeFileContent(fileContent); + }); + } + } + } + + @Nested + class TestHandleEndOfFile { + + @Mock + private File savedFileContent; + + private final IncomingFile incomingFile = IncomingFileTestFactory.createBuilder().file(null).build(); + + @BeforeEach + void setUp() { + doNothing().when(observer).closeOutputPipe(); + doReturn(savedFileContent).when(observer).getSavedFileContent(); + setCurrentFile(incomingFile); + } + + @Test + void shouldCallCloseOutputPipe() { + observer.handleEndOfFile(); + + verify(observer).closeOutputPipe(); + } + + @Nested + class TestOnGroupNameEmpty { + + @BeforeEach + void setUp() { + setGroupName(Optional.empty()); + } + + @Test + void shouldAddFileToRepresentations() { + var expectedIncomingFile = IncomingFileTestFactory.createBuilder().file(savedFileContent).build(); + + observer.handleEndOfFile(); + + assertThat(getRepresentations()).usingRecursiveFieldByFieldElementComparator().containsExactly(expectedIncomingFile); + } + } + + @Nested + class TestOnGroupNameSet { + + @BeforeEach + void setUp() { + setGroupName(Optional.of(GrpcAttachmentFileTestFactory.GROUP_NAME)); + } + + @Test + void shouldAddFileToAttachments() { + var expectedIncomingFile = IncomingFileTestFactory.createBuilder().file(savedFileContent).build(); + + observer.handleEndOfFile(); + + var attachmentGroup = getAttachments().get(GrpcAttachmentFileTestFactory.GROUP_NAME); + assertThat(attachmentGroup).usingRecursiveFieldByFieldElementComparator().containsExactly(expectedIncomingFile); + } + } + + @Test + void shouldCallResetFileReceiving() { + observer.handleEndOfFile(); + + verify(observer).resetFileReceiving(); + } + } + + @Nested + class TestGetSavedFileContent { + @BeforeEach + void setUp() { + doNothing().when(observer).closeInputPipe(); + } + + @Nested + class TestOnNoExceptions { + @Mock + private File fileContent; + + @BeforeEach + void setUp() { + setFileContent(CompletableFuture.completedFuture(fileContent)); + } + + @Test + void shouldReturnFile() { + var savedFileContent = observer.getSavedFileContent(); + + assertThat(savedFileContent).isSameAs(fileContent); + } + + @Test + void shouldCallCloseInputPipe() { + observer.getSavedFileContent(); + + verify(observer).closeInputPipe(); + } + } + + @Nested + class TestOnExecutionException { + + @BeforeEach + void setUp() { + setFileContent(CompletableFuture.failedFuture(new Exception())); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, () -> observer.getSavedFileContent()); + } + + @Test + void shouldCallCloseInputPipe() { + try { + observer.getSavedFileContent(); + } catch (TechnicalException e) { + // expected + } + + verify(observer).closeInputPipe(); + } + } + + @Nested + class TestOnTimeoutException { + + @Mock + private CompletableFuture<File> fileFuture; + + @BeforeEach + @SneakyThrows + void setUp() { + setFileContent(fileFuture); + when(fileFuture.get(anyLong(), any())).thenThrow(new TimeoutException()); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, () -> observer.getSavedFileContent()); + } + + @Test + void shouldCallCloseInputPipe() { + try { + observer.getSavedFileContent(); + } catch (TechnicalException e) { + // expected + } + + verify(observer).closeInputPipe(); + } + } + + @Nested + class TestOnInterruptedException { + + @Mock + private CompletableFuture<File> fileFuture; + + @BeforeEach + @SneakyThrows + void setUp() { + setFileContent(fileFuture); + when(fileFuture.get(anyLong(), any())).thenThrow(new InterruptedException()); + } + + @Test + void shouldThrowTechnicalException() { + assertThrows(TechnicalException.class, () -> observer.getSavedFileContent()); + } + + @Test + void shouldInterruptCurrentThread() { + try { + observer.getSavedFileContent(); + } catch (TechnicalException e) { + // expected + } + + assertThat(Thread.currentThread().isInterrupted()).isTrue(); + } + + @Test + void shouldCallCloseInputPipe() { + try { + observer.getSavedFileContent(); + } catch (TechnicalException e) { + // expected + } + + verify(observer).closeInputPipe(); + } + } + } + + @Nested + class TestResetFielReceiving { + + @BeforeEach + void setUp() { + setCurrentFile(IncomingFileTestFactory.create()); + setGroupName(Optional.of(GrpcAttachmentFileTestFactory.GROUP_NAME)); + setPipedOutput(mock(PipedOutputStream.class)); + setPipedOutput(mock(PipedOutputStream.class)); + setFileContent(CompletableFuture.completedFuture(mock(File.class))); + } + + @Test + void shouldResetCurrentFile() { + observer.resetFileReceiving(); + + assertThat(getCurrentFile()).isNull(); + } + + @Test + void shouldResetGroupName() { + observer.resetFileReceiving(); + + assertThat(getGroupName()).isEmpty(); + } + + @Test + void shouldResetPipedOutput() { + observer.resetFileReceiving(); + + assertThat(getPipedOutput()).isNull(); + } + + @Test + void shouldResetPipedInput() { + observer.resetFileReceiving(); + + assertThat(getPipedInput()).isNull(); + } + + @Test + void shouldResetReceivingFileContent() { + observer.resetFileReceiving(); + + assertThat(getFileContent()).isNull(); + } + } + + @Nested + class TestOnError { + + @BeforeEach + void mock() { + doNothing().when(observer).closeOutputPipe(); + doNothing().when(observer).closeInputPipe(); + } + + @Test + void shouldCallCloseOutputPipe() { + observer.onError(new Exception()); + + verify(observer).closeOutputPipe(); + } + + @Test + void shouldCallCloseInputPipe() { + observer.onError(new Exception()); + + verify(observer).closeInputPipe(); + } + } + + @Nested + class TestCloseOutputPipe { + + @Mock + private PipedOutputStream pipedOutput; + + @BeforeEach + void setUp() { + setPipedOutput(pipedOutput); + } + + @Test + @SneakyThrows + void shouldClosePipedOutput() { + observer.closeOutputPipe(); + + verify(pipedOutput).close(); + } + + @Test + @SneakyThrows + void shouldNotThrowException() { + doThrow(IOException.class).when(pipedOutput).close(); + + assertDoesNotThrow(() -> observer.closeOutputPipe()); + } + } + + @Nested + class TestCloseInputPipe { + + @Mock + private PipedInputStream pipedInput; + + @BeforeEach + void setUp() { + setPipedInput(pipedInput); + } + + @Test + @SneakyThrows + void shouldClosePipedInput() { + observer.closeInputPipe(); + + verify(pipedInput).close(); + } + + @Test + @SneakyThrows + void shouldNotThrowException() { + doThrow(IOException.class).when(pipedInput).close(); + + assertDoesNotThrow(() -> observer.closeInputPipe()); + } + } + + @Nested + class TestOnCompleted { + + private final FormData formData = FormDataTestFactory.create(); + + @BeforeEach + void mock() { + doReturn(formData).when(observer).assembleFormData(); + } + + @Test + void shouldCallAssembleFormData() { + observer.onCompleted(); + + verify(observer).assembleFormData(); + } + + @Test + void shouldCallFormDataConsumer() { + observer.onCompleted(); + + verify(formDataConsumer).accept(formData); + } + + @Test + void shouldCallResponseConsumer() { + observer.onCompleted(); + + verify(responseConsumer).accept(GrpcRouteForwardingResponse.getDefaultInstance()); + } + } + + @Nested + class TestAssembleFormData { + + @Nested + class TestOnFormDataNotSet { + + @Test + void shouldThrowIllegalStateException() { + assertThrows(IllegalStateException.class, () -> observer.assembleFormData()); + } + } + + @Nested + class TestOnFormDataSet { + + private final FormData formData = FormDataTestFactory.createBuilder() + .clearAttachments() + .clearRepresentations() + .numberOfAttachments(0) + .numberOfRepresentations(0) + .control(null) + .build(); + + private final List<IncomingFile> representations = FormDataTestFactory.REPRESENTATIONS; + private final Map.Entry<String, List<IncomingFile>> attachmentEntry = Map.entry( + IncomingFileGroupTestFactory.NAME, IncomingFileGroupTestFactory.INCOMING_FILES); + + @BeforeEach + void setUp() { + setFormData(formData); + setRepresentations(representations); + setAttachments(Map.ofEntries(attachmentEntry)); + } + + @Test + void shouldCallIncomingFileGroupMapper() { + observer.assembleFormData(); + + verify(incomingFileGroupMapper).fromMapEntry(attachmentEntry); + } + + @Test + void shouldReturnFormData() { + var expectedFormData = FormDataTestFactory.createBuilder() + .control(null) + .build(); + when(incomingFileGroupMapper.fromMapEntry(attachmentEntry)).thenReturn(IncomingFileGroupTestFactory.create()); + + var assembledFormData = observer.assembleFormData(); + + assertThat(assembledFormData).usingRecursiveComparison().isEqualTo(expectedFormData); + } + } + } + + private FormData getFormData() { + return (FormData) ReflectionTestUtils.getField(observer, "formData"); + } + + private void setFormData(FormData formData) { + ReflectionTestUtils.setField(observer, "formData", formData); + } + + @SuppressWarnings("unchecked") + private Optional<String> getGroupName() { + return (Optional<String>) ReflectionTestUtils.getField(observer, "groupName"); + } + + private void setGroupName(Optional<String> groupName) { + ReflectionTestUtils.setField(observer, "groupName", groupName); + } + + private IncomingFile getCurrentFile() { + return (IncomingFile) ReflectionTestUtils.getField(observer, "currentFile"); + } + + private void setCurrentFile(IncomingFile incomingFile) { + ReflectionTestUtils.setField(observer, "currentFile", incomingFile); + } + + private void setFileContent(CompletableFuture<File> fileFuture) { + ReflectionTestUtils.setField(observer, "receivingFileContent", fileFuture); + } + + @SuppressWarnings("unchecked") + private CompletableFuture<File> getFileContent() { + return (CompletableFuture<File>) ReflectionTestUtils.getField(observer, "receivingFileContent"); + } + + private PipedInputStream getPipedInput() { + return (PipedInputStream) ReflectionTestUtils.getField(observer, "pipedInput"); + } + + private void setPipedInput(PipedInputStream pipedInput) { + ReflectionTestUtils.setField(observer, "pipedInput", pipedInput); + } + + private PipedOutputStream getPipedOutput() { + return (PipedOutputStream) ReflectionTestUtils.getField(observer, "pipedOutput"); + } + + private void setPipedOutput(PipedOutputStream pipedOutput) { + ReflectionTestUtils.setField(observer, "pipedOutput", pipedOutput); + } + + @SuppressWarnings("unchecked") + private List<IncomingFile> getRepresentations() { + return (List<IncomingFile>) ReflectionTestUtils.getField(observer, "representations"); + } + + private void setRepresentations(List<IncomingFile> representations) { + ReflectionTestUtils.setField(observer, "representations", representations); + } + + @SuppressWarnings("unchecked") + private Map<String, List<IncomingFile>> getAttachments() { + return (Map<String, List<IncomingFile>>) ReflectionTestUtils.getField(observer, "attachments"); + } + + private void setAttachments(Map<String, List<IncomingFile>> attachments) { + ReflectionTestUtils.setField(observer, "attachments", attachments); + } + +} diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java index 12382dcfeb61d76e40707785b00ab67c5c683308..ec48f44e48beb0c2d0cd072050bd7494a1effac3 100644 --- a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java +++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentFileTestFactory.java @@ -23,15 +23,14 @@ */ package de.ozgcloud.eingang.forwarder; -import com.thedeanda.lorem.LoremIpsum; - +import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory; import de.ozgcloud.eingang.common.formdata.IncomingFileTestFactory; import de.ozgcloud.eingang.forwarding.GrpcAttachmentFile; import de.ozgcloud.eingang.forwarding.GrpcAttachmentFile.Builder; public class GrpcAttachmentFileTestFactory { - public static final String GROUP_NAME = LoremIpsum.getInstance().getWords(1); + public static final String GROUP_NAME = IncomingFileGroupTestFactory.NAME; public static GrpcAttachmentFile create() { return createBuilder().build(); diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e5593e13f415f71ceee9fd21e5fb548383f7d8eb --- /dev/null +++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcAttachmentTestFactory.java @@ -0,0 +1,47 @@ +/* + * 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.eingang.forwarder; + +import de.ozgcloud.eingang.forwarding.GrpcAttachment; +import de.ozgcloud.eingang.forwarding.GrpcAttachmentFile; +import de.ozgcloud.eingang.forwarding.GrpcFileContent; + +public class GrpcAttachmentTestFactory { + + public static final GrpcAttachmentFile FILE = GrpcAttachmentFileTestFactory.create(); + public static final GrpcFileContent CONTENT = GrpcFileContentTestFactory.create(); + + public static GrpcAttachment createWithFile() { + return GrpcAttachment.newBuilder() + .setFile(FILE) + .build(); + } + + public static GrpcAttachment createWithContent() { + return GrpcAttachment.newBuilder() + .setContent(CONTENT) + .build(); + } + +} diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcFileContentTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcFileContentTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..d4040f97517ce4c6a379d38eaeb173bc96bf90d0 --- /dev/null +++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcFileContentTestFactory.java @@ -0,0 +1,47 @@ +/* + * 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.eingang.forwarder; + +import com.google.protobuf.ByteString; +import com.thedeanda.lorem.LoremIpsum; + +import de.ozgcloud.eingang.forwarding.GrpcFileContent; +import de.ozgcloud.eingang.forwarding.GrpcFileContent.Builder; + +public class GrpcFileContentTestFactory { + + public static final boolean IS_END_OF_FILE = false; + public static final byte[] CONTENT = LoremIpsum.getInstance().getWords(10).getBytes(); + + public static GrpcFileContent create() { + return createBuilder().build(); + } + + public static Builder createBuilder() { + return GrpcFileContent.newBuilder() + .setContent(ByteString.copyFrom(CONTENT)) + .setIsEndOfFile(IS_END_OF_FILE); + } + +} diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e437c900d0a49cf0f714d4d7718a9f699d681af8 --- /dev/null +++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRepresentationTestFactory.java @@ -0,0 +1,46 @@ +/* + * 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.eingang.forwarder; + +import de.ozgcloud.eingang.forwarding.GrpcFileContent; +import de.ozgcloud.eingang.forwarding.GrpcRepresentation; +import de.ozgcloud.eingang.forwarding.GrpcRepresentationFile; + +public class GrpcRepresentationTestFactory { + + public static final GrpcRepresentationFile FILE = GrpcRepresentationFileTestFactory.create(); + public static final GrpcFileContent CONTENT = GrpcFileContentTestFactory.create(); + + public static GrpcRepresentation createWithFile() { + return GrpcRepresentation.newBuilder() + .setFile(FILE) + .build(); + } + + public static GrpcRepresentation createWithContent() { + return GrpcRepresentation.newBuilder() + .setContent(CONTENT) + .build(); + } +} diff --git a/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..a05ac9a84aa68f8deae2657d2e4e64fd37879291 --- /dev/null +++ b/forwarder/src/test/java/de/ozgcloud/eingang/forwarder/GrpcRouteForwardingRequestTestFactory.java @@ -0,0 +1,54 @@ +/* + * 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.eingang.forwarder; + +import de.ozgcloud.eingang.forwarding.GrpcAttachment; +import de.ozgcloud.eingang.forwarding.GrpcRepresentation; +import de.ozgcloud.eingang.forwarding.GrpcRouteForwarding; +import de.ozgcloud.eingang.forwarding.GrpcRouteForwardingRequest; + +public class GrpcRouteForwardingRequestTestFactory { + + public static final GrpcRepresentation REPRESENTATION = GrpcRepresentationTestFactory.createWithFile(); + public static final GrpcAttachment ATTACHMENT = GrpcAttachmentTestFactory.createWithFile(); + public static final GrpcRouteForwarding ROUTE_FORWARDING = GrpcRouteForwardingTestFactory.create(); + + public static GrpcRouteForwardingRequest createWithRouteForwarding() { + return GrpcRouteForwardingRequest.newBuilder() + .setRouteForwarding(ROUTE_FORWARDING) + .build(); + } + + public static GrpcRouteForwardingRequest createWithAttachment() { + return GrpcRouteForwardingRequest.newBuilder() + .setAttachment(ATTACHMENT) + .build(); + } + + public static GrpcRouteForwardingRequest createWithRepresentation() { + return GrpcRouteForwardingRequest.newBuilder() + .setRepresentation(REPRESENTATION) + .build(); + } +}