From 05a60eb47ba203e233f7b747cfeba6a88c773b49 Mon Sep 17 00:00:00 2001 From: Jan Zickermann <jan.zickermann@dataport.de> Date: Mon, 17 Feb 2025 14:39:41 +0100 Subject: [PATCH] OZG-4097 send-attachment: Wait until upload completed --- .../osiv2/exception/Osi2UploadException.java | 2 +- .../postfach/osiv2/model/Osi2FileUpload.java | 3 + .../osiv2/transfer/Osi2QuarantineService.java | 44 ++----- .../osiv2/transfer/Osi2ResponseMapper.java | 2 +- .../transfer/PostfachApiFacadeService.java | 3 +- .../postfach/osiv2/transfer/WaitUtil.java | 42 +++++++ .../osiv2/OsiPostfachRemoteServiceITCase.java | 20 ++- .../transfer/Osi2QuarantineServiceTest.java | 119 ++++++++++++++++++ .../transfer/Osi2ResponseMapperTest.java | 3 + .../PostfachApiFacadeServiceTest.java | 5 + .../postfach/osiv2/transfer/WaitUtilTest.java | 81 ++++++++++++ 11 files changed, 288 insertions(+), 36 deletions(-) create mode 100644 src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/WaitUtil.java create mode 100644 src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/WaitUtilTest.java diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2UploadException.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2UploadException.java index 447a048..10b9f2a 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2UploadException.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/exception/Osi2UploadException.java @@ -2,7 +2,7 @@ package de.ozgcloud.nachrichten.postfach.osiv2.exception; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus; -public class Osi2UploadException extends RuntimeException { +public class Osi2UploadException extends Exception { public Osi2UploadException(QuarantineStatus uploadStatus) { super(uploadStatus.toString()); } diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java index 5c3ddb4..b5bfd23 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/model/Osi2FileUpload.java @@ -23,4 +23,7 @@ public record Osi2FileUpload( return (file.getSize() + CHUNK_SIZE - 1) / CHUNK_SIZE; } + public String getLoggableString() { + return String.format("FileUpload(guid=%s, ozgFileId=%s, size=%d)", guid, file.getId(), file.getSize()); + } } diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java index ea638e3..d673927 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineService.java @@ -1,22 +1,19 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; +import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.WaitUtil.*; + import java.io.IOException; import java.io.InputStream; import java.time.Duration; import java.util.Comparator; import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.BooleanSupplier; import java.util.stream.LongStream; import java.util.stream.Stream; import de.ozgcloud.apilib.file.OzgCloudFile; import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException; import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; import de.ozgcloud.nachrichten.postfach.osiv2.storage.Osi2AttachmentFileService; @@ -80,37 +77,22 @@ public class Osi2QuarantineService { } synchronized boolean checkVirusScanCompleted(List<Osi2FileUpload> osi2FileMetadata) { - // TODO ... - return true; + return osi2FileMetadata.stream() + .allMatch(this::checkOneVirusScanCompleted); } - void waitForVirusScan(List<Osi2FileUpload> osi2FileMetadata) { + boolean checkOneVirusScanCompleted(Osi2FileUpload osi2FileMetadata) { try { - waitUntil(() -> checkVirusScanCompleted(osi2FileMetadata), POLLING_INTERVAL, POLLING_TIMEOUT); - } catch (ExecutionException e) { - if (e.getCause() instanceof RuntimeException runtimeException) { - throw runtimeException; - } - throw new IllegalStateException("Unexpected exception: Expect the scan to complete successfully!", e.getCause()); - } catch (TimeoutException e) { - throw new Osi2RuntimeException("Expect the scan to complete after %d seconds!".formatted(POLLING_TIMEOUT.getSeconds()), e); - } catch (InterruptedException e) { - LOG.debug("[waitForVirusScan] Interrupt"); - Thread.currentThread().interrupt(); + return postfachApiFacadeService.checkUploadSuccessful(osi2FileMetadata.guid()); + } catch (Osi2UploadException e) { + throw new Osi2RuntimeException("%s failed!".formatted(osi2FileMetadata.getLoggableString()), e); } } - void waitUntil(BooleanSupplier condition, Duration interval, Duration timeout) - throws ExecutionException, InterruptedException, TimeoutException { - CompletableFuture.runAsync(() -> { - try (var executor = Executors.newSingleThreadScheduledExecutor()) { - executor.scheduleAtFixedRate(() -> { - if (condition.getAsBoolean()) { - executor.shutdown(); - } - }, 0, interval.getSeconds(), TimeUnit.SECONDS); - } - }).get(timeout.getSeconds(), TimeUnit.SECONDS); + void waitForVirusScan(List<Osi2FileUpload> osi2FileMetadata) { + if (!waitUntil(() -> checkVirusScanCompleted(osi2FileMetadata), POLLING_INTERVAL, POLLING_TIMEOUT)) { + throw new Osi2RuntimeException("Expect the scan to complete after %d seconds!".formatted(POLLING_TIMEOUT.getSeconds())); + } } public void deleteAttachments(List<Osi2FileUpload> osi2FileMetadata) { diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapper.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapper.java index 0a29139..f64b1b1 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapper.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapper.java @@ -109,7 +109,7 @@ public interface Osi2ResponseMapper { .toList(); } - default boolean isSafe(QuarantineStatus uploadStatus) { + default boolean isSafe(QuarantineStatus uploadStatus) throws Osi2UploadException { return switch (uploadStatus) { case NONE, UNSAFE, CORRUPT, MISSING -> throw new Osi2UploadException(uploadStatus); case LEGACY, COMMITED -> false; diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java index 476b3c5..c27b95d 100644 --- a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeService.java @@ -10,6 +10,7 @@ import org.springframework.core.io.AbstractResource; import de.ozgcloud.nachrichten.postfach.PostfachNachricht; import de.ozgcloud.nachrichten.postfach.osiv2.ServiceIfOsi2Enabled; import de.ozgcloud.nachrichten.postfach.osiv2.config.Osi2PostfachProperties; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException; import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.MessageExchangeApi; import de.ozgcloud.nachrichten.postfach.osiv2.gen.api.QuarantineApi; import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; @@ -44,7 +45,7 @@ public class PostfachApiFacadeService { ); } - public boolean checkUploadSuccessful(final String messageId) { + public boolean checkUploadSuccessful(final String messageId) throws Osi2UploadException { return responseMapper.isSafe( quarantineApi.getUploadStatus(UUID.fromString(messageId)) ); diff --git a/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/WaitUtil.java b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/WaitUtil.java new file mode 100644 index 0000000..5acad33 --- /dev/null +++ b/src/main/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/WaitUtil.java @@ -0,0 +1,42 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.transfer; + +import java.time.Duration; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class WaitUtil { + + public static boolean waitUntil(BooleanSupplier condition, Duration interval, Duration timeout) { + var waitResult = new AtomicBoolean(false); + try (var executor = Executors.newSingleThreadScheduledExecutor()) { + executor.scheduleAtFixedRate(() -> { + if (condition.getAsBoolean()) { + waitResult.set(true); + executor.shutdown(); + } + }, 0, interval.toMillis(), TimeUnit.MILLISECONDS) + .get(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("[waitUntil] Interrupt!", e.getCause()); + } catch (TimeoutException | CancellationException e) { + // ignore + } catch (ExecutionException e) { + if (e.getCause() instanceof RuntimeException runtimeException) { + throw runtimeException; + } else { + throw new IllegalStateException("Unexpected exception while waiting for condition!", e.getCause()); + } + } + return waitResult.get(); + } +} diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java index b698bf7..04bef85 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/OsiPostfachRemoteServiceITCase.java @@ -34,6 +34,8 @@ import de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeReceiveMess import de.ozgcloud.nachrichten.postfach.osiv2.factory.MessageExchangeSendMessageResponseTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.PostfachNachrichtTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineFileResult; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus; import de.ozgcloud.nachrichten.postfach.osiv2.storage.Osi2AttachmentFileService; import lombok.SneakyThrows; @@ -107,8 +109,14 @@ class OsiPostfachRemoteServiceITCase { @Test void shouldSendMessageWithAttachment() { var textFileId = AttachmentExampleUploadUtil.uploadTextFile(osi2AttachmentFileService); - // TODO mock quarantine upload requests - + postfachFacadeMockServer.stubFor(post(urlPathTemplate("/Quarantine/v1/Upload/Chunked")) + .willReturn(okJsonObj(QuarantineFileResult.builder() + .success(true) + .build())) + ); + postfachFacadeMockServer.stubFor(get(urlPathTemplate("/Quarantine/v1/Upload/{guid}")) + .willReturn(okJsonObj(QuarantineStatus.SAFE)) + ); var postfachNachrichtWithAttachment = PostfachNachrichtTestFactory.createBuilder() .attachments(List.of(textFileId)) .build(); @@ -116,6 +124,14 @@ class OsiPostfachRemoteServiceITCase { osiPostfachRemoteService.sendMessage(postfachNachrichtWithAttachment); + postfachFacadeMockServer.verify( + exactly(1), + postRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/Chunked")) + ); + postfachFacadeMockServer.verify( + exactly(1), + postRequestedFor(urlPathTemplate("/Quarantine/v1/Upload/{guid}")) + ); postfachFacadeMockServer.verify( exactly(1), postRequestedFor(urlPathTemplate("/MessageExchange/v1/Send/{mailboxId}")) diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java index b6ead12..8794260 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2QuarantineServiceTest.java @@ -2,25 +2,32 @@ package de.ozgcloud.nachrichten.postfach.osiv2.transfer; import static de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload.*; import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2FileUploadTestFactory.*; +import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.Osi2QuarantineService.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.io.IOException; import java.io.InputStream; import java.util.List; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Spy; import de.ozgcloud.apilib.file.OzgCloudFile; import de.ozgcloud.apilib.file.OzgCloudFileId; import de.ozgcloud.apilib.file.OzgCloudFileTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2RuntimeException; +import de.ozgcloud.nachrichten.postfach.osiv2.exception.Osi2UploadException; +import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus; import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; import de.ozgcloud.nachrichten.postfach.osiv2.storage.Osi2AttachmentFileService; @@ -218,6 +225,118 @@ class Osi2QuarantineServiceTest { @Nested class TestCheckVirusScanCompleted { + private final Osi2FileUpload upload1 = Osi2FileUploadTestFactory.create(); + private final Osi2FileUpload upload2 = Osi2FileUploadTestFactory.createBuilder() + .guid(UUID.randomUUID().toString()) + .build(); + private final Osi2FileUpload upload3 = Osi2FileUploadTestFactory.createBuilder() + .guid(UUID.randomUUID().toString()) + .build(); + + private final List<Osi2FileUpload> uploads = List.of(upload1, upload2, upload3); + + @DisplayName("should return true if all virus scans completed") + @Test + void shouldReturnTrueIfAllVirusScansCompleted() { + doReturn(true).when(service).checkOneVirusScanCompleted(upload1); + doReturn(true).when(service).checkOneVirusScanCompleted(upload2); + doReturn(true).when(service).checkOneVirusScanCompleted(upload3); + + var result = service.checkVirusScanCompleted(uploads); + + assertThat(result).isTrue(); + } + + @DisplayName("should return false if one virus scan not completed") + @Test + void shouldReturnFalseIfOneVirusScanNotCompleted() { + doReturn(true).when(service).checkOneVirusScanCompleted(upload1); + doReturn(false).when(service).checkOneVirusScanCompleted(upload2); + + var result = service.checkVirusScanCompleted(uploads); + + assertThat(result).isFalse(); + } + } + + @DisplayName("check one virus scan completed") + @Nested + class TestCheckOneVirusScanCompleted { + private final Osi2FileUpload upload1 = Osi2FileUploadTestFactory.create(); + + @DisplayName("should call checkUploadSuccessful") + @Test + @SneakyThrows + void shouldCallCheckUploadSuccessful() { + service.checkOneVirusScanCompleted(upload1); + + verify(postfachApiFacadeService).checkUploadSuccessful(UPLOAD_GUID); + } + + @DisplayName("should return") + @ParameterizedTest + @ValueSource(booleans = { true, false }) + @SneakyThrows + void shouldReturn(boolean value) { + when(postfachApiFacadeService.checkUploadSuccessful(any())).thenReturn(value); + + var result = service.checkOneVirusScanCompleted(upload1); + + assertThat(result).isEqualTo(value); + } + + @DisplayName("should throw OsiRuntimeException with file metadata") + @Test + @SneakyThrows + void shouldThrowOsiRuntimeExceptionWithFileMetadata() { + var uploadException = new Osi2UploadException(QuarantineStatus.UNSAFE); + when(postfachApiFacadeService.checkUploadSuccessful(any())).thenThrow(uploadException); + + assertThatThrownBy(() -> service.checkOneVirusScanCompleted(upload1)) + .isInstanceOf(Osi2RuntimeException.class) + .hasMessageContaining(upload1.getLoggableString()) + .hasCause(uploadException); + } + } + + @DisplayName("wait for virus scan") + @Nested + class TestWaitForVirusScan { + private final Osi2FileUpload upload1 = Osi2FileUploadTestFactory.create(); + private final List<Osi2FileUpload> uploads = List.of(upload1); + + @DisplayName("should call waitUntil") + @Test + void shouldCallWaitUntil() { + doReturn(true).when(service).checkVirusScanCompleted(any()); + + service.waitForVirusScan(uploads); + + verify(service).checkVirusScanCompleted(uploads); + } + + @DisplayName("should call with polling interval and timeout") + @Test + void shouldCallWithPollingIntervalAndTimeout() { + try (MockedStatic<WaitUtil> mockedStatic = mockStatic(WaitUtil.class)) { + mockedStatic.when(() -> WaitUtil.waitUntil(any(), any(), any())).thenReturn(true); + + service.waitForVirusScan(uploads); + + mockedStatic.verify(() -> WaitUtil.waitUntil(any(), eq(POLLING_INTERVAL), eq(POLLING_TIMEOUT))); + } + } + + @DisplayName("should throw if scan not completed before timeout") + @Test + void shouldThrowIfScanNotCompletedBeforeTimeout() { + try (MockedStatic<WaitUtil> mockedStatic = mockStatic(WaitUtil.class)) { + mockedStatic.when(() -> WaitUtil.waitUntil(any(), any(), any())).thenReturn(false); + + assertThatThrownBy(() -> service.waitForVirusScan(uploads)) + .isInstanceOf(Osi2RuntimeException.class); + } + } } } \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapperTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapperTest.java index 1c82d18..0bc446f 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapperTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/Osi2ResponseMapperTest.java @@ -23,6 +23,7 @@ import de.ozgcloud.nachrichten.postfach.osiv2.factory.V1ReplyMessageTestFactory; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.QuarantineStatus; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyBehavior; import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyMessage; +import lombok.SneakyThrows; class Osi2ResponseMapperTest { @@ -211,6 +212,7 @@ class Osi2ResponseMapperTest { @DisplayName("should return false for unfinished status") @ParameterizedTest @ValueSource(strings = { "Legacy", "Commited" }) + @SneakyThrows void shouldReturnFalseForUnfinishedStatus(String status) { var uploadStatus = QuarantineStatus.fromValue(status); @@ -221,6 +223,7 @@ class Osi2ResponseMapperTest { @DisplayName("should return true for safe status") @Test + @SneakyThrows void shouldReturnTrueForSafeStatus() { var result = mapper.isSafe(QuarantineStatus.SAFE); diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java index 06f94a9..a0e6ba9 100644 --- a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/PostfachApiFacadeServiceTest.java @@ -34,6 +34,7 @@ import de.ozgcloud.nachrichten.postfach.osiv2.gen.model.V1ReplyMessage; import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkInfo; import de.ozgcloud.nachrichten.postfach.osiv2.model.FileChunkResource; import de.ozgcloud.nachrichten.postfach.osiv2.model.Osi2FileUpload; +import lombok.SneakyThrows; class PostfachApiFacadeServiceTest { @@ -254,6 +255,7 @@ class PostfachApiFacadeServiceTest { private final QuarantineStatus uploadStatus = QuarantineStatus.SAFE; @BeforeEach + @SneakyThrows void mock() { when(osi2ResponseMapper.isSafe(any())).thenReturn(true); when(quarantineApi.getUploadStatus(any())).thenReturn(uploadStatus); @@ -261,6 +263,7 @@ class PostfachApiFacadeServiceTest { @DisplayName("should call getUploadStatus") @Test + @SneakyThrows void shouldCallGetUploadStatus() { service.checkUploadSuccessful(MESSAGE_ID_1); @@ -269,6 +272,7 @@ class PostfachApiFacadeServiceTest { @DisplayName("should call isSafe") @Test + @SneakyThrows void shouldCallIsSafe() { service.checkUploadSuccessful(MESSAGE_ID_1); @@ -277,6 +281,7 @@ class PostfachApiFacadeServiceTest { @DisplayName("should return true") @Test + @SneakyThrows void shouldReturnTrue() { var result = service.checkUploadSuccessful(MESSAGE_ID_1); diff --git a/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/WaitUtilTest.java b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/WaitUtilTest.java new file mode 100644 index 0000000..c33e3e9 --- /dev/null +++ b/src/test/java/de/ozgcloud/nachrichten/postfach/osiv2/transfer/WaitUtilTest.java @@ -0,0 +1,81 @@ +package de.ozgcloud.nachrichten.postfach.osiv2.transfer; + +import static de.ozgcloud.nachrichten.postfach.osiv2.transfer.WaitUtil.*; +import static org.assertj.core.api.Assertions.*; +import static org.awaitility.Awaitility.*; + +import java.time.Duration; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import lombok.SneakyThrows; + +class WaitUtilTest { + + @DisplayName("wait until") + @Nested + class TestWaitUntil { + @DisplayName("should return true if check condition is eventually true") + @Test + void shouldReturnTrueIfCheckConditionIsEventuallyTrue() { + var delayFuture = createDelayedFuture(100); + + var conditionFuture = CompletableFuture.supplyAsync(() -> waitForDelayedCondition(delayFuture)); + + await() + .between(Duration.ofMillis(100), Duration.ofMillis(500)) + .until(conditionFuture::isDone); + assertThat(conditionFuture.getNow(false)).isTrue(); + } + + @DisplayName("should return false if check condition is not true before timeout") + @Test + void shouldReturnFalseIfCheckConditionIsNotTrueBeforeTimeout() { + var delayCondition = createDelayedFuture(1000); + + var conditionFuture = CompletableFuture.supplyAsync(() -> waitForDelayedCondition(delayCondition)); + + await() + .between(Duration.ofMillis(500), Duration.ofMillis(1000)) + .until(conditionFuture::isDone); + assertThat(conditionFuture.getNow(true)).isFalse(); + } + + @DisplayName("should rethrow runtime exceptions of condition") + @Test + void shouldRethrowRuntimeExceptionsOfCondition() { + var exception = new RuntimeException("test"); + + var conditionFuture = CompletableFuture.supplyAsync(() -> + waitUntil(() -> { + throw exception; + }, Duration.ofMillis(50), Duration.ofMillis(500))); + + await() + .atMost(Duration.ofMillis(500)) + .until(conditionFuture::isDone); + assertThatThrownBy(() -> conditionFuture.getNow(false)) + .hasCause(exception); + } + + private CompletableFuture<Boolean> createDelayedFuture(long milliseconds) { + return CompletableFuture.supplyAsync(() -> { + try { + Thread.sleep(milliseconds); + return true; + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + } + + @SneakyThrows + private boolean waitForDelayedCondition(CompletableFuture<Boolean> future) { + return waitUntil(future::isDone, Duration.ofMillis(50), Duration.ofMillis(500)); + } + } + +} \ No newline at end of file -- GitLab